Code

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