Code

. Fixed display of mutlilink properties when using the template
[roundup.git] / roundup / htmltemplate.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17
18 # $Id: htmltemplate.py,v 1.84 2002-03-29 19:41:48 rochecompaan Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno, types
26 import hyperdb, date
27 from i18n import _
29 # This imports the StructureText functionality for the do_stext function
30 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
31 try:
32     from StructuredText.StructuredText import HTML as StructuredText
33 except ImportError:
34     StructuredText = None
36 class MissingTemplateError(ValueError):
37     '''Error raised when a template file is missing
38     '''
39     pass
41 class TemplateFunctions:
42     '''Defines the templating functions that are used in the HTML templates
43        of the roundup web interface.
44     '''
45     def __init__(self):
46         self.form = None
47         self.nodeid = None
48         self.filterspec = None
49         self.globals = {}
50         for key in TemplateFunctions.__dict__.keys():
51             if key[:3] == 'do_':
52                 self.globals[key[3:]] = getattr(self, key)
54         # These are added by the subclass where appropriate
55         self.client = None
56         self.instance = None
57         self.templates = None
58         self.classname = None
59         self.db = None
60         self.cl = None
61         self.properties = None
63     def do_plain(self, property, escape=0):
64         ''' display a String property directly;
66             display a Date property in a specified time zone with an option to
67             omit the time from the date stamp;
69             for a Link or Multilink property, display the key strings of the
70             linked nodes (or the ids if the linked class has no key property)
71         '''
72         if not self.nodeid and self.form is None:
73             return _('[Field: not called from item]')
74         propclass = self.properties[property]
75         if self.nodeid:
76             # make sure the property is a valid one
77             # TODO: this tests, but we should handle the exception
78             dummy = self.cl.getprops()[property]
80             # get the value for this property
81             try:
82                 value = self.cl.get(self.nodeid, property)
83             except KeyError:
84                 # a KeyError here means that the node doesn't have a value
85                 # for the specified property
86                 if isinstance(propclass, hyperdb.Multilink): value = []
87                 else: value = ''
88         else:
89             # TODO: pull the value from the form
90             if isinstance(propclass, hyperdb.Multilink): value = []
91             else: value = ''
92         if isinstance(propclass, hyperdb.String):
93             if value is None: value = ''
94             else: value = str(value)
95         elif isinstance(propclass, hyperdb.Password):
96             if value is None: value = ''
97             else: value = _('*encrypted*')
98         elif isinstance(propclass, hyperdb.Date):
99             # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
100             value = str(value)
101         elif isinstance(propclass, hyperdb.Interval):
102             value = str(value)
103         elif isinstance(propclass, hyperdb.Link):
104             linkcl = self.db.classes[propclass.classname]
105             k = linkcl.labelprop()
106             if value:
107                 value = linkcl.get(value, k)
108             else:
109                 value = _('[unselected]')
110         elif isinstance(propclass, hyperdb.Multilink):
111             linkcl = self.db.classes[propclass.classname]
112             k = linkcl.labelprop()
113             labels = []
114             for v in value:
115                 labels.append(linkcl.get(v, k))
116             value = ', '.join(labels)
117         else:
118             value = _('Plain: bad propclass "%(propclass)s"')%locals()
119         if escape:
120             value = cgi.escape(value)
121         return value
123     def do_stext(self, property, escape=0):
124         '''Render as structured text using the StructuredText module
125            (see above for details)
126         '''
127         s = self.do_plain(property, escape=escape)
128         if not StructuredText:
129             return s
130         return StructuredText(s,level=1,header=0)
132     def determine_value(self, property):
133         '''determine the value of a property using the node, form or
134            filterspec
135         '''
136         propclass = self.properties[property]
137         if self.nodeid:
138             value = self.cl.get(self.nodeid, property, None)
139             if isinstance(propclass, hyperdb.Multilink) and value is None:
140                 return []
141             return value
142         elif self.filterspec is not None:
143             if isinstance(propclass, hyperdb.Multilink):
144                 return self.filterspec.get(property, [])
145             else:
146                 return self.filterspec.get(property, '')
147         # TODO: pull the value from the form
148         if isinstance(propclass, hyperdb.Multilink):
149             return []
150         else:
151             return ''
153     def make_sort_function(self, classname):
154         '''Make a sort function for a given class
155         '''
156         linkcl = self.db.classes[classname]
157         if linkcl.getprops().has_key('order'):
158             sort_on = 'order'
159         else:
160             sort_on = linkcl.labelprop()
161         def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
162             return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
163         return sortfunc
165     def do_field(self, property, size=None, showid=0):
166         ''' display a property like the plain displayer, but in a text field
167             to be edited
169             Note: if you would prefer an option list style display for
170             link or multilink editing, use menu().
171         '''
172         if not self.nodeid and self.form is None and self.filterspec is None:
173             return _('[Field: not called from item]')
175         if size is None:
176             size = 30
178         propclass = self.properties[property]
180         # get the value
181         value = self.determine_value(property)
183         # now display
184         if (isinstance(propclass, hyperdb.String) or
185                 isinstance(propclass, hyperdb.Date) or
186                 isinstance(propclass, hyperdb.Interval)):
187             if value is None:
188                 value = ''
189             else:
190                 value = cgi.escape(str(value))
191                 value = '"'.join(value.split('"'))
192             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
193         elif isinstance(propclass, hyperdb.Password):
194             s = '<input type="password" name="%s" size="%s">'%(property, size)
195         elif isinstance(propclass, hyperdb.Link):
196             sortfunc = self.make_sort_function(propclass.classname)
197             linkcl = self.db.classes[propclass.classname]
198             options = linkcl.list()
199             options.sort(sortfunc)
200             # TODO: make this a field display, not a menu one!
201             l = ['<select name="%s">'%property]
202             k = linkcl.labelprop()
203             if value is None:
204                 s = 'selected '
205             else:
206                 s = ''
207             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
208             for optionid in options:
209                 option = linkcl.get(optionid, k)
210                 s = ''
211                 if optionid == value:
212                     s = 'selected '
213                 if showid:
214                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
215                 else:
216                     lab = option
217                 if size is not None and len(lab) > size:
218                     lab = lab[:size-3] + '...'
219                 lab = cgi.escape(lab)
220                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
221             l.append('</select>')
222             s = '\n'.join(l)
223         elif isinstance(propclass, hyperdb.Multilink):
224             sortfunc = self.make_sort_function(propclass.classname)
225             linkcl = self.db.classes[propclass.classname]
226             list = linkcl.list()
227             list.sort(sortfunc)
228             l = []
229             # map the id to the label property
230             if not showid:
231                 k = linkcl.labelprop()
232                 value = [linkcl.get(v, k) for v in value]
233             value = cgi.escape(','.join(value))
234             s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
235         else:
236             s = _('Plain: bad propclass "%(propclass)s"')%locals()
237         return s
239     def do_multiline(self, property, rows=5, cols=40):
240         ''' display a string property in a multiline text edit field
241         '''
242         if not self.nodeid and self.form is None and self.filterspec is None:
243             return _('[Multiline: not called from item]')
245         propclass = self.properties[property]
247         # make sure this is a link property
248         if not isinstance(propclass, hyperdb.String):
249             return _('[Multiline: not a string]')
251         # get the value
252         value = self.determine_value(property)
253         if value is None:
254             value = ''
256         # display
257         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
258             property, rows, cols, value)
260     def do_menu(self, property, size=None, height=None, showid=0):
261         ''' for a Link property, display a menu of the available choices
262         '''
263         if not self.nodeid and self.form is None and self.filterspec is None:
264             return _('[Field: not called from item]')
266         propclass = self.properties[property]
268         # make sure this is a link property
269         if not (isinstance(propclass, hyperdb.Link) or
270                 isinstance(propclass, hyperdb.Multilink)):
271             return _('[Menu: not a link]')
273         # sort function
274         sortfunc = self.make_sort_function(propclass.classname)
276         # get the value
277         value = self.determine_value(property)
279         # display
280         if isinstance(propclass, hyperdb.Multilink):
281             linkcl = self.db.classes[propclass.classname]
282             options = linkcl.list()
283             options.sort(sortfunc)
284             height = height or min(len(options), 7)
285             l = ['<select multiple name="%s" size="%s">'%(property, height)]
286             k = linkcl.labelprop()
287             for optionid in options:
288                 option = linkcl.get(optionid, k)
289                 s = ''
290                 if optionid in value or option in value:
291                     s = 'selected '
292                 if showid:
293                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
294                 else:
295                     lab = option
296                 if size is not None and len(lab) > size:
297                     lab = lab[:size-3] + '...'
298                 lab = cgi.escape(lab)
299                 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
300                     lab))
301             l.append('</select>')
302             return '\n'.join(l)
303         if isinstance(propclass, hyperdb.Link):
304             # force the value to be a single choice
305             if type(value) is types.ListType:
306                 value = value[0]
307             linkcl = self.db.classes[propclass.classname]
308             l = ['<select name="%s">'%property]
309             k = linkcl.labelprop()
310             s = ''
311             if value is None:
312                 s = 'selected '
313             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
314             options = linkcl.list()
315             options.sort(sortfunc)
316             for optionid in options:
317                 option = linkcl.get(optionid, k)
318                 s = ''
319                 if value in [optionid, option]:
320                     s = 'selected '
321                 if showid:
322                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
323                 else:
324                     lab = option
325                 if size is not None and len(lab) > size:
326                     lab = lab[:size-3] + '...'
327                 lab = cgi.escape(lab)
328                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
329             l.append('</select>')
330             return '\n'.join(l)
331         return _('[Menu: not a link]')
333     #XXX deviates from spec
334     def do_link(self, property=None, is_download=0):
335         '''For a Link or Multilink property, display the names of the linked
336            nodes, hyperlinked to the item views on those nodes.
337            For other properties, link to this node with the property as the
338            text.
340            If is_download is true, append the property value to the generated
341            URL so that the link may be used as a download link and the
342            downloaded file name is correct.
343         '''
344         if not self.nodeid and self.form is None:
345             return _('[Link: not called from item]')
347         # get the value
348         value = self.determine_value(property)
349         if not value:
350             return _('[no %(propname)s]')%{'propname':property.capitalize()}
352         propclass = self.properties[property]
353         if isinstance(propclass, hyperdb.Link):
354             linkname = propclass.classname
355             linkcl = self.db.classes[linkname]
356             k = linkcl.labelprop()
357             linkvalue = cgi.escape(linkcl.get(value, k))
358             if is_download:
359                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
360                     linkvalue, linkvalue)
361             else:
362                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
363         if isinstance(propclass, hyperdb.Multilink):
364             linkname = propclass.classname
365             linkcl = self.db.classes[linkname]
366             k = linkcl.labelprop()
367             l = []
368             for value in value:
369                 linkvalue = cgi.escape(linkcl.get(value, k))
370                 if is_download:
371                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
372                         linkvalue, linkvalue))
373                 else:
374                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
375                         linkvalue))
376             return ', '.join(l)
377         if is_download:
378             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
379                 value, value)
380         else:
381             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
383     def do_count(self, property, **args):
384         ''' for a Multilink property, display a count of the number of links in
385             the list
386         '''
387         if not self.nodeid:
388             return _('[Count: not called from item]')
390         propclass = self.properties[property]
391         if not isinstance(propclass, hyperdb.Multilink):
392             return _('[Count: not a Multilink]')
394         # figure the length then...
395         value = self.cl.get(self.nodeid, property)
396         return str(len(value))
398     # XXX pretty is definitely new ;)
399     def do_reldate(self, property, pretty=0):
400         ''' display a Date property in terms of an interval relative to the
401             current date (e.g. "+ 3w", "- 2d").
403             with the 'pretty' flag, make it pretty
404         '''
405         if not self.nodeid and self.form is None:
406             return _('[Reldate: not called from item]')
408         propclass = self.properties[property]
409         if not isinstance(propclass, hyperdb.Date):
410             return _('[Reldate: not a Date]')
412         if self.nodeid:
413             value = self.cl.get(self.nodeid, property)
414         else:
415             return ''
416         if not value:
417             return ''
419         # figure the interval
420         interval = date.Date('.') - value
421         if pretty:
422             if not self.nodeid:
423                 return _('now')
424             pretty = interval.pretty()
425             if pretty is None:
426                 pretty = value.pretty()
427             return pretty
428         return str(interval)
430     def do_download(self, property, **args):
431         ''' show a Link("file") or Multilink("file") property using links that
432             allow you to download files
433         '''
434         if not self.nodeid:
435             return _('[Download: not called from item]')
436         return self.do_link(property, is_download=1)
439     def do_checklist(self, property, **args):
440         ''' for a Link or Multilink property, display checkboxes for the
441             available choices to permit filtering
442         '''
443         propclass = self.properties[property]
444         if (not isinstance(propclass, hyperdb.Link) and not
445                 isinstance(propclass, hyperdb.Multilink)):
446             return _('[Checklist: not a link]')
448         # get our current checkbox state
449         if self.nodeid:
450             # get the info from the node - make sure it's a list
451             if isinstance(propclass, hyperdb.Link):
452                 value = [self.cl.get(self.nodeid, property)]
453             else:
454                 value = self.cl.get(self.nodeid, property)
455         elif self.filterspec is not None:
456             # get the state from the filter specification (always a list)
457             value = self.filterspec.get(property, [])
458         else:
459             # it's a new node, so there's no state
460             value = []
462         # so we can map to the linked node's "lable" property
463         linkcl = self.db.classes[propclass.classname]
464         l = []
465         k = linkcl.labelprop()
466         for optionid in linkcl.list():
467             option = cgi.escape(linkcl.get(optionid, k))
468             if optionid in value or option in value:
469                 checked = 'checked'
470             else:
471                 checked = ''
472             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
473                 option, checked, property, option))
475         # for Links, allow the "unselected" option too
476         if isinstance(propclass, hyperdb.Link):
477             if value is None or '-1' in value:
478                 checked = 'checked'
479             else:
480                 checked = ''
481             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
482                 'value="-1">')%(checked, property))
483         return '\n'.join(l)
485     def do_note(self, rows=5, cols=80):
486         ''' display a "note" field, which is a text area for entering a note to
487             go along with a change. 
488         '''
489         # TODO: pull the value from the form
490         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
491             '</textarea>'%(rows, cols)
493     # XXX new function
494     def do_list(self, property, reverse=0):
495         ''' list the items specified by property using the standard index for
496             the class
497         '''
498         propcl = self.properties[property]
499         if not isinstance(propcl, hyperdb.Multilink):
500             return _('[List: not a Multilink]')
502         value = self.determine_value(property)
503         if not value:
504             return ''
506         # sort, possibly revers and then re-stringify
507         value = map(int, value)
508         value.sort()
509         if reverse:
510             value.reverse()
511         value = map(str, value)
513         # render the sub-index into a string
514         fp = StringIO.StringIO()
515         try:
516             write_save = self.client.write
517             self.client.write = fp.write
518             index = IndexTemplate(self.client, self.templates, propcl.classname)
519             index.render(nodeids=value, show_display_form=0)
520         finally:
521             self.client.write = write_save
523         return fp.getvalue()
525     # XXX new function
526     def do_history(self, direction='descending'):
527         ''' list the history of the item
529             If "direction" is 'descending' then the most recent event will
530             be displayed first. If it is 'ascending' then the oldest event
531             will be displayed first.
532         '''
533         if self.nodeid is None:
534             return _("[History: node doesn't exist]")
536         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
537             '<tr class="list-header">',
538             _('<th align=left><span class="list-item">Date</span></th>'),
539             _('<th align=left><span class="list-item">User</span></th>'),
540             _('<th align=left><span class="list-item">Action</span></th>'),
541             _('<th align=left><span class="list-item">Args</span></th>'),
542             '</tr>']
544         comments = {}
545         history = self.cl.history(self.nodeid)
546         history.sort()
547         if direction == 'descending':
548             history.reverse()
549         for id, evt_date, user, action, args in history:
550             date_s = str(evt_date).replace("."," ")
551             arg_s = ''
552             if action == 'link' and type(args) == type(()):
553                 if len(args) == 3:
554                     linkcl, linkid, key = args
555                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
556                         linkcl, linkid, key)
557                 else:
558                     arg_s = str(args)
560             elif action == 'unlink' and type(args) == type(()):
561                 if len(args) == 3:
562                     linkcl, linkid, key = args
563                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
564                         linkcl, linkid, key)
565                 else:
566                     arg_s = str(args)
568             elif type(args) == type({}):
569                 cell = []
570                 for k in args.keys():
571                     # try to get the relevant property and treat it
572                     # specially
573                     try:
574                         prop = self.properties[k]
575                     except:
576                         prop = None
577                     if prop is not None:
578                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
579                                 isinstance(prop, hyperdb.Link)):
580                             # figure what the link class is
581                             classname = prop.classname
582                             try:
583                                 linkcl = self.db.classes[classname]
584                             except KeyError:
585                                 labelprop = None
586                                 comments[classname] = _('''The linked class
587                                     %(classname)s no longer exists''')%locals()
588                             labelprop = linkcl.labelprop()
590                         if isinstance(prop, hyperdb.Multilink) and \
591                                 len(args[k]) > 0:
592                             ml = []
593                             for linkid in args[k]:
594                                 label = classname + linkid
595                                 # if we have a label property, try to use it
596                                 # TODO: test for node existence even when
597                                 # there's no labelprop!
598                                 try:
599                                     if labelprop is not None:
600                                         label = linkcl.get(linkid, labelprop)
601                                 except IndexError:
602                                     comments['no_link'] = _('''<strike>The
603                                         linked node no longer
604                                         exists</strike>''')
605                                     ml.append('<strike>%s</strike>'%label)
606                                 else:
607                                     ml.append('<a href="%s%s">%s</a>'%(
608                                         classname, linkid, label))
609                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
610                         elif isinstance(prop, hyperdb.Link) and args[k]:
611                             label = classname + args[k]
612                             # if we have a label property, try to use it
613                             # TODO: test for node existence even when
614                             # there's no labelprop!
615                             if labelprop is not None:
616                                 try:
617                                     label = linkcl.get(args[k], labelprop)
618                                 except IndexError:
619                                     comments['no_link'] = _('''<strike>The
620                                         linked node no longer
621                                         exists</strike>''')
622                                     cell.append(' <strike>%s</strike>,\n'%label)
623                                     # "flag" this is done .... euwww
624                                     label = None
625                             if label is not None:
626                                 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
627                                     classname, args[k], label))
629                         elif isinstance(prop, hyperdb.Date) and args[k]:
630                             d = date.Date(args[k])
631                             cell.append('%s: %s'%(k, str(d)))
633                         elif isinstance(prop, hyperdb.Interval) and args[k]:
634                             d = date.Interval(args[k])
635                             cell.append('%s: %s'%(k, str(d)))
637                         elif not args[k]:
638                             cell.append('%s: (no value)\n'%k)
640                         else:
641                             cell.append('%s: %s\n'%(k, str(args[k])))
642                     else:
643                         # property no longer exists
644                         comments['no_exist'] = _('''<em>The indicated property
645                             no longer exists</em>''')
646                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
647                 arg_s = '<br />'.join(cell)
648             else:
649                 # unkown event!!
650                 comments['unknown'] = _('''<strong><em>This event is not
651                     handled by the history display!</em></strong>''')
652                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
653             date_s = date_s.replace(' ', '&nbsp;')
654             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
655                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
656                 user, action, arg_s))
657         if comments:
658             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
659         for entry in comments.values():
660             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
661         l.append('</table>')
662         return '\n'.join(l)
664     # XXX new function
665     def do_submit(self):
666         ''' add a submit button for the item
667         '''
668         if self.nodeid:
669             return _('<input type="submit" name="submit" value="Submit Changes">')
670         elif self.form is not None:
671             return _('<input type="submit" name="submit" value="Submit New Entry">')
672         else:
673             return _('[Submit: not called from item]')
675     def do_classhelp(self, classname, properties, label='?', width='400',
676             height='400'):
677         '''pop up a javascript window with class help
679            This generates a link to a popup window which displays the 
680            properties indicated by "properties" of the class named by
681            "classname". The "properties" should be a comma-separated list
682            (eg. 'id,name,description').
684            You may optionally override the label displayed, the width and
685            height. The popup window will be resizable and scrollable.
686         '''
687         return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
688             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
689             properties, width, height, label)
691 #   INDEX TEMPLATES
693 class IndexTemplateReplace:
694     '''Regular-expression based parser that turns the template into HTML. 
695     '''
696     def __init__(self, globals, locals, props):
697         self.globals = globals
698         self.locals = locals
699         self.props = props
701     replace=re.compile(
702         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
703         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
704     def go(self, text):
705         return self.replace.sub(self, text)
707     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
708         if m.group('name'):
709             if m.group('name') in self.props:
710                 text = m.group('text')
711                 replace = IndexTemplateReplace(self.globals, {}, self.props)
712                 return replace.go(text)
713             else:
714                 return ''
715         if m.group('display'):
716             command = m.group('command')
717             return eval(command, self.globals, self.locals)
718         return '*** unhandled match: %s'%str(m.groupdict())
720 class IndexTemplate(TemplateFunctions):
721     '''Templating functionality specifically for index pages
722     '''
723     def __init__(self, client, templates, classname):
724         TemplateFunctions.__init__(self)
725         self.client = client
726         self.instance = client.instance
727         self.templates = templates
728         self.classname = classname
730         # derived
731         self.db = self.client.db
732         self.cl = self.db.classes[self.classname]
733         self.properties = self.cl.getprops()
735     col_re=re.compile(r'<property\s+name="([^>]+)">')
736     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
737             show_display_form=1, nodeids=None, show_customization=1):
738         self.filterspec = filterspec
740         w = self.client.write
742         # get the filter template
743         try:
744             filter_template = open(os.path.join(self.templates,
745                 self.classname+'.filter')).read()
746             all_filters = self.col_re.findall(filter_template)
747         except IOError, error:
748             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
749             filter_template = None
750             all_filters = []
752         # XXX deviate from spec here ...
753         # load the index section template and figure the default columns from it
754         try:
755             template = open(os.path.join(self.templates,
756                 self.classname+'.index')).read()
757         except IOError, error:
758             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
759             raise MissingTemplateError, self.classname+'.index'
760         all_columns = self.col_re.findall(template)
761         if not columns:
762             columns = []
763             for name in all_columns:
764                 columns.append(name)
765         else:
766             # re-sort columns to be the same order as all_columns
767             l = []
768             for name in all_columns:
769                 if name in columns:
770                     l.append(name)
771             columns = l
773         # display the filter section
774         if (show_display_form and 
775                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
776             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
777             self.filter_section(filter_template, filter, columns, group,
778                 all_filters, all_columns, show_customization)
779             # make sure that the sorting doesn't get lost either
780             if sort:
781                 w('<input type="hidden" name=":sort" value="%s">'%
782                     ','.join(sort))
783             w('</form>\n')
786         # now display the index section
787         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
788         w('<tr class="list-header">\n')
789         for name in columns:
790             cname = name.capitalize()
791             if show_display_form:
792                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
793                 anchor = "%s?%s"%(self.classname, sb)
794                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
795                     anchor, cname))
796             else:
797                 w('<td><span class="list-header">%s</span></td>\n'%cname)
798         w('</tr>\n')
800         # this stuff is used for group headings - optimise the group names
801         old_group = None
802         group_names = []
803         if group:
804             for name in group:
805                 if name[0] == '-': group_names.append(name[1:])
806                 else: group_names.append(name)
808         # now actually loop through all the nodes we get from the filter and
809         # apply the template
810         if nodeids is None:
811             nodeids = self.cl.filter(filterspec, sort, group)
812         for nodeid in nodeids:
813             # check for a group heading
814             if group_names:
815                 this_group = [self.cl.get(nodeid, name, _('[no value]'))
816                     for name in group_names]
817                 if this_group != old_group:
818                     l = []
819                     for name in group_names:
820                         prop = self.properties[name]
821                         if isinstance(prop, hyperdb.Link):
822                             group_cl = self.db.classes[prop.classname]
823                             key = group_cl.getkey()
824                             value = self.cl.get(nodeid, name)
825                             if value is None:
826                                 l.append(_('[unselected %(classname)s]')%{
827                                     'classname': prop.classname})
828                             else:
829                                 l.append(group_cl.get(self.cl.get(nodeid,
830                                     name), key))
831                         elif isinstance(prop, hyperdb.Multilink):
832                             group_cl = self.db.classes[prop.classname]
833                             key = group_cl.getkey()
834                             for value in self.cl.get(nodeid, name):
835                                 l.append(group_cl.get(value, key))
836                         else:
837                             value = self.cl.get(nodeid, name, _('[no value]'))
838                             if value is None:
839                                 value = _('[empty %(name)s]')%locals()
840                             else:
841                                 value = str(value)
842                             l.append(value)
843                     w('<tr class="section-bar">'
844                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
845                         len(columns), ', '.join(l)))
846                     old_group = this_group
848             # display this node's row
849             replace = IndexTemplateReplace(self.globals, locals(), columns)
850             self.nodeid = nodeid
851             w(replace.go(template))
852             self.nodeid = None
854         w('</table>')
856         # display the filter section
857         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
858                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
859             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
860             self.filter_section(filter_template, filter, columns, group,
861                 all_filters, all_columns, show_customization)
862             # make sure that the sorting doesn't get lost either
863             if sort:
864                 w('<input type="hidden" name=":sort" value="%s">'%
865                     ','.join(sort))
866             w('</form>\n')
869     def filter_section(self, template, filter, columns, group, all_filters,
870             all_columns, show_customization):
872         w = self.client.write
874         # wrap the template in a single table to ensure the whole widget
875         # is displayed at once
876         w('<table><tr><td>')
878         if template and filter:
879             # display the filter section
880             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
881             w('<tr class="location-bar">')
882             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
883             w('</tr>')
884             replace = IndexTemplateReplace(self.globals, locals(), filter)
885             w(replace.go(template))
886             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
887             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
888             w('</table>')
890         # now add in the filter/columns/group/etc config table form
891         w('<input type="hidden" name="show_customization" value="%s">' %
892             show_customization )
893         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
894         names = []
895         seen = {}
896         for name in all_filters + all_columns:
897             if self.properties.has_key(name) and not seen.has_key(name):
898                 names.append(name)
899             seen[name] = 1
900         if show_customization:
901             action = '-'
902         else:
903             action = '+'
904             # hide the values for filters, columns and grouping in the form
905             # if the customization widget is not visible
906             for name in names:
907                 if all_filters and name in filter:
908                     w('<input type="hidden" name=":filter" value="%s">' % name)
909                 if all_columns and name in columns:
910                     w('<input type="hidden" name=":columns" value="%s">' % name)
911                 if all_columns and name in group:
912                     w('<input type="hidden" name=":group" value="%s">' % name)
914         # TODO: The widget style can go into the stylesheet
915         w(_('<th align="left" colspan=%s>'
916           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
917           'customisation...</th></tr>\n')%(len(names)+1, action))
919         if not show_customization:
920             w('</table>\n')
921             return
923         w('<tr class="location-bar"><th>&nbsp;</th>')
924         for name in names:
925             w('<th>%s</th>'%name.capitalize())
926         w('</tr>\n')
928         # Filter
929         if all_filters:
930             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
931             for name in names:
932                 if name not in all_filters:
933                     w('<td>&nbsp;</td>')
934                     continue
935                 if name in filter: checked=' checked'
936                 else: checked=''
937                 w('<td align=middle>\n')
938                 w(' <input type="checkbox" name=":filter" value="%s" '
939                   '%s></td>\n'%(name, checked))
940             w('</tr>\n')
942         # Columns
943         if all_columns:
944             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
945             for name in names:
946                 if name not in all_columns:
947                     w('<td>&nbsp;</td>')
948                     continue
949                 if name in columns: checked=' checked'
950                 else: checked=''
951                 w('<td align=middle>\n')
952                 w(' <input type="checkbox" name=":columns" value="%s"'
953                   '%s></td>\n'%(name, checked))
954             w('</tr>\n')
956             # Grouping
957             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
958             for name in names:
959                 if name not in all_columns:
960                     w('<td>&nbsp;</td>')
961                     continue
962                 if name in group: checked=' checked'
963                 else: checked=''
964                 w('<td align=middle>\n')
965                 w(' <input type="checkbox" name=":group" value="%s"'
966                   '%s></td>\n'%(name, checked))
967             w('</tr>\n')
969         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
970         w('<td colspan="%s">'%len(names))
971         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
972         w('</tr>\n')
973         w('</table>\n')
975         # and the outer table
976         w('</td></tr></table>')
979     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
980         l = []
981         w = l.append
982         for k, v in filterspec.items():
983             k = urllib.quote(k)
984             if type(v) == type([]):
985                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
986             else:
987                 w('%s=%s'%(k, urllib.quote(v)))
988         if columns:
989             w(':columns=%s'%','.join(map(urllib.quote, columns)))
990         if filter:
991             w(':filter=%s'%','.join(map(urllib.quote, filter)))
992         if group:
993             w(':group=%s'%','.join(map(urllib.quote, group)))
994         m = []
995         s_dir = ''
996         for name in sort:
997             dir = name[0]
998             if dir == '-':
999                 name = name[1:]
1000             else:
1001                 dir = ''
1002             if sort_name == name:
1003                 if dir == '-':
1004                     s_dir = ''
1005                 else:
1006                     s_dir = '-'
1007             else:
1008                 m.append(dir+urllib.quote(name))
1009         m.insert(0, s_dir+urllib.quote(sort_name))
1010         # so things don't get completely out of hand, limit the sort to
1011         # two columns
1012         w(':sort=%s'%','.join(m[:2]))
1013         return '&'.join(l)
1016 #   ITEM TEMPLATES
1018 class ItemTemplateReplace:
1019     '''Regular-expression based parser that turns the template into HTML. 
1020     '''
1021     def __init__(self, globals, locals, cl, nodeid):
1022         self.globals = globals
1023         self.locals = locals
1024         self.cl = cl
1025         self.nodeid = nodeid
1027     replace=re.compile(
1028         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1029         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1030     def go(self, text):
1031         return self.replace.sub(self, text)
1033     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1034         if m.group('name'):
1035             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1036                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1037                     self.nodeid)
1038                 return replace.go(m.group('text'))
1039             else:
1040                 return ''
1041         if m.group('display'):
1042             command = m.group('command')
1043             return eval(command, self.globals, self.locals)
1044         return '*** unhandled match: %s'%str(m.groupdict())
1047 class ItemTemplate(TemplateFunctions):
1048     '''Templating functionality specifically for item (node) display
1049     '''
1050     def __init__(self, client, templates, classname):
1051         TemplateFunctions.__init__(self)
1052         self.client = client
1053         self.instance = client.instance
1054         self.templates = templates
1055         self.classname = classname
1057         # derived
1058         self.db = self.client.db
1059         self.cl = self.db.classes[self.classname]
1060         self.properties = self.cl.getprops()
1062     def render(self, nodeid):
1063         self.nodeid = nodeid
1065         if (self.properties.has_key('type') and
1066                 self.properties.has_key('content')):
1067             pass
1068             # XXX we really want to return this as a downloadable...
1069             #  currently I handle this at a higher level by detecting 'file'
1070             #  designators...
1072         w = self.client.write
1073         w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1074             self.classname, nodeid))
1075         s = open(os.path.join(self.templates, self.classname+'.item')).read()
1076         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1077         w(replace.go(s))
1078         w('</form>')
1081 class NewItemTemplate(TemplateFunctions):
1082     '''Templating functionality specifically for NEW item (node) display
1083     '''
1084     def __init__(self, client, templates, classname):
1085         TemplateFunctions.__init__(self)
1086         self.client = client
1087         self.instance = client.instance
1088         self.templates = templates
1089         self.classname = classname
1091         # derived
1092         self.db = self.client.db
1093         self.cl = self.db.classes[self.classname]
1094         self.properties = self.cl.getprops()
1096     def render(self, form):
1097         self.form = form
1098         w = self.client.write
1099         c = self.classname
1100         try:
1101             s = open(os.path.join(self.templates, c+'.newitem')).read()
1102         except IOError:
1103             s = open(os.path.join(self.templates, c+'.item')).read()
1104         w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1105         for key in form.keys():
1106             if key[0] == ':':
1107                 value = form[key].value
1108                 if type(value) != type([]): value = [value]
1109                 for value in value:
1110                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
1111         replace = ItemTemplateReplace(self.globals, locals(), None, None)
1112         w(replace.go(s))
1113         w('</form>')
1116 # $Log: not supported by cvs2svn $
1117 # Revision 1.83  2002/02/27 04:14:31  richard
1118 # Ran it through pychecker, made fixes
1120 # Revision 1.82  2002/02/21 23:11:45  richard
1121 #  . fixed some problems in date calculations (calendar.py doesn't handle over-
1122 #    and under-flow). Also, hour/minute/second intervals may now be more than
1123 #    99 each.
1125 # Revision 1.81  2002/02/21 07:21:38  richard
1126 # docco
1128 # Revision 1.80  2002/02/21 07:19:08  richard
1129 # ... and label, width and height control for extra flavour!
1131 # Revision 1.79  2002/02/21 06:57:38  richard
1132 #  . Added popup help for classes using the classhelp html template function.
1133 #    - add <display call="classhelp('priority', 'id,name,description')">
1134 #      to an item page, and it generates a link to a popup window which displays
1135 #      the id, name and description for the priority class. The description
1136 #      field won't exist in most installations, but it will be added to the
1137 #      default templates.
1139 # Revision 1.78  2002/02/21 06:23:00  richard
1140 # *** empty log message ***
1142 # Revision 1.77  2002/02/20 05:05:29  richard
1143 #  . Added simple editing for classes that don't define a templated interface.
1144 #    - access using the admin "class list" interface
1145 #    - limited to admin-only
1146 #    - requires the csv module from object-craft (url given if it's missing)
1148 # Revision 1.76  2002/02/16 09:10:52  richard
1149 # oops
1151 # Revision 1.75  2002/02/16 08:43:23  richard
1152 #  . #517906 ] Attribute order in "View customisation"
1154 # Revision 1.74  2002/02/16 08:39:42  richard
1155 #  . #516854 ] "My Issues" and redisplay
1157 # Revision 1.73  2002/02/15 07:08:44  richard
1158 #  . Alternate email addresses are now available for users. See the MIGRATION
1159 #    file for info on how to activate the feature.
1161 # Revision 1.72  2002/02/14 23:39:18  richard
1162 # . All forms now have "double-submit" protection when Javascript is enabled
1163 #   on the client-side.
1165 # Revision 1.71  2002/01/23 06:15:24  richard
1166 # real (non-string, duh) sorting of lists by node id
1168 # Revision 1.70  2002/01/23 05:47:57  richard
1169 # more HTML template cleanup and unit tests
1171 # Revision 1.69  2002/01/23 05:10:27  richard
1172 # More HTML template cleanup and unit tests.
1173 #  - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1174 #    templates, but link(is_download=1) will still work for existing templates]
1176 # Revision 1.68  2002/01/22 22:55:28  richard
1177 #  . htmltemplate list() wasn't sorting...
1179 # Revision 1.67  2002/01/22 22:46:22  richard
1180 # more htmltemplate cleanups and unit tests
1182 # Revision 1.66  2002/01/22 06:35:40  richard
1183 # more htmltemplate tests and cleanup
1185 # Revision 1.65  2002/01/22 00:12:06  richard
1186 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1187 # off the implementation of some of the functions so they behave sanely.
1189 # Revision 1.64  2002/01/21 03:25:59  richard
1190 # oops
1192 # Revision 1.63  2002/01/21 02:59:10  richard
1193 # Fixed up the HTML display of history so valid links are actually displayed.
1194 # Oh for some unit tests! :(
1196 # Revision 1.62  2002/01/18 08:36:12  grubert
1197 #  . add nowrap to history table date cell i.e. <td nowrap ...
1199 # Revision 1.61  2002/01/17 23:04:53  richard
1200 #  . much nicer history display (actualy real handling of property types etc)
1202 # Revision 1.60  2002/01/17 08:48:19  grubert
1203 #  . display superseder as html link in history.
1205 # Revision 1.59  2002/01/17 07:58:24  grubert
1206 #  . display links a html link in history.
1208 # Revision 1.58  2002/01/15 00:50:03  richard
1209 # #502949 ] index view for non-issues and redisplay
1211 # Revision 1.57  2002/01/14 23:31:21  richard
1212 # reverted the change that had plain() hyperlinking the link displays -
1213 # that's what link() is for!
1215 # Revision 1.56  2002/01/14 07:04:36  richard
1216 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1217 #    the linked node's page.
1218 #    ... this allows a display very similar to bugzilla's where you can actually
1219 #    find out information about the linked node.
1221 # Revision 1.55  2002/01/14 06:45:03  richard
1222 #  . #502953 ] nosy-like treatment of other multilinks
1223 #    ... had to revert most of the previous change to the multilink field
1224 #    display... not good.
1226 # Revision 1.54  2002/01/14 05:16:51  richard
1227 # The submit buttons need a name attribute or mozilla won't submit without a
1228 # file upload. Yeah, that's bloody obscure. Grr.
1230 # Revision 1.53  2002/01/14 04:03:32  richard
1231 # How about that ... date fields have never worked ...
1233 # Revision 1.52  2002/01/14 02:20:14  richard
1234 #  . changed all config accesses so they access either the instance or the
1235 #    config attriubute on the db. This means that all config is obtained from
1236 #    instance_config instead of the mish-mash of classes. This will make
1237 #    switching to a ConfigParser setup easier too, I hope.
1239 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1240 # 0.5.0 switch, I hope!)
1242 # Revision 1.51  2002/01/10 10:02:15  grubert
1243 # In do_history: replace "." in date by " " so html wraps more sensible.
1244 # Should this be done in date's string converter ?
1246 # Revision 1.50  2002/01/05 02:35:10  richard
1247 # I18N'ification
1249 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1250 # Features added:
1251 #  .  Multilink properties are now displayed as comma separated values in
1252 #     a textbox
1253 #  .  The add user link is now only visible to the admin user
1254 #  .  Modified the mail gateway to reject submissions from unknown
1255 #     addresses if ANONYMOUS_ACCESS is denied
1257 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1258 # Bugs fixed:
1259 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1260 #     lost somewhere
1261 #   . Internet Explorer submits full path for filename - we now strip away
1262 #     the path
1263 # Features added:
1264 #   . Link and multilink properties are now displayed sorted in the cgi
1265 #     interface
1267 # Revision 1.47  2001/11/26 22:55:56  richard
1268 # Feature:
1269 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1270 #    the instance.
1271 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1272 #    signature info in e-mails.
1273 #  . Some more flexibility in the mail gateway and more error handling.
1274 #  . Login now takes you to the page you back to the were denied access to.
1276 # Fixed:
1277 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1279 # Revision 1.46  2001/11/24 00:53:12  jhermann
1280 # "except:" is bad, bad , bad!
1282 # Revision 1.45  2001/11/22 15:46:42  jhermann
1283 # Added module docstrings to all modules.
1285 # Revision 1.44  2001/11/21 23:35:45  jhermann
1286 # Added globbing for win32, and sample marking in a 2nd file to test it
1288 # Revision 1.43  2001/11/21 04:04:43  richard
1289 # *sigh* more missing value handling
1291 # Revision 1.42  2001/11/21 03:40:54  richard
1292 # more new property handling
1294 # Revision 1.41  2001/11/15 10:26:01  richard
1295 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1297 # Revision 1.40  2001/11/03 01:56:51  richard
1298 # More HTML compliance fixes. This will probably fix the Netscape problem
1299 # too.
1301 # Revision 1.39  2001/11/03 01:43:47  richard
1302 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1304 # Revision 1.38  2001/10/31 06:58:51  richard
1305 # Added the wrap="hard" attribute to the textarea of the note field so the
1306 # messages wrap sanely.
1308 # Revision 1.37  2001/10/31 06:24:35  richard
1309 # Added do_stext to htmltemplate, thanks Brad Clements.
1311 # Revision 1.36  2001/10/28 22:51:38  richard
1312 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1314 # Revision 1.35  2001/10/24 00:04:41  richard
1315 # Removed the "infinite authentication loop", thanks Roch'e
1317 # Revision 1.34  2001/10/23 22:56:36  richard
1318 # Bugfix in filter "widget" placement, thanks Roch'e
1320 # Revision 1.33  2001/10/23 01:00:18  richard
1321 # Re-enabled login and registration access after lopping them off via
1322 # disabling access for anonymous users.
1323 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1324 # a couple of bugs while I was there. Probably introduced a couple, but
1325 # things seem to work OK at the moment.
1327 # Revision 1.32  2001/10/22 03:25:01  richard
1328 # Added configuration for:
1329 #  . anonymous user access and registration (deny/allow)
1330 #  . filter "widget" location on index page (top, bottom, both)
1331 # Updated some documentation.
1333 # Revision 1.31  2001/10/21 07:26:35  richard
1334 # feature #473127: Filenames. I modified the file.index and htmltemplate
1335 #  source so that the filename is used in the link and the creation
1336 #  information is displayed.
1338 # Revision 1.30  2001/10/21 04:44:50  richard
1339 # bug #473124: UI inconsistency with Link fields.
1340 #    This also prompted me to fix a fairly long-standing usability issue -
1341 #    that of being able to turn off certain filters.
1343 # Revision 1.29  2001/10/21 00:17:56  richard
1344 # CGI interface view customisation section may now be hidden (patch from
1345 #  Roch'e Compaan.)
1347 # Revision 1.28  2001/10/21 00:00:16  richard
1348 # Fixed Checklist function - wasn't always working on a list.
1350 # Revision 1.27  2001/10/20 12:13:44  richard
1351 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1353 # Revision 1.26  2001/10/14 10:55:00  richard
1354 # Handle empty strings in HTML template Link function
1356 # Revision 1.25  2001/10/09 07:25:59  richard
1357 # Added the Password property type. See "pydoc roundup.password" for
1358 # implementation details. Have updated some of the documentation too.
1360 # Revision 1.24  2001/09/27 06:45:58  richard
1361 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1362 # on the plain() template function to escape the text for HTML.
1364 # Revision 1.23  2001/09/10 09:47:18  richard
1365 # Fixed bug in the generation of links to Link/Multilink in indexes.
1366 #   (thanks Hubert Hoegl)
1367 # Added AssignedTo to the "classic" schema's item page.
1369 # Revision 1.22  2001/08/30 06:01:17  richard
1370 # Fixed missing import in mailgw :(
1372 # Revision 1.21  2001/08/16 07:34:59  richard
1373 # better CGI text searching - but hidden filter fields are disappearing...
1375 # Revision 1.20  2001/08/15 23:43:18  richard
1376 # Fixed some isFooTypes that I missed.
1377 # Refactored some code in the CGI code.
1379 # Revision 1.19  2001/08/12 06:32:36  richard
1380 # using isinstance(blah, Foo) now instead of isFooType
1382 # Revision 1.18  2001/08/07 00:24:42  richard
1383 # stupid typo
1385 # Revision 1.17  2001/08/07 00:15:51  richard
1386 # Added the copyright/license notice to (nearly) all files at request of
1387 # Bizar Software.
1389 # Revision 1.16  2001/08/01 03:52:23  richard
1390 # Checklist was using wrong name.
1392 # Revision 1.15  2001/07/30 08:12:17  richard
1393 # Added time logging and file uploading to the templates.
1395 # Revision 1.14  2001/07/30 06:17:45  richard
1396 # Features:
1397 #  . Added ability for cgi newblah forms to indicate that the new node
1398 #    should be linked somewhere.
1399 # Fixed:
1400 #  . Fixed the agument handling for the roundup-admin find command.
1401 #  . Fixed handling of summary when no note supplied for newblah. Again.
1402 #  . Fixed detection of no form in htmltemplate Field display.
1404 # Revision 1.13  2001/07/30 02:37:53  richard
1405 # Temporary measure until we have decent schema migration.
1407 # Revision 1.12  2001/07/30 01:24:33  richard
1408 # Handles new node display now.
1410 # Revision 1.11  2001/07/29 09:31:35  richard
1411 # oops
1413 # Revision 1.10  2001/07/29 09:28:23  richard
1414 # Fixed sorting by clicking on column headings.
1416 # Revision 1.9  2001/07/29 08:27:40  richard
1417 # Fixed handling of passed-in values in form elements (ie. during a
1418 # drill-down)
1420 # Revision 1.8  2001/07/29 07:01:39  richard
1421 # Added vim command to all source so that we don't get no steenkin' tabs :)
1423 # Revision 1.7  2001/07/29 05:36:14  richard
1424 # Cleanup of the link label generation.
1426 # Revision 1.6  2001/07/29 04:06:42  richard
1427 # Fixed problem in link display when Link value is None.
1429 # Revision 1.5  2001/07/28 08:17:09  richard
1430 # fixed use of stylesheet
1432 # Revision 1.4  2001/07/28 07:59:53  richard
1433 # Replaced errno integers with their module values.
1434 # De-tabbed templatebuilder.py
1436 # Revision 1.3  2001/07/25 03:39:47  richard
1437 # Hrm - displaying links to classes that don't specify a key property. I've
1438 # got it defaulting to 'name', then 'title' and then a "random" property (first
1439 # one returned by getprops().keys().
1440 # Needs to be moved onto the Class I think...
1442 # Revision 1.2  2001/07/22 12:09:32  richard
1443 # Final commit of Grande Splite
1445 # Revision 1.1  2001/07/22 11:58:35  richard
1446 # More Grande Splite
1449 # vim: set filetype=python ts=4 sw=4 et si