Code

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