Code

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