b458c6c7e2a932fb8bf3cebe9f6c54b76c1f9a49
1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
4 from roundup.i18n import _
6 try:
7 import cPickle as pickle
8 except ImportError:
9 import pickle
10 try:
11 import cStringIO as StringIO
12 except ImportError:
13 import StringIO
14 try:
15 import StructuredText
16 except ImportError:
17 StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 # XXX WAH pagetemplates aren't pickleable :(
26 #def getTemplate(dir, name, classname=None, request=None):
27 # ''' Interface to get a template, possibly loading a compiled template.
28 # '''
29 # # source
30 # src = os.path.join(dir, name)
31 #
32 # # see if we can get a compile from the template"c" directory (most
33 # # likely is "htmlc"
34 # split = list(os.path.split(dir))
35 # split[-1] = split[-1] + 'c'
36 # cdir = os.path.join(*split)
37 # split.append(name)
38 # cpl = os.path.join(*split)
39 #
40 # # ok, now see if the source is newer than the compiled (or if the
41 # # compiled even exists)
42 # MTIME = os.path.stat.ST_MTIME
43 # if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
44 # # nope, we need to compile
45 # pt = RoundupPageTemplate()
46 # pt.write(open(src).read())
47 # pt.id = name
48 #
49 # # save off the compiled template
50 # if not os.path.exists(cdir):
51 # os.makedirs(cdir)
52 # f = open(cpl, 'wb')
53 # pickle.dump(pt, f)
54 # f.close()
55 # else:
56 # # yay, use the compiled template
57 # f = open(cpl, 'rb')
58 # pt = pickle.load(f)
59 # return pt
61 templates = {}
63 class NoTemplate(Exception):
64 pass
66 def getTemplate(dir, name, extension, classname=None, request=None):
67 ''' Interface to get a template, possibly loading a compiled template.
69 "name" and "extension" indicate the template we're after, which in
70 most cases will be "name.extension". If "extension" is None, then
71 we look for a template just called "name" with no extension.
73 If the file "name.extension" doesn't exist, we look for
74 "_generic.extension" as a fallback.
75 '''
76 # default the name to "home"
77 if name is None:
78 name = 'home'
80 # find the source, figure the time it was last modified
81 if extension:
82 filename = '%s.%s'%(name, extension)
83 else:
84 filename = name
85 src = os.path.join(dir, filename)
86 try:
87 stime = os.stat(src)[os.path.stat.ST_MTIME]
88 except os.error, error:
89 if error.errno != errno.ENOENT:
90 raise
91 if not extension:
92 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
94 # try for a generic template
95 generic = '_generic.%s'%extension
96 src = os.path.join(dir, generic)
97 try:
98 stime = os.stat(src)[os.path.stat.ST_MTIME]
99 except os.error, error:
100 if error.errno != errno.ENOENT:
101 raise
102 # nicer error
103 raise NoTemplate, 'No template file exists for templating '\
104 '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
105 extension, filename, generic)
106 filename = generic
108 key = (dir, filename)
109 if templates.has_key(key) and stime < templates[key].mtime:
110 # compiled template is up to date
111 return templates[key]
113 # compile the template
114 templates[key] = pt = RoundupPageTemplate()
115 pt.write(open(src).read())
116 pt.id = filename
117 pt.mtime = time.time()
118 return pt
120 class RoundupPageTemplate(PageTemplate.PageTemplate):
121 ''' A Roundup-specific PageTemplate.
123 Interrogate the client to set up the various template variables to
124 be available:
126 *context*
127 this is one of three things:
128 1. None - we're viewing a "home" page
129 2. The current class of item being displayed. This is an HTMLClass
130 instance.
131 3. The current item from the database, if we're viewing a specific
132 item, as an HTMLItem instance.
133 *request*
134 Includes information about the current request, including:
135 - the url
136 - the current index information (``filterspec``, ``filter`` args,
137 ``properties``, etc) parsed out of the form.
138 - methods for easy filterspec link generation
139 - *user*, the current user node as an HTMLItem instance
140 - *form*, the current CGI form information as a FieldStorage
141 *instance*
142 The current instance
143 *db*
144 The current database, through which db.config may be reached.
145 '''
146 def getContext(self, client, classname, request):
147 c = {
148 'options': {},
149 'nothing': None,
150 'request': request,
151 'content': client.content,
152 'db': HTMLDatabase(client),
153 'instance': client.instance
154 }
155 # add in the item if there is one
156 if client.nodeid:
157 c['context'] = HTMLItem(client, classname, client.nodeid)
158 else:
159 c['context'] = HTMLClass(client, classname)
160 return c
162 def render(self, client, classname, request, **options):
163 """Render this Page Template"""
165 if not self._v_cooked:
166 self._cook()
168 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
170 if self._v_errors:
171 raise PageTemplate.PTRuntimeError, \
172 'Page Template %s has errors.' % self.id
174 # figure the context
175 classname = classname or client.classname
176 request = request or HTMLRequest(client)
177 c = self.getContext(client, classname, request)
178 c.update({'options': options})
180 # and go
181 output = StringIO.StringIO()
182 TALInterpreter(self._v_program, self._v_macros,
183 getEngine().getContext(c), output, tal=1, strictinsert=0)()
184 return output.getvalue()
186 class HTMLDatabase:
187 ''' Return HTMLClasses for valid class fetches
188 '''
189 def __init__(self, client):
190 self._client = client
192 # we want config to be exposed
193 self.config = client.db.config
195 def __getattr__(self, attr):
196 try:
197 self._client.db.getclass(attr)
198 except KeyError:
199 raise AttributeError, attr
200 return HTMLClass(self._client, attr)
201 def classes(self):
202 l = self._client.db.classes.keys()
203 l.sort()
204 return [HTMLClass(self._client, cn) for cn in l]
206 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
207 cl = db.getclass(prop.classname)
208 l = []
209 for entry in ids:
210 if num_re.match(entry):
211 l.append(entry)
212 else:
213 l.append(cl.lookup(entry))
214 return l
216 class HTMLClass:
217 ''' Accesses through a class (either through *class* or *db.<classname>*)
218 '''
219 def __init__(self, client, classname):
220 self._client = client
221 self._db = client.db
223 # we want classname to be exposed
224 self.classname = classname
225 if classname is not None:
226 self._klass = self._db.getclass(self.classname)
227 self._props = self._klass.getprops()
229 def __repr__(self):
230 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
232 def __getitem__(self, item):
233 ''' return an HTMLProperty instance
234 '''
235 #print 'HTMLClass.getitem', (self, item)
237 # we don't exist
238 if item == 'id':
239 return None
241 # get the property
242 prop = self._props[item]
244 # look up the correct HTMLProperty class
245 form = self._client.form
246 for klass, htmlklass in propclasses:
247 if not isinstance(prop, klass):
248 continue
249 if form.has_key(item):
250 if isinstance(prop, hyperdb.Multilink):
251 value = lookupIds(self._db, prop,
252 handleListCGIValue(form[item]))
253 elif isinstance(prop, hyperdb.Link):
254 value = form[item].value.strip()
255 if value:
256 value = lookupIds(self._db, prop, [value])[0]
257 else:
258 value = None
259 else:
260 value = form[item].value.strip() or None
261 else:
262 if isinstance(prop, hyperdb.Multilink):
263 value = []
264 else:
265 value = None
266 print (prop, value)
267 return htmlklass(self._client, '', prop, item, value)
269 # no good
270 raise KeyError, item
272 def __getattr__(self, attr):
273 ''' convenience access '''
274 try:
275 return self[attr]
276 except KeyError:
277 raise AttributeError, attr
279 def properties(self):
280 ''' Return HTMLProperty for all props
281 '''
282 l = []
283 for name, prop in self._props.items():
284 for klass, htmlklass in propclasses:
285 if isinstance(prop, hyperdb.Multilink):
286 value = []
287 else:
288 value = None
289 if isinstance(prop, klass):
290 l.append(htmlklass(self._client, '', prop, name, value))
291 return l
293 def list(self):
294 if self.classname == 'user':
295 klass = HTMLUser
296 else:
297 klass = HTMLItem
298 l = [klass(self._client, self.classname, x) for x in self._klass.list()]
299 return l
301 def csv(self):
302 ''' Return the items of this class as a chunk of CSV text.
303 '''
304 # get the CSV module
305 try:
306 import csv
307 except ImportError:
308 return 'Sorry, you need the csv module to use this function.\n'\
309 'Get it from: http://www.object-craft.com.au/projects/csv/'
311 props = self.propnames()
312 p = csv.parser()
313 s = StringIO.StringIO()
314 s.write(p.join(props) + '\n')
315 for nodeid in self._klass.list():
316 l = []
317 for name in props:
318 value = self._klass.get(nodeid, name)
319 if value is None:
320 l.append('')
321 elif isinstance(value, type([])):
322 l.append(':'.join(map(str, value)))
323 else:
324 l.append(str(self._klass.get(nodeid, name)))
325 s.write(p.join(l) + '\n')
326 return s.getvalue()
328 def propnames(self):
329 ''' Return the list of the names of the properties of this class.
330 '''
331 idlessprops = self._klass.getprops(protected=0).keys()
332 idlessprops.sort()
333 return ['id'] + idlessprops
335 def filter(self, request=None):
336 ''' Return a list of items from this class, filtered and sorted
337 by the current requested filterspec/filter/sort/group args
338 '''
339 if request is not None:
340 filterspec = request.filterspec
341 sort = request.sort
342 group = request.group
343 if self.classname == 'user':
344 klass = HTMLUser
345 else:
346 klass = HTMLItem
347 l = [klass(self._client, self.classname, x)
348 for x in self._klass.filter(None, filterspec, sort, group)]
349 return l
351 def classhelp(self, properties, label='?', width='400', height='400'):
352 '''pop up a javascript window with class help
354 This generates a link to a popup window which displays the
355 properties indicated by "properties" of the class named by
356 "classname". The "properties" should be a comma-separated list
357 (eg. 'id,name,description').
359 You may optionally override the label displayed, the width and
360 height. The popup window will be resizable and scrollable.
361 '''
362 return '<a href="javascript:help_window(\'%s?:template=help&' \
363 ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
364 '(%s)</b></a>'%(self.classname, properties, width, height, label)
366 def submit(self, label="Submit New Entry"):
367 ''' Generate a submit button (and action hidden element)
368 '''
369 return ' <input type="hidden" name=":action" value="new">\n'\
370 ' <input type="submit" name="submit" value="%s">'%label
372 def history(self):
373 return 'New node - no history'
375 def renderWith(self, name, **kwargs):
376 ''' Render this class with the given template.
377 '''
378 # create a new request and override the specified args
379 req = HTMLRequest(self._client)
380 req.classname = self.classname
381 req.update(kwargs)
383 # new template, using the specified classname and request
384 pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
386 # use our fabricated request
387 return pt.render(self._client, self.classname, req)
389 class HTMLItem:
390 ''' Accesses through an *item*
391 '''
392 def __init__(self, client, classname, nodeid):
393 self._client = client
394 self._db = client.db
395 self._classname = classname
396 self._nodeid = nodeid
397 self._klass = self._db.getclass(classname)
398 self._props = self._klass.getprops()
400 def __repr__(self):
401 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
402 self._nodeid)
404 def __getitem__(self, item):
405 ''' return an HTMLProperty instance
406 '''
407 #print 'HTMLItem.getitem', (self, item)
408 if item == 'id':
409 return self._nodeid
411 # get the property
412 prop = self._props[item]
414 # get the value, handling missing values
415 value = self._klass.get(self._nodeid, item, None)
416 if value is None:
417 if isinstance(self._props[item], hyperdb.Multilink):
418 value = []
420 # look up the correct HTMLProperty class
421 for klass, htmlklass in propclasses:
422 if isinstance(prop, klass):
423 return htmlklass(self._client, self._nodeid, prop, item, value)
425 raise KeyErorr, item
427 def __getattr__(self, attr):
428 ''' convenience access to properties '''
429 try:
430 return self[attr]
431 except KeyError:
432 raise AttributeError, attr
434 def submit(self, label="Submit Changes"):
435 ''' Generate a submit button (and action hidden element)
436 '''
437 return ' <input type="hidden" name=":action" value="edit">\n'\
438 ' <input type="submit" name="submit" value="%s">'%label
440 def journal(self, direction='descending'):
441 ''' Return a list of HTMLJournalEntry instances.
442 '''
443 # XXX do this
444 return []
446 def history(self, direction='descending'):
447 l = ['<table class="history">'
448 '<tr><th colspan="4" class="header">',
449 _('History'),
450 '</th></tr><tr>',
451 _('<th>Date</th>'),
452 _('<th>User</th>'),
453 _('<th>Action</th>'),
454 _('<th>Args</th>'),
455 '</tr>']
456 comments = {}
457 history = self._klass.history(self._nodeid)
458 history.sort()
459 if direction == 'descending':
460 history.reverse()
461 for id, evt_date, user, action, args in history:
462 date_s = str(evt_date).replace("."," ")
463 arg_s = ''
464 if action == 'link' and type(args) == type(()):
465 if len(args) == 3:
466 linkcl, linkid, key = args
467 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
468 linkcl, linkid, key)
469 else:
470 arg_s = str(args)
472 elif action == 'unlink' and type(args) == type(()):
473 if len(args) == 3:
474 linkcl, linkid, key = args
475 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
476 linkcl, linkid, key)
477 else:
478 arg_s = str(args)
480 elif type(args) == type({}):
481 cell = []
482 for k in args.keys():
483 # try to get the relevant property and treat it
484 # specially
485 try:
486 prop = self._props[k]
487 except KeyError:
488 prop = None
489 if prop is not None:
490 if args[k] and (isinstance(prop, hyperdb.Multilink) or
491 isinstance(prop, hyperdb.Link)):
492 # figure what the link class is
493 classname = prop.classname
494 try:
495 linkcl = self._db.getclass(classname)
496 except KeyError:
497 labelprop = None
498 comments[classname] = _('''The linked class
499 %(classname)s no longer exists''')%locals()
500 labelprop = linkcl.labelprop(1)
501 hrefable = os.path.exists(
502 os.path.join(self._db.config.TEMPLATES,
503 classname+'.item'))
505 if isinstance(prop, hyperdb.Multilink) and \
506 len(args[k]) > 0:
507 ml = []
508 for linkid in args[k]:
509 if isinstance(linkid, type(())):
510 sublabel = linkid[0] + ' '
511 linkids = linkid[1]
512 else:
513 sublabel = ''
514 linkids = [linkid]
515 subml = []
516 for linkid in linkids:
517 label = classname + linkid
518 # if we have a label property, try to use it
519 # TODO: test for node existence even when
520 # there's no labelprop!
521 try:
522 if labelprop is not None:
523 label = linkcl.get(linkid, labelprop)
524 except IndexError:
525 comments['no_link'] = _('''<strike>The
526 linked node no longer
527 exists</strike>''')
528 subml.append('<strike>%s</strike>'%label)
529 else:
530 if hrefable:
531 subml.append('<a href="%s%s">%s</a>'%(
532 classname, linkid, label))
533 ml.append(sublabel + ', '.join(subml))
534 cell.append('%s:\n %s'%(k, ', '.join(ml)))
535 elif isinstance(prop, hyperdb.Link) and args[k]:
536 label = classname + args[k]
537 # if we have a label property, try to use it
538 # TODO: test for node existence even when
539 # there's no labelprop!
540 if labelprop is not None:
541 try:
542 label = linkcl.get(args[k], labelprop)
543 except IndexError:
544 comments['no_link'] = _('''<strike>The
545 linked node no longer
546 exists</strike>''')
547 cell.append(' <strike>%s</strike>,\n'%label)
548 # "flag" this is done .... euwww
549 label = None
550 if label is not None:
551 if hrefable:
552 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
553 classname, args[k], label))
554 else:
555 cell.append('%s: %s' % (k,label))
557 elif isinstance(prop, hyperdb.Date) and args[k]:
558 d = date.Date(args[k])
559 cell.append('%s: %s'%(k, str(d)))
561 elif isinstance(prop, hyperdb.Interval) and args[k]:
562 d = date.Interval(args[k])
563 cell.append('%s: %s'%(k, str(d)))
565 elif isinstance(prop, hyperdb.String) and args[k]:
566 cell.append('%s: %s'%(k, cgi.escape(args[k])))
568 elif not args[k]:
569 cell.append('%s: (no value)\n'%k)
571 else:
572 cell.append('%s: %s\n'%(k, str(args[k])))
573 else:
574 # property no longer exists
575 comments['no_exist'] = _('''<em>The indicated property
576 no longer exists</em>''')
577 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
578 arg_s = '<br />'.join(cell)
579 else:
580 # unkown event!!
581 comments['unknown'] = _('''<strong><em>This event is not
582 handled by the history display!</em></strong>''')
583 arg_s = '<strong><em>' + str(args) + '</em></strong>'
584 date_s = date_s.replace(' ', ' ')
585 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
586 date_s, user, action, arg_s))
587 if comments:
588 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
589 for entry in comments.values():
590 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
591 l.append('</table>')
592 return '\n'.join(l)
594 def renderQueryForm(self):
595 ''' Render this item, which is a query, as a search form.
596 '''
597 # create a new request and override the specified args
598 req = HTMLRequest(self._client)
599 req.classname = self._klass.get(self._nodeid, 'klass')
600 req.updateFromURL(self._klass.get(self._nodeid, 'url'))
602 # new template, using the specified classname and request
603 pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
605 # use our fabricated request
606 return pt.render(self._client, req.classname, req)
608 class HTMLUser(HTMLItem):
609 ''' Accesses through the *user* (a special case of item)
610 '''
611 def __init__(self, client, classname, nodeid):
612 HTMLItem.__init__(self, client, 'user', nodeid)
613 self._default_classname = client.classname
615 # used for security checks
616 self._security = client.db.security
617 _marker = []
618 def hasPermission(self, role, classname=_marker):
619 ''' Determine if the user has the Role.
621 The class being tested defaults to the template's class, but may
622 be overidden for this test by suppling an alternate classname.
623 '''
624 if classname is self._marker:
625 classname = self._default_classname
626 return self._security.hasPermission(role, self._nodeid, classname)
628 class HTMLProperty:
629 ''' String, Number, Date, Interval HTMLProperty
631 Hase useful attributes:
633 _name the name of the property
634 _value the value of the property if any
636 A wrapper object which may be stringified for the plain() behaviour.
637 '''
638 def __init__(self, client, nodeid, prop, name, value):
639 self._client = client
640 self._db = client.db
641 self._nodeid = nodeid
642 self._prop = prop
643 self._name = name
644 self._value = value
645 def __repr__(self):
646 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
647 def __str__(self):
648 return self.plain()
649 def __cmp__(self, other):
650 if isinstance(other, HTMLProperty):
651 return cmp(self._value, other._value)
652 return cmp(self._value, other)
654 class StringHTMLProperty(HTMLProperty):
655 def plain(self, escape=0):
656 if self._value is None:
657 return ''
658 if escape:
659 return cgi.escape(str(self._value))
660 return str(self._value)
662 def stext(self, escape=0):
663 s = self.plain(escape=escape)
664 if not StructuredText:
665 return s
666 return StructuredText(s,level=1,header=0)
668 def field(self, size = 30):
669 if self._value is None:
670 value = ''
671 else:
672 value = cgi.escape(str(self._value))
673 value = '"'.join(value.split('"'))
674 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
676 def multiline(self, escape=0, rows=5, cols=40):
677 if self._value is None:
678 value = ''
679 else:
680 value = cgi.escape(str(self._value))
681 value = '"'.join(value.split('"'))
682 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
683 self._name, rows, cols, value)
685 def email(self, escape=1):
686 ''' fudge email '''
687 if self._value is None: value = ''
688 else: value = str(self._value)
689 value = value.replace('@', ' at ')
690 value = value.replace('.', ' ')
691 if escape:
692 value = cgi.escape(value)
693 return value
695 class PasswordHTMLProperty(HTMLProperty):
696 def plain(self):
697 if self._value is None:
698 return ''
699 return _('*encrypted*')
701 def field(self, size = 30):
702 return '<input type="password" name="%s" size="%s">'%(self._name, size)
704 class NumberHTMLProperty(HTMLProperty):
705 def plain(self):
706 return str(self._value)
708 def field(self, size = 30):
709 if self._value is None:
710 value = ''
711 else:
712 value = cgi.escape(str(self._value))
713 value = '"'.join(value.split('"'))
714 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
716 class BooleanHTMLProperty(HTMLProperty):
717 def plain(self):
718 if self.value is None:
719 return ''
720 return self._value and "Yes" or "No"
722 def field(self):
723 checked = self._value and "checked" or ""
724 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
725 checked)
726 if checked:
727 checked = ""
728 else:
729 checked = "checked"
730 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
731 checked)
732 return s
734 class DateHTMLProperty(HTMLProperty):
735 def plain(self):
736 if self._value is None:
737 return ''
738 return str(self._value)
740 def field(self, size = 30):
741 if self._value is None:
742 value = ''
743 else:
744 value = cgi.escape(str(self._value))
745 value = '"'.join(value.split('"'))
746 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
748 def reldate(self, pretty=1):
749 if not self._value:
750 return ''
752 # figure the interval
753 interval = date.Date('.') - self._value
754 if pretty:
755 return interval.pretty()
756 return str(interval)
758 class IntervalHTMLProperty(HTMLProperty):
759 def plain(self):
760 if self._value is None:
761 return ''
762 return str(self._value)
764 def pretty(self):
765 return self._value.pretty()
767 def field(self, size = 30):
768 if self._value is None:
769 value = ''
770 else:
771 value = cgi.escape(str(self._value))
772 value = '"'.join(value.split('"'))
773 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
775 class LinkHTMLProperty(HTMLProperty):
776 ''' Link HTMLProperty
777 Include the above as well as being able to access the class
778 information. Stringifying the object itself results in the value
779 from the item being displayed. Accessing attributes of this object
780 result in the appropriate entry from the class being queried for the
781 property accessed (so item/assignedto/name would look up the user
782 entry identified by the assignedto property on item, and then the
783 name property of that user)
784 '''
785 def __getattr__(self, attr):
786 ''' return a new HTMLItem '''
787 #print 'Link.getattr', (self, attr, self._value)
788 if not self._value:
789 raise AttributeError, "Can't access missing value"
790 if self._prop.classname == 'user':
791 klass = HTMLUser
792 else:
793 klass = HTMLItem
794 i = klass(self._client, self._prop.classname, self._value)
795 return getattr(i, attr)
797 def plain(self, escape=0):
798 if self._value is None:
799 return ''
800 linkcl = self._db.classes[self._prop.classname]
801 k = linkcl.labelprop(1)
802 value = str(linkcl.get(self._value, k))
803 if escape:
804 value = cgi.escape(value)
805 return value
807 def field(self):
808 linkcl = self._db.getclass(self._prop.classname)
809 if linkcl.getprops().has_key('order'):
810 sort_on = 'order'
811 else:
812 sort_on = linkcl.labelprop()
813 options = linkcl.filter(None, {}, [sort_on], [])
814 # TODO: make this a field display, not a menu one!
815 l = ['<select name="%s">'%property]
816 k = linkcl.labelprop(1)
817 if value is None:
818 s = 'selected '
819 else:
820 s = ''
821 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
822 for optionid in options:
823 option = linkcl.get(optionid, k)
824 s = ''
825 if optionid == value:
826 s = 'selected '
827 if showid:
828 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
829 else:
830 lab = option
831 if size is not None and len(lab) > size:
832 lab = lab[:size-3] + '...'
833 lab = cgi.escape(lab)
834 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
835 l.append('</select>')
836 return '\n'.join(l)
838 def menu(self, size=None, height=None, showid=0, additional=[],
839 **conditions):
840 value = self._value
842 # sort function
843 sortfunc = make_sort_function(self._db, self._prop.classname)
845 # force the value to be a single choice
846 if isinstance(value, type('')):
847 value = value[0]
848 linkcl = self._db.getclass(self._prop.classname)
849 l = ['<select name="%s">'%self._name]
850 k = linkcl.labelprop(1)
851 s = ''
852 if value is None:
853 s = 'selected '
854 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
855 if linkcl.getprops().has_key('order'):
856 sort_on = ('+', 'order')
857 else:
858 sort_on = ('+', linkcl.labelprop())
859 options = linkcl.filter(None, conditions, sort_on, (None, None))
860 for optionid in options:
861 option = linkcl.get(optionid, k)
862 s = ''
863 if value in [optionid, option]:
864 s = 'selected '
865 if showid:
866 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
867 else:
868 lab = option
869 if size is not None and len(lab) > size:
870 lab = lab[:size-3] + '...'
871 if additional:
872 m = []
873 for propname in additional:
874 m.append(linkcl.get(optionid, propname))
875 lab = lab + ' (%s)'%', '.join(map(str, m))
876 lab = cgi.escape(lab)
877 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
878 l.append('</select>')
879 return '\n'.join(l)
880 # def checklist(self, ...)
882 class MultilinkHTMLProperty(HTMLProperty):
883 ''' Multilink HTMLProperty
885 Also be iterable, returning a wrapper object like the Link case for
886 each entry in the multilink.
887 '''
888 def __len__(self):
889 ''' length of the multilink '''
890 return len(self._value)
892 def __getattr__(self, attr):
893 ''' no extended attribute accesses make sense here '''
894 raise AttributeError, attr
896 def __getitem__(self, num):
897 ''' iterate and return a new HTMLItem
898 '''
899 #print 'Multi.getitem', (self, num)
900 value = self._value[num]
901 if self._prop.classname == 'user':
902 klass = HTMLUser
903 else:
904 klass = HTMLItem
905 return klass(self._client, self._prop.classname, value)
907 def __contains__(self, value):
908 ''' Support the "in" operator
909 '''
910 return value in self._value
912 def reverse(self):
913 ''' return the list in reverse order
914 '''
915 l = self._value[:]
916 l.reverse()
917 if self._prop.classname == 'user':
918 klass = HTMLUser
919 else:
920 klass = HTMLItem
921 return [klass(self._client, self._prop.classname, value) for value in l]
923 def plain(self, escape=0):
924 linkcl = self._db.classes[self._prop.classname]
925 k = linkcl.labelprop(1)
926 labels = []
927 for v in self._value:
928 labels.append(linkcl.get(v, k))
929 value = ', '.join(labels)
930 if escape:
931 value = cgi.escape(value)
932 return value
934 def field(self, size=30, showid=0):
935 sortfunc = make_sort_function(self._db, self._prop.classname)
936 linkcl = self._db.getclass(self._prop.classname)
937 value = self._value[:]
938 if value:
939 value.sort(sortfunc)
940 # map the id to the label property
941 if not showid:
942 k = linkcl.labelprop(1)
943 value = [linkcl.get(v, k) for v in value]
944 value = cgi.escape(','.join(value))
945 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
947 def menu(self, size=None, height=None, showid=0, additional=[],
948 **conditions):
949 value = self._value
951 # sort function
952 sortfunc = make_sort_function(self._db, self._prop.classname)
954 linkcl = self._db.getclass(self._prop.classname)
955 if linkcl.getprops().has_key('order'):
956 sort_on = ('+', 'order')
957 else:
958 sort_on = ('+', linkcl.labelprop())
959 options = linkcl.filter(None, conditions, sort_on, (None,None))
960 height = height or min(len(options), 7)
961 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
962 k = linkcl.labelprop(1)
963 for optionid in options:
964 option = linkcl.get(optionid, k)
965 s = ''
966 if optionid in value or option in value:
967 s = 'selected '
968 if showid:
969 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
970 else:
971 lab = option
972 if size is not None and len(lab) > size:
973 lab = lab[:size-3] + '...'
974 if additional:
975 m = []
976 for propname in additional:
977 m.append(linkcl.get(optionid, propname))
978 lab = lab + ' (%s)'%', '.join(m)
979 lab = cgi.escape(lab)
980 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
981 lab))
982 l.append('</select>')
983 return '\n'.join(l)
985 # set the propclasses for HTMLItem
986 propclasses = (
987 (hyperdb.String, StringHTMLProperty),
988 (hyperdb.Number, NumberHTMLProperty),
989 (hyperdb.Boolean, BooleanHTMLProperty),
990 (hyperdb.Date, DateHTMLProperty),
991 (hyperdb.Interval, IntervalHTMLProperty),
992 (hyperdb.Password, PasswordHTMLProperty),
993 (hyperdb.Link, LinkHTMLProperty),
994 (hyperdb.Multilink, MultilinkHTMLProperty),
995 )
997 def make_sort_function(db, classname):
998 '''Make a sort function for a given class
999 '''
1000 linkcl = db.getclass(classname)
1001 if linkcl.getprops().has_key('order'):
1002 sort_on = 'order'
1003 else:
1004 sort_on = linkcl.labelprop()
1005 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1006 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1007 return sortfunc
1009 def handleListCGIValue(value):
1010 ''' Value is either a single item or a list of items. Each item has a
1011 .value that we're actually interested in.
1012 '''
1013 if isinstance(value, type([])):
1014 return [value.value for value in value]
1015 else:
1016 value = value.value.strip()
1017 if not value:
1018 return []
1019 return value.split(',')
1021 class ShowDict:
1022 ''' A convenience access to the :columns index parameters
1023 '''
1024 def __init__(self, columns):
1025 self.columns = {}
1026 for col in columns:
1027 self.columns[col] = 1
1028 def __getitem__(self, name):
1029 return self.columns.has_key(name)
1031 class HTMLRequest:
1032 ''' The *request*, holding the CGI form and environment.
1034 "form" the CGI form as a cgi.FieldStorage
1035 "env" the CGI environment variables
1036 "url" the current URL path for this request
1037 "base" the base URL for this instance
1038 "user" a HTMLUser instance for this user
1039 "classname" the current classname (possibly None)
1040 "template" the current template (suffix, also possibly None)
1042 Index args:
1043 "columns" dictionary of the columns to display in an index page
1044 "show" a convenience access to columns - request/show/colname will
1045 be true if the columns should be displayed, false otherwise
1046 "sort" index sort column (direction, column name)
1047 "group" index grouping property (direction, column name)
1048 "filter" properties to filter the index on
1049 "filterspec" values to filter the index on
1050 "search_text" text to perform a full-text search on for an index
1052 '''
1053 def __init__(self, client):
1054 self.client = client
1056 # easier access vars
1057 self.form = client.form
1058 self.env = client.env
1059 self.base = client.base
1060 self.url = client.url
1061 self.user = HTMLUser(client, 'user', client.userid)
1063 # store the current class name and action
1064 self.classname = client.classname
1065 self.template = client.template
1067 self._post_init()
1069 def _post_init(self):
1070 ''' Set attributes based on self.form
1071 '''
1072 # extract the index display information from the form
1073 self.columns = []
1074 if self.form.has_key(':columns'):
1075 self.columns = handleListCGIValue(self.form[':columns'])
1076 self.show = ShowDict(self.columns)
1078 # sorting
1079 self.sort = (None, None)
1080 if self.form.has_key(':sort'):
1081 sort = self.form[':sort'].value
1082 if sort.startswith('-'):
1083 self.sort = ('-', sort[1:])
1084 else:
1085 self.sort = ('+', sort)
1086 if self.form.has_key(':sortdir'):
1087 self.sort = ('-', self.sort[1])
1089 # grouping
1090 self.group = (None, None)
1091 if self.form.has_key(':group'):
1092 group = self.form[':group'].value
1093 if group.startswith('-'):
1094 self.group = ('-', group[1:])
1095 else:
1096 self.group = ('+', group)
1097 if self.form.has_key(':groupdir'):
1098 self.group = ('-', self.group[1])
1100 # filtering
1101 self.filter = []
1102 if self.form.has_key(':filter'):
1103 self.filter = handleListCGIValue(self.form[':filter'])
1104 self.filterspec = {}
1105 if self.classname is not None:
1106 props = self.client.db.getclass(self.classname).getprops()
1107 for name in self.filter:
1108 if self.form.has_key(name):
1109 prop = props[name]
1110 fv = self.form[name]
1111 if (isinstance(prop, hyperdb.Link) or
1112 isinstance(prop, hyperdb.Multilink)):
1113 self.filterspec[name] = handleListCGIValue(fv)
1114 else:
1115 self.filterspec[name] = fv.value
1117 # full-text search argument
1118 self.search_text = None
1119 if self.form.has_key(':search_text'):
1120 self.search_text = self.form[':search_text'].value
1122 # pagination - size and start index
1123 # figure batch args
1124 if self.form.has_key(':pagesize'):
1125 self.pagesize = int(self.form[':pagesize'].value)
1126 else:
1127 self.pagesize = 50
1128 if self.form.has_key(':startwith'):
1129 self.startwith = int(self.form[':startwith'].value)
1130 else:
1131 self.startwith = 0
1133 def updateFromURL(self, url):
1134 ''' Parse the URL for query args, and update my attributes using the
1135 values.
1136 '''
1137 self.form = {}
1138 for name, value in cgi.parse_qsl(url):
1139 if self.form.has_key(name):
1140 if isinstance(self.form[name], type([])):
1141 self.form[name].append(cgi.MiniFieldStorage(name, value))
1142 else:
1143 self.form[name] = [self.form[name],
1144 cgi.MiniFieldStorage(name, value)]
1145 else:
1146 self.form[name] = cgi.MiniFieldStorage(name, value)
1147 self._post_init()
1149 def update(self, kwargs):
1150 ''' Update my attributes using the keyword args
1151 '''
1152 self.__dict__.update(kwargs)
1153 if kwargs.has_key('columns'):
1154 self.show = ShowDict(self.columns)
1156 def description(self):
1157 ''' Return a description of the request - handle for the page title.
1158 '''
1159 s = [self.client.db.config.TRACKER_NAME]
1160 if self.classname:
1161 if self.client.nodeid:
1162 s.append('- %s%s'%(self.classname, self.client.nodeid))
1163 else:
1164 s.append('- index of '+self.classname)
1165 else:
1166 s.append('- home')
1167 return ' '.join(s)
1169 def __str__(self):
1170 d = {}
1171 d.update(self.__dict__)
1172 f = ''
1173 for k in self.form.keys():
1174 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1175 d['form'] = f
1176 e = ''
1177 for k,v in self.env.items():
1178 e += '\n %r=%r'%(k, v)
1179 d['env'] = e
1180 return '''
1181 form: %(form)s
1182 url: %(url)r
1183 base: %(base)r
1184 classname: %(classname)r
1185 template: %(template)r
1186 columns: %(columns)r
1187 sort: %(sort)r
1188 group: %(group)r
1189 filter: %(filter)r
1190 search_text: %(search_text)r
1191 pagesize: %(pagesize)r
1192 startwith: %(startwith)r
1193 env: %(env)s
1194 '''%d
1196 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1197 filterspec=1):
1198 ''' return the current index args as form elements '''
1199 l = []
1200 s = '<input type="hidden" name="%s" value="%s">'
1201 if columns and self.columns:
1202 l.append(s%(':columns', ','.join(self.columns)))
1203 if sort and self.sort[1] is not None:
1204 if self.sort[0] == '-':
1205 val = '-'+self.sort[1]
1206 else:
1207 val = self.sort[1]
1208 l.append(s%(':sort', val))
1209 if group and self.group[1] is not None:
1210 if self.group[0] == '-':
1211 val = '-'+self.group[1]
1212 else:
1213 val = self.group[1]
1214 l.append(s%(':group', val))
1215 if filter and self.filter:
1216 l.append(s%(':filter', ','.join(self.filter)))
1217 if filterspec:
1218 for k,v in self.filterspec.items():
1219 l.append(s%(k, ','.join(v)))
1220 if self.search_text:
1221 l.append(s%(':search_text', self.search_text))
1222 l.append(s%(':pagesize', self.pagesize))
1223 l.append(s%(':startwith', self.startwith))
1224 return '\n'.join(l)
1226 def indexargs_href(self, url, args):
1227 ''' embed the current index args in a URL '''
1228 l = ['%s=%s'%(k,v) for k,v in args.items()]
1229 if self.columns and not args.has_key(':columns'):
1230 l.append(':columns=%s'%(','.join(self.columns)))
1231 if self.sort[1] is not None and not args.has_key(':sort'):
1232 if self.sort[0] == '-':
1233 val = '-'+self.sort[1]
1234 else:
1235 val = self.sort[1]
1236 l.append(':sort=%s'%val)
1237 if self.group[1] is not None and not args.has_key(':group'):
1238 if self.group[0] == '-':
1239 val = '-'+self.group[1]
1240 else:
1241 val = self.group[1]
1242 l.append(':group=%s'%val)
1243 if self.filter and not args.has_key(':columns'):
1244 l.append(':filter=%s'%(','.join(self.filter)))
1245 for k,v in self.filterspec.items():
1246 if not args.has_key(k):
1247 l.append('%s=%s'%(k, ','.join(v)))
1248 if self.search_text and not args.has_key(':search_text'):
1249 l.append(':search_text=%s'%self.search_text)
1250 if not args.has_key(':pagesize'):
1251 l.append(':pagesize=%s'%self.pagesize)
1252 if not args.has_key(':startwith'):
1253 l.append(':startwith=%s'%self.startwith)
1254 return '%s?%s'%(url, '&'.join(l))
1256 def base_javascript(self):
1257 return '''
1258 <script language="javascript">
1259 submitted = false;
1260 function submit_once() {
1261 if (submitted) {
1262 alert("Your request is being processed.\\nPlease be patient.");
1263 return 0;
1264 }
1265 submitted = true;
1266 return 1;
1267 }
1269 function help_window(helpurl, width, height) {
1270 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1271 }
1272 </script>
1273 '''%self.base
1275 def batch(self):
1276 ''' Return a batch object for results from the "current search"
1277 '''
1278 filterspec = self.filterspec
1279 sort = self.sort
1280 group = self.group
1282 # get the list of ids we're batching over
1283 klass = self.client.db.getclass(self.classname)
1284 if self.search_text:
1285 matches = self.client.db.indexer.search(
1286 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1287 else:
1288 matches = None
1289 l = klass.filter(matches, filterspec, sort, group)
1291 # return the batch object
1292 return Batch(self.client, self.classname, l, self.pagesize,
1293 self.startwith)
1296 # extend the standard ZTUtils Batch object to remove dependency on
1297 # Acquisition and add a couple of useful methods
1298 class Batch(ZTUtils.Batch):
1299 def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1300 self.client = client
1301 self.classname = classname
1302 self.last_index = self.last_item = None
1303 self.current_item = None
1304 ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1306 # overwrite so we can late-instantiate the HTMLItem instance
1307 def __getitem__(self, index):
1308 if index < 0:
1309 if index + self.end < self.first: raise IndexError, index
1310 return self._sequence[index + self.end]
1312 if index >= self.length: raise IndexError, index
1314 # move the last_item along - but only if the fetched index changes
1315 # (for some reason, index 0 is fetched twice)
1316 if index != self.last_index:
1317 self.last_item = self.current_item
1318 self.last_index = index
1320 # wrap the return in an HTMLItem
1321 if self.classname == 'user':
1322 klass = HTMLUser
1323 else:
1324 klass = HTMLItem
1325 self.current_item = klass(self.client, self.classname,
1326 self._sequence[index+self.first])
1327 return self.current_item
1329 def propchanged(self, property):
1330 ''' Detect if the property marked as being the group property
1331 changed in the last iteration fetch
1332 '''
1333 if (self.last_item is None or
1334 self.last_item[property] != self.current_item[property]):
1335 return 1
1336 return 0
1338 # override these 'cos we don't have access to acquisition
1339 def previous(self):
1340 if self.start == 1:
1341 return None
1342 return Batch(self.client, self.classname, self._sequence, self._size,
1343 self.first - self._size + self.overlap, 0, self.orphan,
1344 self.overlap)
1346 def next(self):
1347 try:
1348 self._sequence[self.end]
1349 except IndexError:
1350 return None
1351 return Batch(self.client, self.classname, self._sequence, self._size,
1352 self.end - self.overlap, 0, self.orphan, self.overlap)
1354 def length(self):
1355 self.sequence_length = l = len(self._sequence)
1356 return l