Code

More informative error message
[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.92 2002-06-11 04:57:04 richard Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno, types, urllib
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, lookup=1):
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             when the lookup argument is true, otherwise just return the
72             linked ids
73         '''
74         if not self.nodeid and self.form is None:
75             return _('[Field: not called from item]')
76         propclass = self.properties[property]
77         if self.nodeid:
78             # make sure the property is a valid one
79             # TODO: this tests, but we should handle the exception
80             dummy = self.cl.getprops()[property]
82             # get the value for this property
83             try:
84                 value = self.cl.get(self.nodeid, property)
85             except KeyError:
86                 # a KeyError here means that the node doesn't have a value
87                 # for the specified property
88                 if isinstance(propclass, hyperdb.Multilink): value = []
89                 else: value = ''
90         else:
91             # TODO: pull the value from the form
92             if isinstance(propclass, hyperdb.Multilink): value = []
93             else: value = ''
94         if isinstance(propclass, hyperdb.String):
95             if value is None: value = ''
96             else: value = str(value)
97         elif isinstance(propclass, hyperdb.Password):
98             if value is None: value = ''
99             else: value = _('*encrypted*')
100         elif isinstance(propclass, hyperdb.Date):
101             # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
102             value = str(value)
103         elif isinstance(propclass, hyperdb.Interval):
104             value = str(value)
105         elif isinstance(propclass, hyperdb.Link):
106             if value:
107                 if lookup:
108                     linkcl = self.db.classes[propclass.classname]
109                     k = linkcl.labelprop()
110                     value = linkcl.get(value, k)
111             else:
112                 value = _('[unselected]')
113         elif isinstance(propclass, hyperdb.Multilink):
114             if lookup:
115                 linkcl = self.db.classes[propclass.classname]
116                 k = linkcl.labelprop()
117                 labels = []
118                 for v in value:
119                     labels.append(linkcl.get(v, k))
120                 value = ', '.join(labels)
121             else:
122                 value = ', '.join(value)
123         else:
124             value = _('Plain: bad propclass "%(propclass)s"')%locals()
125         if escape:
126             value = cgi.escape(value)
127         return value
129     def do_stext(self, property, escape=0):
130         '''Render as structured text using the StructuredText module
131            (see above for details)
132         '''
133         s = self.do_plain(property, escape=escape)
134         if not StructuredText:
135             return s
136         return StructuredText(s,level=1,header=0)
138     def determine_value(self, property):
139         '''determine the value of a property using the node, form or
140            filterspec
141         '''
142         propclass = self.properties[property]
143         if self.nodeid:
144             value = self.cl.get(self.nodeid, property, None)
145             if isinstance(propclass, hyperdb.Multilink) and value is None:
146                 return []
147             return value
148         elif self.filterspec is not None:
149             if isinstance(propclass, hyperdb.Multilink):
150                 return self.filterspec.get(property, [])
151             else:
152                 return self.filterspec.get(property, '')
153         # TODO: pull the value from the form
154         if isinstance(propclass, hyperdb.Multilink):
155             return []
156         else:
157             return ''
159     def make_sort_function(self, classname):
160         '''Make a sort function for a given class
161         '''
162         linkcl = self.db.classes[classname]
163         if linkcl.getprops().has_key('order'):
164             sort_on = 'order'
165         else:
166             sort_on = linkcl.labelprop()
167         def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
168             return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
169         return sortfunc
171     def do_field(self, property, size=None, showid=0):
172         ''' display a property like the plain displayer, but in a text field
173             to be edited
175             Note: if you would prefer an option list style display for
176             link or multilink editing, use menu().
177         '''
178         if not self.nodeid and self.form is None and self.filterspec is None:
179             return _('[Field: not called from item]')
181         if size is None:
182             size = 30
184         propclass = self.properties[property]
186         # get the value
187         value = self.determine_value(property)
189         # now display
190         if (isinstance(propclass, hyperdb.String) or
191                 isinstance(propclass, hyperdb.Date) or
192                 isinstance(propclass, hyperdb.Interval)):
193             if value is None:
194                 value = ''
195             else:
196                 value = cgi.escape(str(value))
197                 value = '"'.join(value.split('"'))
198             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
199         elif isinstance(propclass, hyperdb.Password):
200             s = '<input type="password" name="%s" size="%s">'%(property, size)
201         elif isinstance(propclass, hyperdb.Link):
202             sortfunc = self.make_sort_function(propclass.classname)
203             linkcl = self.db.classes[propclass.classname]
204             options = linkcl.list()
205             options.sort(sortfunc)
206             # TODO: make this a field display, not a menu one!
207             l = ['<select name="%s">'%property]
208             k = linkcl.labelprop()
209             if value is None:
210                 s = 'selected '
211             else:
212                 s = ''
213             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
214             for optionid in options:
215                 option = linkcl.get(optionid, k)
216                 s = ''
217                 if optionid == value:
218                     s = 'selected '
219                 if showid:
220                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
221                 else:
222                     lab = option
223                 if size is not None and len(lab) > size:
224                     lab = lab[:size-3] + '...'
225                 lab = cgi.escape(lab)
226                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
227             l.append('</select>')
228             s = '\n'.join(l)
229         elif isinstance(propclass, hyperdb.Multilink):
230             sortfunc = self.make_sort_function(propclass.classname)
231             linkcl = self.db.classes[propclass.classname]
232             if value:
233                 value.sort(sortfunc)
234             # map the id to the label property
235             if not showid:
236                 k = linkcl.labelprop()
237                 value = [linkcl.get(v, k) for v in value]
238             value = cgi.escape(','.join(value))
239             s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
240         else:
241             s = _('Plain: bad propclass "%(propclass)s"')%locals()
242         return s
244     def do_multiline(self, property, rows=5, cols=40):
245         ''' display a string property in a multiline text edit field
246         '''
247         if not self.nodeid and self.form is None and self.filterspec is None:
248             return _('[Multiline: not called from item]')
250         propclass = self.properties[property]
252         # make sure this is a link property
253         if not isinstance(propclass, hyperdb.String):
254             return _('[Multiline: not a string]')
256         # get the value
257         value = self.determine_value(property)
258         if value is None:
259             value = ''
261         # display
262         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
263             property, rows, cols, value)
265     def do_menu(self, property, size=None, height=None, showid=0,
266             additional=[]):
267         ''' For a Link/Multilink property, display a menu of the available
268             choices
270             If the additional properties are specified, they will be
271             included in the text of each option in (brackets, with, commas).
272         '''
273         if not self.nodeid and self.form is None and self.filterspec is None:
274             return _('[Field: not called from item]')
276         propclass = self.properties[property]
278         # make sure this is a link property
279         if not (isinstance(propclass, hyperdb.Link) or
280                 isinstance(propclass, hyperdb.Multilink)):
281             return _('[Menu: not a link]')
283         # sort function
284         sortfunc = self.make_sort_function(propclass.classname)
286         # get the value
287         value = self.determine_value(property)
289         # display
290         if isinstance(propclass, hyperdb.Multilink):
291             linkcl = self.db.classes[propclass.classname]
292             options = linkcl.list()
293             options.sort(sortfunc)
294             height = height or min(len(options), 7)
295             l = ['<select multiple name="%s" size="%s">'%(property, height)]
296             k = linkcl.labelprop()
297             for optionid in options:
298                 option = linkcl.get(optionid, k)
299                 s = ''
300                 if optionid in value or option in value:
301                     s = 'selected '
302                 if showid:
303                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
304                 else:
305                     lab = option
306                 if size is not None and len(lab) > size:
307                     lab = lab[:size-3] + '...'
308                 if additional:
309                     m = []
310                     for propname in additional:
311                         m.append(linkcl.get(optionid, propname))
312                     lab = lab + ' (%s)'%', '.join(m)
313                 lab = cgi.escape(lab)
314                 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
315                     lab))
316             l.append('</select>')
317             return '\n'.join(l)
318         if isinstance(propclass, hyperdb.Link):
319             # force the value to be a single choice
320             if type(value) is types.ListType:
321                 value = value[0]
322             linkcl = self.db.classes[propclass.classname]
323             l = ['<select name="%s">'%property]
324             k = linkcl.labelprop()
325             s = ''
326             if value is None:
327                 s = 'selected '
328             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
329             options = linkcl.list()
330             options.sort(sortfunc)
331             for optionid in options:
332                 option = linkcl.get(optionid, k)
333                 s = ''
334                 if value in [optionid, option]:
335                     s = 'selected '
336                 if showid:
337                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
338                 else:
339                     lab = option
340                 if size is not None and len(lab) > size:
341                     lab = lab[:size-3] + '...'
342                 if additional:
343                     m = []
344                     for propname in additional:
345                         m.append(linkcl.get(optionid, propname))
346                     lab = lab + ' (%s)'%', '.join(m)
347                 lab = cgi.escape(lab)
348                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
349             l.append('</select>')
350             return '\n'.join(l)
351         return _('[Menu: not a link]')
353     #XXX deviates from spec
354     def do_link(self, property=None, is_download=0, showid=0):
355         '''For a Link or Multilink property, display the names of the linked
356            nodes, hyperlinked to the item views on those nodes.
357            For other properties, link to this node with the property as the
358            text.
360            If is_download is true, append the property value to the generated
361            URL so that the link may be used as a download link and the
362            downloaded file name is correct.
363         '''
364         if not self.nodeid and self.form is None:
365             return _('[Link: not called from item]')
367         # get the value
368         value = self.determine_value(property)
369         if not value:
370             return _('[no %(propname)s]')%{'propname':property.capitalize()}
372         propclass = self.properties[property]
373         if isinstance(propclass, hyperdb.Link):
374             linkname = propclass.classname
375             linkcl = self.db.classes[linkname]
376             k = linkcl.labelprop()
377             linkvalue = cgi.escape(str(linkcl.get(value, k)))
378             if showid:
379                 label = value
380                 title = ' title="%s"'%linkvalue
381                 # note ... this should be urllib.quote(linkcl.get(value, k))
382             else:
383                 label = linkvalue
384                 title = ''
385             if is_download:
386                 return '<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
387                     linkvalue, title, label)
388             else:
389                 return '<a href="%s%s"%s>%s</a>'%(linkname, value, title, label)
390         if isinstance(propclass, hyperdb.Multilink):
391             linkname = propclass.classname
392             linkcl = self.db.classes[linkname]
393             k = linkcl.labelprop()
394             l = []
395             for value in value:
396                 linkvalue = cgi.escape(str(linkcl.get(value, k)))
397                 if showid:
398                     label = value
399                     title = ' title="%s"'%linkvalue
400                     # note ... this should be urllib.quote(linkcl.get(value, k))
401                 else:
402                     label = linkvalue
403                     title = ''
404                 if is_download:
405                     l.append('<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
406                         linkvalue, title, label))
407                 else:
408                     l.append('<a href="%s%s"%s>%s</a>'%(linkname, value,
409                         title, label))
410             return ', '.join(l)
411         if is_download:
412             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
413                 value, value)
414         else:
415             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
417     def do_count(self, property, **args):
418         ''' for a Multilink property, display a count of the number of links in
419             the list
420         '''
421         if not self.nodeid:
422             return _('[Count: not called from item]')
424         propclass = self.properties[property]
425         if not isinstance(propclass, hyperdb.Multilink):
426             return _('[Count: not a Multilink]')
428         # figure the length then...
429         value = self.cl.get(self.nodeid, property)
430         return str(len(value))
432     # XXX pretty is definitely new ;)
433     def do_reldate(self, property, pretty=0):
434         ''' display a Date property in terms of an interval relative to the
435             current date (e.g. "+ 3w", "- 2d").
437             with the 'pretty' flag, make it pretty
438         '''
439         if not self.nodeid and self.form is None:
440             return _('[Reldate: not called from item]')
442         propclass = self.properties[property]
443         if not isinstance(propclass, hyperdb.Date):
444             return _('[Reldate: not a Date]')
446         if self.nodeid:
447             value = self.cl.get(self.nodeid, property)
448         else:
449             return ''
450         if not value:
451             return ''
453         # figure the interval
454         interval = date.Date('.') - value
455         if pretty:
456             if not self.nodeid:
457                 return _('now')
458             return interval.pretty()
459         return str(interval)
461     def do_download(self, property, **args):
462         ''' show a Link("file") or Multilink("file") property using links that
463             allow you to download files
464         '''
465         if not self.nodeid:
466             return _('[Download: not called from item]')
467         return self.do_link(property, is_download=1)
470     def do_checklist(self, property, **args):
471         ''' for a Link or Multilink property, display checkboxes for the
472             available choices to permit filtering
473         '''
474         propclass = self.properties[property]
475         if (not isinstance(propclass, hyperdb.Link) and not
476                 isinstance(propclass, hyperdb.Multilink)):
477             return _('[Checklist: not a link]')
479         # get our current checkbox state
480         if self.nodeid:
481             # get the info from the node - make sure it's a list
482             if isinstance(propclass, hyperdb.Link):
483                 value = [self.cl.get(self.nodeid, property)]
484             else:
485                 value = self.cl.get(self.nodeid, property)
486         elif self.filterspec is not None:
487             # get the state from the filter specification (always a list)
488             value = self.filterspec.get(property, [])
489         else:
490             # it's a new node, so there's no state
491             value = []
493         # so we can map to the linked node's "lable" property
494         linkcl = self.db.classes[propclass.classname]
495         l = []
496         k = linkcl.labelprop()
497         for optionid in linkcl.list():
498             option = cgi.escape(str(linkcl.get(optionid, k)))
499             if optionid in value or option in value:
500                 checked = 'checked'
501             else:
502                 checked = ''
503             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
504                 option, checked, property, option))
506         # for Links, allow the "unselected" option too
507         if isinstance(propclass, hyperdb.Link):
508             if value is None or '-1' in value:
509                 checked = 'checked'
510             else:
511                 checked = ''
512             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
513                 'value="-1">')%(checked, property))
514         return '\n'.join(l)
516     def do_note(self, rows=5, cols=80):
517         ''' display a "note" field, which is a text area for entering a note to
518             go along with a change. 
519         '''
520         # TODO: pull the value from the form
521         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
522             '</textarea>'%(rows, cols)
524     # XXX new function
525     def do_list(self, property, reverse=0):
526         ''' list the items specified by property using the standard index for
527             the class
528         '''
529         propcl = self.properties[property]
530         if not isinstance(propcl, hyperdb.Multilink):
531             return _('[List: not a Multilink]')
533         value = self.determine_value(property)
534         if not value:
535             return ''
537         # sort, possibly revers and then re-stringify
538         value = map(int, value)
539         value.sort()
540         if reverse:
541             value.reverse()
542         value = map(str, value)
544         # render the sub-index into a string
545         fp = StringIO.StringIO()
546         try:
547             write_save = self.client.write
548             self.client.write = fp.write
549             index = IndexTemplate(self.client, self.templates, propcl.classname)
550             index.render(nodeids=value, show_display_form=0)
551         finally:
552             self.client.write = write_save
554         return fp.getvalue()
556     # XXX new function
557     def do_history(self, direction='descending'):
558         ''' list the history of the item
560             If "direction" is 'descending' then the most recent event will
561             be displayed first. If it is 'ascending' then the oldest event
562             will be displayed first.
563         '''
564         if self.nodeid is None:
565             return _("[History: node doesn't exist]")
567         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
568             '<tr class="list-header">',
569             _('<th align=left><span class="list-item">Date</span></th>'),
570             _('<th align=left><span class="list-item">User</span></th>'),
571             _('<th align=left><span class="list-item">Action</span></th>'),
572             _('<th align=left><span class="list-item">Args</span></th>'),
573             '</tr>']
575         comments = {}
576         history = self.cl.history(self.nodeid)
577         history.sort()
578         if direction == 'descending':
579             history.reverse()
580         for id, evt_date, user, action, args in history:
581             date_s = str(evt_date).replace("."," ")
582             arg_s = ''
583             if action == 'link' and type(args) == type(()):
584                 if len(args) == 3:
585                     linkcl, linkid, key = args
586                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
587                         linkcl, linkid, key)
588                 else:
589                     arg_s = str(args)
591             elif action == 'unlink' and type(args) == type(()):
592                 if len(args) == 3:
593                     linkcl, linkid, key = args
594                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
595                         linkcl, linkid, key)
596                 else:
597                     arg_s = str(args)
599             elif type(args) == type({}):
600                 cell = []
601                 for k in args.keys():
602                     # try to get the relevant property and treat it
603                     # specially
604                     try:
605                         prop = self.properties[k]
606                     except:
607                         prop = None
608                     if prop is not None:
609                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
610                                 isinstance(prop, hyperdb.Link)):
611                             # figure what the link class is
612                             classname = prop.classname
613                             try:
614                                 linkcl = self.db.classes[classname]
615                             except KeyError:
616                                 labelprop = None
617                                 comments[classname] = _('''The linked class
618                                     %(classname)s no longer exists''')%locals()
619                             labelprop = linkcl.labelprop()
621                         if isinstance(prop, hyperdb.Multilink) and \
622                                 len(args[k]) > 0:
623                             ml = []
624                             for linkid in args[k]:
625                                 label = classname + linkid
626                                 # if we have a label property, try to use it
627                                 # TODO: test for node existence even when
628                                 # there's no labelprop!
629                                 try:
630                                     if labelprop is not None:
631                                         label = linkcl.get(linkid, labelprop)
632                                 except IndexError:
633                                     comments['no_link'] = _('''<strike>The
634                                         linked node no longer
635                                         exists</strike>''')
636                                     ml.append('<strike>%s</strike>'%label)
637                                 else:
638                                     ml.append('<a href="%s%s">%s</a>'%(
639                                         classname, linkid, label))
640                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
641                         elif isinstance(prop, hyperdb.Link) and args[k]:
642                             label = classname + args[k]
643                             # if we have a label property, try to use it
644                             # TODO: test for node existence even when
645                             # there's no labelprop!
646                             if labelprop is not None:
647                                 try:
648                                     label = linkcl.get(args[k], labelprop)
649                                 except IndexError:
650                                     comments['no_link'] = _('''<strike>The
651                                         linked node no longer
652                                         exists</strike>''')
653                                     cell.append(' <strike>%s</strike>,\n'%label)
654                                     # "flag" this is done .... euwww
655                                     label = None
656                             if label is not None:
657                                 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
658                                     classname, args[k], label))
660                         elif isinstance(prop, hyperdb.Date) and args[k]:
661                             d = date.Date(args[k])
662                             cell.append('%s: %s'%(k, str(d)))
664                         elif isinstance(prop, hyperdb.Interval) and args[k]:
665                             d = date.Interval(args[k])
666                             cell.append('%s: %s'%(k, str(d)))
668                         elif not args[k]:
669                             cell.append('%s: (no value)\n'%k)
671                         else:
672                             cell.append('%s: %s\n'%(k, str(args[k])))
673                     else:
674                         # property no longer exists
675                         comments['no_exist'] = _('''<em>The indicated property
676                             no longer exists</em>''')
677                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
678                 arg_s = '<br />'.join(cell)
679             else:
680                 # unkown event!!
681                 comments['unknown'] = _('''<strong><em>This event is not
682                     handled by the history display!</em></strong>''')
683                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
684             date_s = date_s.replace(' ', '&nbsp;')
685             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
686                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
687                 user, action, arg_s))
688         if comments:
689             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
690         for entry in comments.values():
691             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
692         l.append('</table>')
693         return '\n'.join(l)
695     # XXX new function
696     def do_submit(self):
697         ''' add a submit button for the item
698         '''
699         if self.nodeid:
700             return _('<input type="submit" name="submit" value="Submit Changes">')
701         elif self.form is not None:
702             return _('<input type="submit" name="submit" value="Submit New Entry">')
703         else:
704             return _('[Submit: not called from item]')
706     def do_classhelp(self, classname, properties, label='?', width='400',
707             height='400'):
708         '''pop up a javascript window with class help
710            This generates a link to a popup window which displays the 
711            properties indicated by "properties" of the class named by
712            "classname". The "properties" should be a comma-separated list
713            (eg. 'id,name,description').
715            You may optionally override the label displayed, the width and
716            height. The popup window will be resizable and scrollable.
717         '''
718         return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
719             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
720             properties, width, height, label)
722 #   INDEX TEMPLATES
724 class IndexTemplateReplace:
725     '''Regular-expression based parser that turns the template into HTML. 
726     '''
727     def __init__(self, globals, locals, props):
728         self.globals = globals
729         self.locals = locals
730         self.props = props
732     replace=re.compile(
733         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
734         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
735     def go(self, text):
736         return self.replace.sub(self, text)
738     def __call__(self, m, search_text=None, filter=None, columns=None,
739             sort=None, group=None):
740         if m.group('name'):
741             if m.group('name') in self.props:
742                 text = m.group('text')
743                 replace = IndexTemplateReplace(self.globals, {}, self.props)
744                 return replace.go(text)
745             else:
746                 return ''
747         if m.group('display'):
748             command = m.group('command')
749             return eval(command, self.globals, self.locals)
750         return '*** unhandled match: %s'%str(m.groupdict())
752 class IndexTemplate(TemplateFunctions):
753     '''Templating functionality specifically for index pages
754     '''
755     def __init__(self, client, templates, classname):
756         TemplateFunctions.__init__(self)
757         self.client = client
758         self.instance = client.instance
759         self.templates = templates
760         self.classname = classname
762         # derived
763         self.db = self.client.db
764         self.cl = self.db.classes[self.classname]
765         self.properties = self.cl.getprops()
767     col_re=re.compile(r'<property\s+name="([^>]+)">')
768     def render(self, filterspec={}, search_text='', filter=[], columns=[], 
769             sort=[], group=[], show_display_form=1, nodeids=None,
770             show_customization=1, show_nodes=1):
771         self.filterspec = filterspec
773         w = self.client.write
775         # get the filter template
776         try:
777             filter_template = open(os.path.join(self.templates,
778                 self.classname+'.filter')).read()
779             all_filters = self.col_re.findall(filter_template)
780         except IOError, error:
781             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
782             filter_template = None
783             all_filters = []
785         # XXX deviate from spec here ...
786         # load the index section template and figure the default columns from it
787         try:
788             template = open(os.path.join(self.templates,
789                 self.classname+'.index')).read()
790         except IOError, error:
791             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
792             raise MissingTemplateError, self.classname+'.index'
793         all_columns = self.col_re.findall(template)
794         if not columns:
795             columns = []
796             for name in all_columns:
797                 columns.append(name)
798         else:
799             # re-sort columns to be the same order as all_columns
800             l = []
801             for name in all_columns:
802                 if name in columns:
803                     l.append(name)
804             columns = l
806         # display the filter section
807         if (show_display_form and 
808                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
809             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
810             self.filter_section(filter_template, search_text, filter, 
811                 columns, group, all_filters, all_columns, show_customization)
812             # make sure that the sorting doesn't get lost either
813             if sort:
814                 w('<input type="hidden" name=":sort" value="%s">'%
815                     ','.join(sort))
816             w('</form>\n')
819         # now display the index section
820         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
821         w('<tr class="list-header">\n')
822         for name in columns:
823             cname = name.capitalize()
824             if show_display_form:
825                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
826                 anchor = "%s?%s"%(self.classname, sb)
827                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
828                     anchor, cname))
829             else:
830                 w('<td><span class="list-header">%s</span></td>\n'%cname)
831         w('</tr>\n')
833         # this stuff is used for group headings - optimise the group names
834         old_group = None
835         group_names = []
836         if group:
837             for name in group:
838                 if name[0] == '-': group_names.append(name[1:])
839                 else: group_names.append(name)
841         # now actually loop through all the nodes we get from the filter and
842         # apply the template
843         if show_nodes:
844             matches = None
845             if nodeids is None:
846                 if search_text != '':
847                     matches = self.client.indexer.search(
848                         search_text.split(' '), self.cl)
849                 nodeids = self.cl.filter(matches, filterspec, sort, group)
850             for nodeid in nodeids:
851                 # check for a group heading
852                 if group_names:
853                     this_group = [self.cl.get(nodeid, name, _('[no value]'))
854                         for name in group_names]
855                     if this_group != old_group:
856                         l = []
857                         for name in group_names:
858                             prop = self.properties[name]
859                             if isinstance(prop, hyperdb.Link):
860                                 group_cl = self.db.classes[prop.classname]
861                                 key = group_cl.getkey()
862                                 value = self.cl.get(nodeid, name)
863                                 if value is None:
864                                     l.append(_('[unselected %(classname)s]')%{
865                                         'classname': prop.classname})
866                                 else:
867                                     l.append(group_cl.get(self.cl.get(nodeid,
868                                         name), key))
869                             elif isinstance(prop, hyperdb.Multilink):
870                                 group_cl = self.db.classes[prop.classname]
871                                 key = group_cl.getkey()
872                                 for value in self.cl.get(nodeid, name):
873                                     l.append(group_cl.get(value, key))
874                             else:
875                                 value = self.cl.get(nodeid, name, 
876                                     _('[no value]'))
877                                 if value is None:
878                                     value = _('[empty %(name)s]')%locals()
879                                 else:
880                                     value = str(value)
881                                 l.append(value)
882                         w('<tr class="section-bar">'
883                         '<td align=middle colspan=%s>'
884                         '<strong>%s</strong></td></tr>'%(
885                             len(columns), ', '.join(l)))
886                         old_group = this_group
888                 # display this node's row
889                 replace = IndexTemplateReplace(self.globals, locals(), columns)
890                 self.nodeid = nodeid
891                 w(replace.go(template))
892                 if matches:
893                     self.node_matches(matches[nodeid], len(columns))
894                 self.nodeid = None
896         w('</table>')
898         # display the filter section
899         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
900                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
901             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
902             self.filter_section(filter_template, search_text, filter, 
903                 columns, group, all_filters, all_columns, show_customization)
904             # make sure that the sorting doesn't get lost either
905             if sort:
906                 w('<input type="hidden" name=":sort" value="%s">'%
907                     ','.join(sort))
908             w('</form>\n')
910     def node_matches(self, match, colspan):
911         ''' display the files and messages for a node that matched a
912             full text search
913         '''
914         w = self.client.write
916         message_links = []
917         file_links = []
918         if match.has_key('messages'):
919             for msgid in match['messages']:
920                 k = self.db.msg.labelprop()
921                 lab = self.db.msg.get(msgid, k)
922                 msgpath = 'msg%s'%msgid
923                 message_links.append('<a href="%(msgpath)s">%(lab)s</a>'
924                     %locals())
925             w(_('<tr class="row-hilite"><td colspan="%s">'
926                 '&nbsp;&nbsp;Matched messages: %s</td></tr>')%(
927                     colspan, ', '.join(message_links)))
929         if match.has_key('files'):
930             for fileid in match['files']:
931                 filename = self.db.file.get(fileid, 'name')
932                 filepath = 'file%s/%s'%(fileid, filename)
933                 file_links.append('<a href="%(filepath)s">%(filename)s</a>'
934                     %locals())
935             w(_('<tr class="row-hilite"><td colspan="%s">'
936                 '&nbsp;&nbsp;Matched files: %s</td></tr>')%(
937                     colspan, ', '.join(file_links)))
940     def filter_section(self, template, search_text, filter, columns, group, 
941             all_filters, all_columns, show_customization):
943         w = self.client.write
945         # wrap the template in a single table to ensure the whole widget
946         # is displayed at once
947         w('<table><tr><td>')
949         if template and filter:
950             # display the filter section
951             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
952             w('<tr class="location-bar">')
953             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
954             w('</tr>')
955             w('<tr>')
956             w('<th class="location-bar">Search terms</th>')
957             w('<td><input name="search_text" value="%s" size="50"></td>'%(
958                 search_text))
959             w('</tr>')
960             replace = IndexTemplateReplace(self.globals, locals(), filter)
961             w(replace.go(template))
962             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
963             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
964             w('</table>')
966         # now add in the filter/columns/group/etc config table form
967         w('<input type="hidden" name="show_customization" value="%s">' %
968             show_customization )
969         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
970         names = []
971         seen = {}
972         for name in all_filters + all_columns:
973             if self.properties.has_key(name) and not seen.has_key(name):
974                 names.append(name)
975             seen[name] = 1
976         if show_customization:
977             action = '-'
978         else:
979             action = '+'
980             # hide the values for filters, columns and grouping in the form
981             # if the customization widget is not visible
982             for name in names:
983                 if all_filters and name in filter:
984                     w('<input type="hidden" name=":filter" value="%s">' % name)
985                 if all_columns and name in columns:
986                     w('<input type="hidden" name=":columns" value="%s">' % name)
987                 if all_columns and name in group:
988                     w('<input type="hidden" name=":group" value="%s">' % name)
990         # TODO: The widget style can go into the stylesheet
991         w(_('<th align="left" colspan=%s>'
992           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
993           'customisation...</th></tr>\n')%(len(names)+1, action))
995         if not show_customization:
996             w('</table>\n')
997             return
999         w('<tr class="location-bar"><th>&nbsp;</th>')
1000         for name in names:
1001             w('<th>%s</th>'%name.capitalize())
1002         w('</tr>\n')
1004         # Filter
1005         if all_filters:
1006             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
1007             for name in names:
1008                 if name not in all_filters:
1009                     w('<td>&nbsp;</td>')
1010                     continue
1011                 if name in filter: checked=' checked'
1012                 else: checked=''
1013                 w('<td align=middle>\n')
1014                 w(' <input type="checkbox" name=":filter" value="%s" '
1015                   '%s></td>\n'%(name, checked))
1016             w('</tr>\n')
1018         # Columns
1019         if all_columns:
1020             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
1021             for name in names:
1022                 if name not in all_columns:
1023                     w('<td>&nbsp;</td>')
1024                     continue
1025                 if name in columns: checked=' checked'
1026                 else: checked=''
1027                 w('<td align=middle>\n')
1028                 w(' <input type="checkbox" name=":columns" value="%s"'
1029                   '%s></td>\n'%(name, checked))
1030             w('</tr>\n')
1032             # Grouping
1033             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
1034             for name in names:
1035                 if name not in all_columns:
1036                     w('<td>&nbsp;</td>')
1037                     continue
1038                 if name in group: checked=' checked'
1039                 else: checked=''
1040                 w('<td align=middle>\n')
1041                 w(' <input type="checkbox" name=":group" value="%s"'
1042                   '%s></td>\n'%(name, checked))
1043             w('</tr>\n')
1045         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
1046         w('<td colspan="%s">'%len(names))
1047         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
1048         w('</tr>\n')
1049         w('</table>\n')
1051         # and the outer table
1052         w('</td></tr></table>')
1055     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
1056         l = []
1057         w = l.append
1058         for k, v in filterspec.items():
1059             k = urllib.quote(k)
1060             if type(v) == type([]):
1061                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
1062             else:
1063                 w('%s=%s'%(k, urllib.quote(v)))
1064         if columns:
1065             w(':columns=%s'%','.join(map(urllib.quote, columns)))
1066         if filter:
1067             w(':filter=%s'%','.join(map(urllib.quote, filter)))
1068         if group:
1069             w(':group=%s'%','.join(map(urllib.quote, group)))
1070         m = []
1071         s_dir = ''
1072         for name in sort:
1073             dir = name[0]
1074             if dir == '-':
1075                 name = name[1:]
1076             else:
1077                 dir = ''
1078             if sort_name == name:
1079                 if dir == '-':
1080                     s_dir = ''
1081                 else:
1082                     s_dir = '-'
1083             else:
1084                 m.append(dir+urllib.quote(name))
1085         m.insert(0, s_dir+urllib.quote(sort_name))
1086         # so things don't get completely out of hand, limit the sort to
1087         # two columns
1088         w(':sort=%s'%','.join(m[:2]))
1089         return '&'.join(l)
1093 #   ITEM TEMPLATES
1095 class ItemTemplateReplace:
1096     '''Regular-expression based parser that turns the template into HTML. 
1097     '''
1098     def __init__(self, globals, locals, cl, nodeid):
1099         self.globals = globals
1100         self.locals = locals
1101         self.cl = cl
1102         self.nodeid = nodeid
1104     replace=re.compile(
1105         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1106         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1107     def go(self, text):
1108         return self.replace.sub(self, text)
1110     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1111         if m.group('name'):
1112             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1113                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1114                     self.nodeid)
1115                 return replace.go(m.group('text'))
1116             else:
1117                 return ''
1118         if m.group('display'):
1119             command = m.group('command')
1120             return eval(command, self.globals, self.locals)
1121         return '*** unhandled match: %s'%str(m.groupdict())
1124 class ItemTemplate(TemplateFunctions):
1125     '''Templating functionality specifically for item (node) display
1126     '''
1127     def __init__(self, client, templates, classname):
1128         TemplateFunctions.__init__(self)
1129         self.client = client
1130         self.instance = client.instance
1131         self.templates = templates
1132         self.classname = classname
1134         # derived
1135         self.db = self.client.db
1136         self.cl = self.db.classes[self.classname]
1137         self.properties = self.cl.getprops()
1139     def render(self, nodeid):
1140         self.nodeid = nodeid
1142         if (self.properties.has_key('type') and
1143                 self.properties.has_key('content')):
1144             pass
1145             # XXX we really want to return this as a downloadable...
1146             #  currently I handle this at a higher level by detecting 'file'
1147             #  designators...
1149         w = self.client.write
1150         w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1151             self.classname, nodeid))
1152         s = open(os.path.join(self.templates, self.classname+'.item')).read()
1153         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1154         w(replace.go(s))
1155         w('</form>')
1158 class NewItemTemplate(TemplateFunctions):
1159     '''Templating functionality specifically for NEW item (node) display
1160     '''
1161     def __init__(self, client, templates, classname):
1162         TemplateFunctions.__init__(self)
1163         self.client = client
1164         self.instance = client.instance
1165         self.templates = templates
1166         self.classname = classname
1168         # derived
1169         self.db = self.client.db
1170         self.cl = self.db.classes[self.classname]
1171         self.properties = self.cl.getprops()
1173     def render(self, form):
1174         self.form = form
1175         w = self.client.write
1176         c = self.classname
1177         try:
1178             s = open(os.path.join(self.templates, c+'.newitem')).read()
1179         except IOError:
1180             s = open(os.path.join(self.templates, c+'.item')).read()
1181         w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1182         for key in form.keys():
1183             if key[0] == ':':
1184                 value = form[key].value
1185                 if type(value) != type([]): value = [value]
1186                 for value in value:
1187                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
1188         replace = ItemTemplateReplace(self.globals, locals(), None, None)
1189         w(replace.go(s))
1190         w('</form>')
1193 # $Log: not supported by cvs2svn $
1194 # Revision 1.91  2002/05/31 00:08:02  richard
1195 # can now just display a link/multilink id - useful for stylesheet stuff
1197 # Revision 1.90  2002/05/25 07:16:24  rochecompaan
1198 # Merged search_indexing-branch with HEAD
1200 # Revision 1.89  2002/05/15 06:34:47  richard
1201 # forgot to fix the templating for last change
1203 # Revision 1.88  2002/04/24 08:34:35  rochecompaan
1204 # Sorting was applied to all nodes of the MultiLink class instead of
1205 # the nodes that are actually linked to in the "field" template
1206 # function.  This adds about 20+ seconds in the display of an issue if
1207 # your database has a 1000 or more issue in it.
1209 # Revision 1.87  2002/04/03 06:12:46  richard
1210 # Fix for date properties as labels.
1212 # Revision 1.86  2002/04/03 05:54:31  richard
1213 # Fixed serialisation problem by moving the serialisation step out of the
1214 # hyperdb.Class (get, set) into the hyperdb.Database.
1216 # Also fixed htmltemplate after the showid changes I made yesterday.
1218 # Unit tests for all of the above written.
1220 # Revision 1.85  2002/04/02 01:40:58  richard
1221 #  . link() htmltemplate function now has a "showid" option for links and
1222 #    multilinks. When true, it only displays the linked node id as the anchor
1223 #    text. The link value is displayed as a tooltip using the title anchor
1224 #    attribute.
1226 # Revision 1.84.2.2  2002/04/20 13:23:32  rochecompaan
1227 # We now have a separate search page for nodes.  Search links for
1228 # different classes can be customized in instance_config similar to
1229 # index links.
1231 # Revision 1.84.2.1  2002/04/19 19:54:42  rochecompaan
1232 # cgi_client.py
1233 #     removed search link for the time being
1234 #     moved rendering of matches to htmltemplate
1235 # hyperdb.py
1236 #     filtering of nodes on full text search incorporated in filter method
1237 # roundupdb.py
1238 #     added paramater to call of filter method
1239 # roundup_indexer.py
1240 #     added search method to RoundupIndexer class
1242 # Revision 1.84  2002/03/29 19:41:48  rochecompaan
1243 #  . Fixed display of mutlilink properties when using the template
1244 #    functions, menu and plain.
1246 # Revision 1.83  2002/02/27 04:14:31  richard
1247 # Ran it through pychecker, made fixes
1249 # Revision 1.82  2002/02/21 23:11:45  richard
1250 #  . fixed some problems in date calculations (calendar.py doesn't handle over-
1251 #    and under-flow). Also, hour/minute/second intervals may now be more than
1252 #    99 each.
1254 # Revision 1.81  2002/02/21 07:21:38  richard
1255 # docco
1257 # Revision 1.80  2002/02/21 07:19:08  richard
1258 # ... and label, width and height control for extra flavour!
1260 # Revision 1.79  2002/02/21 06:57:38  richard
1261 #  . Added popup help for classes using the classhelp html template function.
1262 #    - add <display call="classhelp('priority', 'id,name,description')">
1263 #      to an item page, and it generates a link to a popup window which displays
1264 #      the id, name and description for the priority class. The description
1265 #      field won't exist in most installations, but it will be added to the
1266 #      default templates.
1268 # Revision 1.78  2002/02/21 06:23:00  richard
1269 # *** empty log message ***
1271 # Revision 1.77  2002/02/20 05:05:29  richard
1272 #  . Added simple editing for classes that don't define a templated interface.
1273 #    - access using the admin "class list" interface
1274 #    - limited to admin-only
1275 #    - requires the csv module from object-craft (url given if it's missing)
1277 # Revision 1.76  2002/02/16 09:10:52  richard
1278 # oops
1280 # Revision 1.75  2002/02/16 08:43:23  richard
1281 #  . #517906 ] Attribute order in "View customisation"
1283 # Revision 1.74  2002/02/16 08:39:42  richard
1284 #  . #516854 ] "My Issues" and redisplay
1286 # Revision 1.73  2002/02/15 07:08:44  richard
1287 #  . Alternate email addresses are now available for users. See the MIGRATION
1288 #    file for info on how to activate the feature.
1290 # Revision 1.72  2002/02/14 23:39:18  richard
1291 # . All forms now have "double-submit" protection when Javascript is enabled
1292 #   on the client-side.
1294 # Revision 1.71  2002/01/23 06:15:24  richard
1295 # real (non-string, duh) sorting of lists by node id
1297 # Revision 1.70  2002/01/23 05:47:57  richard
1298 # more HTML template cleanup and unit tests
1300 # Revision 1.69  2002/01/23 05:10:27  richard
1301 # More HTML template cleanup and unit tests.
1302 #  - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1303 #    templates, but link(is_download=1) will still work for existing templates]
1305 # Revision 1.68  2002/01/22 22:55:28  richard
1306 #  . htmltemplate list() wasn't sorting...
1308 # Revision 1.67  2002/01/22 22:46:22  richard
1309 # more htmltemplate cleanups and unit tests
1311 # Revision 1.66  2002/01/22 06:35:40  richard
1312 # more htmltemplate tests and cleanup
1314 # Revision 1.65  2002/01/22 00:12:06  richard
1315 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1316 # off the implementation of some of the functions so they behave sanely.
1318 # Revision 1.64  2002/01/21 03:25:59  richard
1319 # oops
1321 # Revision 1.63  2002/01/21 02:59:10  richard
1322 # Fixed up the HTML display of history so valid links are actually displayed.
1323 # Oh for some unit tests! :(
1325 # Revision 1.62  2002/01/18 08:36:12  grubert
1326 #  . add nowrap to history table date cell i.e. <td nowrap ...
1328 # Revision 1.61  2002/01/17 23:04:53  richard
1329 #  . much nicer history display (actualy real handling of property types etc)
1331 # Revision 1.60  2002/01/17 08:48:19  grubert
1332 #  . display superseder as html link in history.
1334 # Revision 1.59  2002/01/17 07:58:24  grubert
1335 #  . display links a html link in history.
1337 # Revision 1.58  2002/01/15 00:50:03  richard
1338 # #502949 ] index view for non-issues and redisplay
1340 # Revision 1.57  2002/01/14 23:31:21  richard
1341 # reverted the change that had plain() hyperlinking the link displays -
1342 # that's what link() is for!
1344 # Revision 1.56  2002/01/14 07:04:36  richard
1345 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1346 #    the linked node's page.
1347 #    ... this allows a display very similar to bugzilla's where you can actually
1348 #    find out information about the linked node.
1350 # Revision 1.55  2002/01/14 06:45:03  richard
1351 #  . #502953 ] nosy-like treatment of other multilinks
1352 #    ... had to revert most of the previous change to the multilink field
1353 #    display... not good.
1355 # Revision 1.54  2002/01/14 05:16:51  richard
1356 # The submit buttons need a name attribute or mozilla won't submit without a
1357 # file upload. Yeah, that's bloody obscure. Grr.
1359 # Revision 1.53  2002/01/14 04:03:32  richard
1360 # How about that ... date fields have never worked ...
1362 # Revision 1.52  2002/01/14 02:20:14  richard
1363 #  . changed all config accesses so they access either the instance or the
1364 #    config attriubute on the db. This means that all config is obtained from
1365 #    instance_config instead of the mish-mash of classes. This will make
1366 #    switching to a ConfigParser setup easier too, I hope.
1368 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1369 # 0.5.0 switch, I hope!)
1371 # Revision 1.51  2002/01/10 10:02:15  grubert
1372 # In do_history: replace "." in date by " " so html wraps more sensible.
1373 # Should this be done in date's string converter ?
1375 # Revision 1.50  2002/01/05 02:35:10  richard
1376 # I18N'ification
1378 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1379 # Features added:
1380 #  .  Multilink properties are now displayed as comma separated values in
1381 #     a textbox
1382 #  .  The add user link is now only visible to the admin user
1383 #  .  Modified the mail gateway to reject submissions from unknown
1384 #     addresses if ANONYMOUS_ACCESS is denied
1386 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1387 # Bugs fixed:
1388 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1389 #     lost somewhere
1390 #   . Internet Explorer submits full path for filename - we now strip away
1391 #     the path
1392 # Features added:
1393 #   . Link and multilink properties are now displayed sorted in the cgi
1394 #     interface
1396 # Revision 1.47  2001/11/26 22:55:56  richard
1397 # Feature:
1398 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1399 #    the instance.
1400 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1401 #    signature info in e-mails.
1402 #  . Some more flexibility in the mail gateway and more error handling.
1403 #  . Login now takes you to the page you back to the were denied access to.
1405 # Fixed:
1406 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1408 # Revision 1.46  2001/11/24 00:53:12  jhermann
1409 # "except:" is bad, bad , bad!
1411 # Revision 1.45  2001/11/22 15:46:42  jhermann
1412 # Added module docstrings to all modules.
1414 # Revision 1.44  2001/11/21 23:35:45  jhermann
1415 # Added globbing for win32, and sample marking in a 2nd file to test it
1417 # Revision 1.43  2001/11/21 04:04:43  richard
1418 # *sigh* more missing value handling
1420 # Revision 1.42  2001/11/21 03:40:54  richard
1421 # more new property handling
1423 # Revision 1.41  2001/11/15 10:26:01  richard
1424 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1426 # Revision 1.40  2001/11/03 01:56:51  richard
1427 # More HTML compliance fixes. This will probably fix the Netscape problem
1428 # too.
1430 # Revision 1.39  2001/11/03 01:43:47  richard
1431 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1433 # Revision 1.38  2001/10/31 06:58:51  richard
1434 # Added the wrap="hard" attribute to the textarea of the note field so the
1435 # messages wrap sanely.
1437 # Revision 1.37  2001/10/31 06:24:35  richard
1438 # Added do_stext to htmltemplate, thanks Brad Clements.
1440 # Revision 1.36  2001/10/28 22:51:38  richard
1441 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1443 # Revision 1.35  2001/10/24 00:04:41  richard
1444 # Removed the "infinite authentication loop", thanks Roch'e
1446 # Revision 1.34  2001/10/23 22:56:36  richard
1447 # Bugfix in filter "widget" placement, thanks Roch'e
1449 # Revision 1.33  2001/10/23 01:00:18  richard
1450 # Re-enabled login and registration access after lopping them off via
1451 # disabling access for anonymous users.
1452 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1453 # a couple of bugs while I was there. Probably introduced a couple, but
1454 # things seem to work OK at the moment.
1456 # Revision 1.32  2001/10/22 03:25:01  richard
1457 # Added configuration for:
1458 #  . anonymous user access and registration (deny/allow)
1459 #  . filter "widget" location on index page (top, bottom, both)
1460 # Updated some documentation.
1462 # Revision 1.31  2001/10/21 07:26:35  richard
1463 # feature #473127: Filenames. I modified the file.index and htmltemplate
1464 #  source so that the filename is used in the link and the creation
1465 #  information is displayed.
1467 # Revision 1.30  2001/10/21 04:44:50  richard
1468 # bug #473124: UI inconsistency with Link fields.
1469 #    This also prompted me to fix a fairly long-standing usability issue -
1470 #    that of being able to turn off certain filters.
1472 # Revision 1.29  2001/10/21 00:17:56  richard
1473 # CGI interface view customisation section may now be hidden (patch from
1474 #  Roch'e Compaan.)
1476 # Revision 1.28  2001/10/21 00:00:16  richard
1477 # Fixed Checklist function - wasn't always working on a list.
1479 # Revision 1.27  2001/10/20 12:13:44  richard
1480 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1482 # Revision 1.26  2001/10/14 10:55:00  richard
1483 # Handle empty strings in HTML template Link function
1485 # Revision 1.25  2001/10/09 07:25:59  richard
1486 # Added the Password property type. See "pydoc roundup.password" for
1487 # implementation details. Have updated some of the documentation too.
1489 # Revision 1.24  2001/09/27 06:45:58  richard
1490 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1491 # on the plain() template function to escape the text for HTML.
1493 # Revision 1.23  2001/09/10 09:47:18  richard
1494 # Fixed bug in the generation of links to Link/Multilink in indexes.
1495 #   (thanks Hubert Hoegl)
1496 # Added AssignedTo to the "classic" schema's item page.
1498 # Revision 1.22  2001/08/30 06:01:17  richard
1499 # Fixed missing import in mailgw :(
1501 # Revision 1.21  2001/08/16 07:34:59  richard
1502 # better CGI text searching - but hidden filter fields are disappearing...
1504 # Revision 1.20  2001/08/15 23:43:18  richard
1505 # Fixed some isFooTypes that I missed.
1506 # Refactored some code in the CGI code.
1508 # Revision 1.19  2001/08/12 06:32:36  richard
1509 # using isinstance(blah, Foo) now instead of isFooType
1511 # Revision 1.18  2001/08/07 00:24:42  richard
1512 # stupid typo
1514 # Revision 1.17  2001/08/07 00:15:51  richard
1515 # Added the copyright/license notice to (nearly) all files at request of
1516 # Bizar Software.
1518 # Revision 1.16  2001/08/01 03:52:23  richard
1519 # Checklist was using wrong name.
1521 # Revision 1.15  2001/07/30 08:12:17  richard
1522 # Added time logging and file uploading to the templates.
1524 # Revision 1.14  2001/07/30 06:17:45  richard
1525 # Features:
1526 #  . Added ability for cgi newblah forms to indicate that the new node
1527 #    should be linked somewhere.
1528 # Fixed:
1529 #  . Fixed the agument handling for the roundup-admin find command.
1530 #  . Fixed handling of summary when no note supplied for newblah. Again.
1531 #  . Fixed detection of no form in htmltemplate Field display.
1533 # Revision 1.13  2001/07/30 02:37:53  richard
1534 # Temporary measure until we have decent schema migration.
1536 # Revision 1.12  2001/07/30 01:24:33  richard
1537 # Handles new node display now.
1539 # Revision 1.11  2001/07/29 09:31:35  richard
1540 # oops
1542 # Revision 1.10  2001/07/29 09:28:23  richard
1543 # Fixed sorting by clicking on column headings.
1545 # Revision 1.9  2001/07/29 08:27:40  richard
1546 # Fixed handling of passed-in values in form elements (ie. during a
1547 # drill-down)
1549 # Revision 1.8  2001/07/29 07:01:39  richard
1550 # Added vim command to all source so that we don't get no steenkin' tabs :)
1552 # Revision 1.7  2001/07/29 05:36:14  richard
1553 # Cleanup of the link label generation.
1555 # Revision 1.6  2001/07/29 04:06:42  richard
1556 # Fixed problem in link display when Link value is None.
1558 # Revision 1.5  2001/07/28 08:17:09  richard
1559 # fixed use of stylesheet
1561 # Revision 1.4  2001/07/28 07:59:53  richard
1562 # Replaced errno integers with their module values.
1563 # De-tabbed templatebuilder.py
1565 # Revision 1.3  2001/07/25 03:39:47  richard
1566 # Hrm - displaying links to classes that don't specify a key property. I've
1567 # got it defaulting to 'name', then 'title' and then a "random" property (first
1568 # one returned by getprops().keys().
1569 # Needs to be moved onto the Class I think...
1571 # Revision 1.2  2001/07/22 12:09:32  richard
1572 # Final commit of Grande Splite
1574 # Revision 1.1  2001/07/22 11:58:35  richard
1575 # More Grande Splite
1578 # vim: set filetype=python ts=4 sw=4 et si