Code

. added sorting of checklist HTML display
[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.98 2002-07-10 00:17:46 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, sortby=None):
484         ''' for a Link or Multilink property, display checkboxes for the
485             available choices to permit filtering
487             sort the checklist by the argument (+/- property name)
488         '''
489         propclass = self.properties[property]
490         if (not isinstance(propclass, hyperdb.Link) and not
491                 isinstance(propclass, hyperdb.Multilink)):
492             return _('[Checklist: not a link]')
494         # get our current checkbox state
495         if self.nodeid:
496             # get the info from the node - make sure it's a list
497             if isinstance(propclass, hyperdb.Link):
498                 value = [self.cl.get(self.nodeid, property)]
499             else:
500                 value = self.cl.get(self.nodeid, property)
501         elif self.filterspec is not None:
502             # get the state from the filter specification (always a list)
503             value = self.filterspec.get(property, [])
504         else:
505             # it's a new node, so there's no state
506             value = []
508         # so we can map to the linked node's "lable" property
509         linkcl = self.db.classes[propclass.classname]
510         l = []
511         k = linkcl.labelprop(1)
513         # build list of options and then sort it, either
514         # by id + label or <sortby>-value + label;
515         # a minus reverses the sort order, while + or no
516         # prefix sort in increasing order
517         reversed = 0
518         if sortby:
519             if sortby[0] == '-':
520                 reversed = 1
521                 sortby = sortby[1:]
522             elif sortby[0] == '+':
523                 sortby = sortby[1:]
524         options = []
525         for optionid in linkcl.list():
526             if sortby:
527                 sortval = linkcl.get(optionid, sortby)
528             else:
529                 sortval = int(optionid)
530             option = cgi.escape(str(linkcl.get(optionid, k)))
531             options.append((sortval, option, optionid))
532         options.sort()
533         if reversed:
534             options.reverse()
536         # build checkboxes
537         for sortval, option, optionid in options:
538             if optionid in value or option in value:
539                 checked = 'checked'
540             else:
541                 checked = ''
542             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
543                 option, checked, property, option))
545         # for Links, allow the "unselected" option too
546         if isinstance(propclass, hyperdb.Link):
547             if value is None or '-1' in value:
548                 checked = 'checked'
549             else:
550                 checked = ''
551             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
552                 'value="-1">')%(checked, property))
553         return '\n'.join(l)
555     def do_note(self, rows=5, cols=80):
556         ''' display a "note" field, which is a text area for entering a note to
557             go along with a change. 
558         '''
559         # TODO: pull the value from the form
560         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
561             '</textarea>'%(rows, cols)
563     # XXX new function
564     def do_list(self, property, reverse=0):
565         ''' list the items specified by property using the standard index for
566             the class
567         '''
568         propcl = self.properties[property]
569         if not isinstance(propcl, hyperdb.Multilink):
570             return _('[List: not a Multilink]')
572         value = self.determine_value(property)
573         if not value:
574             return ''
576         # sort, possibly revers and then re-stringify
577         value = map(int, value)
578         value.sort()
579         if reverse:
580             value.reverse()
581         value = map(str, value)
583         # render the sub-index into a string
584         fp = StringIO.StringIO()
585         try:
586             write_save = self.client.write
587             self.client.write = fp.write
588             index = IndexTemplate(self.client, self.templates, propcl.classname)
589             index.render(nodeids=value, show_display_form=0)
590         finally:
591             self.client.write = write_save
593         return fp.getvalue()
595     # XXX new function
596     def do_history(self, direction='descending'):
597         ''' list the history of the item
599             If "direction" is 'descending' then the most recent event will
600             be displayed first. If it is 'ascending' then the oldest event
601             will be displayed first.
602         '''
603         if self.nodeid is None:
604             return _("[History: node doesn't exist]")
606         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
607             '<tr class="list-header">',
608             _('<th align=left><span class="list-item">Date</span></th>'),
609             _('<th align=left><span class="list-item">User</span></th>'),
610             _('<th align=left><span class="list-item">Action</span></th>'),
611             _('<th align=left><span class="list-item">Args</span></th>'),
612             '</tr>']
614         comments = {}
615         history = self.cl.history(self.nodeid)
616         history.sort()
617         if direction == 'descending':
618             history.reverse()
619         for id, evt_date, user, action, args in history:
620             date_s = str(evt_date).replace("."," ")
621             arg_s = ''
622             if action == 'link' and type(args) == type(()):
623                 if len(args) == 3:
624                     linkcl, linkid, key = args
625                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
626                         linkcl, linkid, key)
627                 else:
628                     arg_s = str(args)
630             elif action == 'unlink' and type(args) == type(()):
631                 if len(args) == 3:
632                     linkcl, linkid, key = args
633                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
634                         linkcl, linkid, key)
635                 else:
636                     arg_s = str(args)
638             elif type(args) == type({}):
639                 cell = []
640                 for k in args.keys():
641                     # try to get the relevant property and treat it
642                     # specially
643                     try:
644                         prop = self.properties[k]
645                     except:
646                         prop = None
647                     if prop is not None:
648                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
649                                 isinstance(prop, hyperdb.Link)):
650                             # figure what the link class is
651                             classname = prop.classname
652                             try:
653                                 linkcl = self.db.classes[classname]
654                             except KeyError:
655                                 labelprop = None
656                                 comments[classname] = _('''The linked class
657                                     %(classname)s no longer exists''')%locals()
658                             labelprop = linkcl.labelprop(1)
659                             hrefable = os.path.exists(
660                                 os.path.join(self.templates, classname+'.item'))
662                         if isinstance(prop, hyperdb.Multilink) and \
663                                 len(args[k]) > 0:
664                             ml = []
665                             for linkid in args[k]:
666                                 label = classname + linkid
667                                 # if we have a label property, try to use it
668                                 # TODO: test for node existence even when
669                                 # there's no labelprop!
670                                 try:
671                                     if labelprop is not None:
672                                         label = linkcl.get(linkid, labelprop)
673                                 except IndexError:
674                                     comments['no_link'] = _('''<strike>The
675                                         linked node no longer
676                                         exists</strike>''')
677                                     ml.append('<strike>%s</strike>'%label)
678                                 else:
679                                     if hrefable:
680                                         ml.append('<a href="%s%s">%s</a>'%(
681                                             classname, linkid, label))
682                                     else:
683                                         ml.append(label)
684                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
685                         elif isinstance(prop, hyperdb.Link) and args[k]:
686                             label = classname + args[k]
687                             # if we have a label property, try to use it
688                             # TODO: test for node existence even when
689                             # there's no labelprop!
690                             if labelprop is not None:
691                                 try:
692                                     label = linkcl.get(args[k], labelprop)
693                                 except IndexError:
694                                     comments['no_link'] = _('''<strike>The
695                                         linked node no longer
696                                         exists</strike>''')
697                                     cell.append(' <strike>%s</strike>,\n'%label)
698                                     # "flag" this is done .... euwww
699                                     label = None
700                             if label is not None:
701                                 if hrefable:
702                                     cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
703                                         classname, args[k], label))
704                                 else:
705                                     cell.append('%s: %s' % (k,label))
707                         elif isinstance(prop, hyperdb.Date) and args[k]:
708                             d = date.Date(args[k])
709                             cell.append('%s: %s'%(k, str(d)))
711                         elif isinstance(prop, hyperdb.Interval) and args[k]:
712                             d = date.Interval(args[k])
713                             cell.append('%s: %s'%(k, str(d)))
715                         elif isinstance(prop, hyperdb.String) and args[k]:
716                             cell.append('%s: %s'%(k, cgi.escape(args[k])))
718                         elif not args[k]:
719                             cell.append('%s: (no value)\n'%k)
721                         else:
722                             cell.append('%s: %s\n'%(k, str(args[k])))
723                     else:
724                         # property no longer exists
725                         comments['no_exist'] = _('''<em>The indicated property
726                             no longer exists</em>''')
727                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
728                 arg_s = '<br />'.join(cell)
729             else:
730                 # unkown event!!
731                 comments['unknown'] = _('''<strong><em>This event is not
732                     handled by the history display!</em></strong>''')
733                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
734             date_s = date_s.replace(' ', '&nbsp;')
735             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
736                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
737                 user, action, arg_s))
738         if comments:
739             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
740         for entry in comments.values():
741             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
742         l.append('</table>')
743         return '\n'.join(l)
745     # XXX new function
746     def do_submit(self):
747         ''' add a submit button for the item
748         '''
749         if self.nodeid:
750             return _('<input type="submit" name="submit" value="Submit Changes">')
751         elif self.form is not None:
752             return _('<input type="submit" name="submit" value="Submit New Entry">')
753         else:
754             return _('[Submit: not called from item]')
756     def do_classhelp(self, classname, properties, label='?', width='400',
757             height='400'):
758         '''pop up a javascript window with class help
760            This generates a link to a popup window which displays the 
761            properties indicated by "properties" of the class named by
762            "classname". The "properties" should be a comma-separated list
763            (eg. 'id,name,description').
765            You may optionally override the label displayed, the width and
766            height. The popup window will be resizable and scrollable.
767         '''
768         return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
769             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
770             properties, width, height, label)
772     def do_email(self, property, escape=0):
773         '''display the property as one or more "fudged" email addrs
774         '''
775         if not self.nodeid and self.form is None:
776             return _('[Email: not called from item]')
777         propclass = self.properties[property]
778         if self.nodeid:
779             # get the value for this property
780             try:
781                 value = self.cl.get(self.nodeid, property)
782             except KeyError:
783                 # a KeyError here means that the node doesn't have a value
784                 # for the specified property
785                 value = ''
786         else:
787             value = ''
788         if isinstance(propclass, hyperdb.String):
789             if value is None: value = ''
790             else: value = str(value)
791             value = value.replace('@', ' at ')
792             value = value.replace('.', ' ')
793         else:
794             value = _('[Email: not a string]')%locals()
795         if escape:
796             value = cgi.escape(value)
797         return value
801 #   INDEX TEMPLATES
803 class IndexTemplateReplace:
804     '''Regular-expression based parser that turns the template into HTML. 
805     '''
806     def __init__(self, globals, locals, props):
807         self.globals = globals
808         self.locals = locals
809         self.props = props
811     replace=re.compile(
812         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
813         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
814     def go(self, text):
815         newtext = self.replace.sub(self, text)
816         self.locals = self.globals = None
817         return newtext
819     def __call__(self, m, search_text=None, filter=None, columns=None,
820             sort=None, group=None):
821         if m.group('name'):
822             if m.group('name') in self.props:
823                 text = m.group('text')
824                 replace = self.__class__(self.globals, {}, self.props)
825                 return replace.go(text)
826             else:
827                 return ''
828         if m.group('display'):
829             command = m.group('command')
830             return eval(command, self.globals, self.locals)
831         return '*** unhandled match: %s'%str(m.groupdict())
833 class IndexTemplate(TemplateFunctions):
834     '''Templating functionality specifically for index pages
835     '''
836     def __init__(self, client, templates, classname):
837         TemplateFunctions.__init__(self)
838         self.client = client
839         self.instance = client.instance
840         self.templates = templates
841         self.classname = classname
843         # derived
844         self.db = self.client.db
845         self.cl = self.db.classes[self.classname]
846         self.properties = self.cl.getprops()
848     def clear(self):
849         self.db = self.cl = self.properties = None
850         TemplateFunctions.clear(self)
851         
852     def buildurl(self, filterspec, search_text, filter, columns, sort, group, pagesize):
853         d = {'pagesize':pagesize, 'pagesize':pagesize, 'classname':self.classname}
854         d['filter'] = ','.join(map(urllib.quote,filter))
855         d['columns'] = ','.join(map(urllib.quote,columns))
856         d['sort'] = ','.join(map(urllib.quote,sort))
857         d['group'] = ','.join(map(urllib.quote,group))
858         tmp = []
859         for col, vals in filterspec.items():
860             vals = ','.join(map(urllib.quote,vals))
861             tmp.append('%s=%s' % (col, vals))
862         d['filters'] = '&'.join(tmp)
863         return '%(classname)s?%(filters)s&:sort=%(sort)s&:filter=%(filter)s&:group=%(group)s&:columns=%(columns)s&:pagesize=%(pagesize)s' % d
864     
865     col_re=re.compile(r'<property\s+name="([^>]+)">')
866     def render(self, filterspec={}, search_text='', filter=[], columns=[], 
867             sort=[], group=[], show_display_form=1, nodeids=None,
868             show_customization=1, show_nodes=1, pagesize=50, startwith=0):
869         
870         self.filterspec = filterspec
872         w = self.client.write
874         # XXX deviate from spec here ...
875         # load the index section template and figure the default columns from it
876         try:
877             template = open(os.path.join(self.templates,
878                 self.classname+'.index')).read()
879         except IOError, error:
880             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
881             raise MissingTemplateError, self.classname+'.index'
882         all_columns = self.col_re.findall(template)
883         if not columns:
884             columns = []
885             for name in all_columns:
886                 columns.append(name)
887         else:
888             # re-sort columns to be the same order as all_columns
889             l = []
890             for name in all_columns:
891                 if name in columns:
892                     l.append(name)
893             columns = l
895         # display the filter section
896         if (show_display_form and 
897                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
898             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
899             self.filter_section(search_text, filter, columns, group, all_columns, sort, filterspec,
900                                 pagesize, startwith)
902         # now display the index section
903         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
904         w('<tr class="list-header">\n')
905         for name in columns:
906             cname = name.capitalize()
907             if show_display_form:
908                 sb = self.sortby(name, filterspec, columns, filter, group, sort, pagesize, startwith)
909                 anchor = "%s?%s"%(self.classname, sb)
910                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
911                     anchor, cname))
912             else:
913                 w('<td><span class="list-header">%s</span></td>\n'%cname)
914         w('</tr>\n')
916         # this stuff is used for group headings - optimise the group names
917         old_group = None
918         group_names = []
919         if group:
920             for name in group:
921                 if name[0] == '-': group_names.append(name[1:])
922                 else: group_names.append(name)
924         # now actually loop through all the nodes we get from the filter and
925         # apply the template
926         if show_nodes:
927             matches = None
928             if nodeids is None:
929                 if search_text != '':
930                     matches = self.db.indexer.search(
931                         search_text.split(' '), self.cl)
932                 nodeids = self.cl.filter(matches, filterspec, sort, group)
933             for nodeid in nodeids[startwith:startwith+pagesize]:
934                 # check for a group heading
935                 if group_names:
936                     this_group = [self.cl.get(nodeid, name, _('[no value]'))
937                         for name in group_names]
938                     if this_group != old_group:
939                         l = []
940                         for name in group_names:
941                             prop = self.properties[name]
942                             if isinstance(prop, hyperdb.Link):
943                                 group_cl = self.db.classes[prop.classname]
944                                 key = group_cl.getkey()
945                                 if key is None:
946                                     key = group_cl.labelprop()
947                                 value = self.cl.get(nodeid, name)
948                                 if value is None:
949                                     l.append(_('[unselected %(classname)s]')%{
950                                         'classname': prop.classname})
951                                 else:
952                                     l.append(group_cl.get(value, key))
953                             elif isinstance(prop, hyperdb.Multilink):
954                                 group_cl = self.db.classes[prop.classname]
955                                 key = group_cl.getkey()
956                                 for value in self.cl.get(nodeid, name):
957                                     l.append(group_cl.get(value, key))
958                             else:
959                                 value = self.cl.get(nodeid, name, 
960                                     _('[no value]'))
961                                 if value is None:
962                                     value = _('[empty %(name)s]')%locals()
963                                 else:
964                                     value = str(value)
965                                 l.append(value)
966                         w('<tr class="section-bar">'
967                         '<td align=middle colspan=%s>'
968                         '<strong>%s</strong></td></tr>'%(
969                             len(columns), ', '.join(l)))
970                         old_group = this_group
972                 # display this node's row
973                 replace = IndexTemplateReplace(self.globals, locals(), columns)
974                 self.nodeid = nodeid
975                 w(replace.go(template))
976                 if matches:
977                     self.node_matches(matches[nodeid], len(columns))
978                 self.nodeid = None
980         w('</table>')
981         # the previous and next links
982         if nodeids:
983             baseurl = self.buildurl(filterspec, search_text, filter, columns, sort, group, pagesize)
984             if startwith > 0:
985                 prevurl = '<a href="%s&:startwith=%s">&lt;&lt; Previous page</a>' % \
986                           (baseurl, max(0, startwith-pagesize)) 
987             else:
988                 prevurl = "" 
989             if startwith + pagesize < len(nodeids):
990                 nexturl = '<a href="%s&:startwith=%s">Next page &gt;&gt;</a>' % (baseurl, startwith+pagesize)
991             else:
992                 nexturl = ""
993             if prevurl or nexturl:
994                 w('<table width="100%%"><tr><td width="50%%" align="center">%s</td><td width="50%%" align="center">%s</td></tr></table>' % (prevurl, nexturl))
996         # display the filter section
997         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
998                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
999             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
1000             self.filter_section(search_text, filter, columns, group, all_columns, sort, filterspec,
1001                                 pagesize, startwith)
1003         self.clear()
1005     def node_matches(self, match, colspan):
1006         ''' display the files and messages for a node that matched a
1007             full text search
1008         '''
1009         w = self.client.write
1011         message_links = []
1012         file_links = []
1013         if match.has_key('messages'):
1014             for msgid in match['messages']:
1015                 k = self.db.msg.labelprop(1)
1016                 lab = self.db.msg.get(msgid, k)
1017                 msgpath = 'msg%s'%msgid
1018                 message_links.append('<a href="%(msgpath)s">%(lab)s</a>'
1019                     %locals())
1020             w(_('<tr class="row-hilite"><td colspan="%s">'
1021                 '&nbsp;&nbsp;Matched messages: %s</td></tr>')%(
1022                     colspan, ', '.join(message_links)))
1024         if match.has_key('files'):
1025             for fileid in match['files']:
1026                 filename = self.db.file.get(fileid, 'name')
1027                 filepath = 'file%s/%s'%(fileid, filename)
1028                 file_links.append('<a href="%(filepath)s">%(filename)s</a>'
1029                     %locals())
1030             w(_('<tr class="row-hilite"><td colspan="%s">'
1031                 '&nbsp;&nbsp;Matched files: %s</td></tr>')%(
1032                     colspan, ', '.join(file_links)))
1035     def filter_section(self, search_text, filter, columns, group, all_columns, sort, filterspec,
1036                        pagesize, startwith):
1038         sortspec = {}
1039         for i in range(len(sort)):
1040             mod = ''
1041             colnm = sort[i]
1042             if colnm[0] == '-':
1043                 mod = '-'
1044                 colnm = colnm[1:]
1045             sortspec[colnm] = '%d%s' % (i+1, mod)
1046             
1047         startwith = 0
1048         w = self.client.write
1050         # display the filter section
1051         w(  '<br>\n')
1052         w(  '<table border=0 cellspacing=0 cellpadding=0>\n')
1053         w(  '<tr class="list-header">\n')
1054         w(_(' <th align="left" colspan="7">Filter specification...</th>\n'))
1055         w(  '</tr>\n')
1056         # see if we have any indexed properties
1057         if self.classname in self.db.config.HEADER_SEARCH_LINKS:
1058         #if self.properties.has_key('messages') or self.properties.has_key('files'):
1059             w(  '<tr class="location-bar">\n')
1060             w(  ' <td align="right" class="form-label"><b>Search Terms</b></td>\n')
1061             w(  ' <td>&nbsp</td>\n')
1062             w(  ' <td colspan=5 class="form-text"><input type="text" name="search_text" value="%s" size="50"></td>\n' % search_text)
1063             w(  '</tr>\n')
1064         w(  '<tr class="location-bar">\n')
1065         w(  ' <th align="center" width="20%">&nbsp;</th>\n')
1066         w(_(' <th align="center" width="10%">Show</th>\n'))
1067         w(_(' <th align="center" width="10%">Group</th>\n'))
1068         w(_(' <th align="center" width="10%">Sort</th>\n'))
1069         w(_(' <th colspan="3" align="center">Condition</th>\n'))
1070         w(  '</tr>\n')
1071         
1072         for nm in all_columns:
1073             propdescr = self.properties.get(nm, None)
1074             if not propdescr:
1075                 print "hey sysadmin - %s is not a property of %r" % (nm, self.classname)
1076                 continue
1077             w(  '<tr class="location-bar">\n')
1078             w(_(' <td align="right" class="form-label"><b>%s</b></td>\n' % nm.capitalize()))
1079             # show column - can't show multilinks
1080             if isinstance(propdescr, hyperdb.Multilink):
1081                 w(' <td></td>\n')
1082             else:
1083                 checked = columns and nm in columns or 0
1084                 checked = ('', 'checked')[checked]
1085                 w(' <td align="center" class="form-text"><input type="checkbox" name=":columns" value="%s" %s></td>\n' % (nm, checked) )
1086             # can only group on Link 
1087             if isinstance(propdescr, hyperdb.Link):
1088                 checked = group and nm in group or 0
1089                 checked = ('', 'checked')[checked]
1090                 w(' <td align="center" class="form-text"><input type="checkbox" name=":group" value="%s" %s></td>\n' % (nm, checked) )
1091             else:
1092                 w(' <td></td>\n')
1093             # sort - no sort on Multilinks
1094             if isinstance(propdescr, hyperdb.Multilink):
1095                 w('<td></td>\n')
1096             else:
1097                 val = sortspec.get(nm, '')
1098                 w('<td align="center" class="form-text"><input type="text" name=":%s_ss" size="3" value="%s"></td>\n' % (nm,val))
1099             # condition
1100             val = ''
1101             if isinstance(propdescr, hyperdb.Link):
1102                 op = "is in&nbsp;"
1103                 xtra = '<a href="javascript:help_window(\'classhelp?classname=%s&properties=id,%s\', \'200\', \'400\')"><b>(list)</b></a>'\
1104                        % (propdescr.classname, self.db.getclass(propdescr.classname).labelprop())
1105                 val = ','.join(filterspec.get(nm, ''))
1106             elif isinstance(propdescr, hyperdb.Multilink):
1107                 op = "contains&nbsp;"
1108                 xtra = '<a href="javascript:help_window(\'classhelp?classname=%s&properties=id,%s\', \'200\', \'400\')"><b>(list)</b></a>'\
1109                        % (propdescr.classname, self.db.getclass(propdescr.classname).labelprop())
1110                 val = ','.join(filterspec.get(nm, ''))
1111             elif isinstance(propdescr, hyperdb.String) and nm != 'id':
1112                 op = "equals&nbsp;"
1113                 xtra = ""
1114                 val = filterspec.get(nm, '')
1115             else:
1116                 w('<td></td><td></td><td></td></tr>\n')
1117                 continue
1118             checked = filter and nm in filter or 0
1119             checked = ('', 'checked')[checked]
1120             w(  ' <td class="form-text"><input type="checkbox" name=":filter" value="%s" %s></td>\n' % (nm, checked))
1121             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)))
1122             w(  '</tr>\n')
1123         w('<tr class="location-bar">\n')
1124         w(' <td colspan=7><hr></td>\n')
1125         w('</tr>\n')
1126         w('<tr class="location-bar">\n')
1127         w(_(' <td align="right" class="form-label">Pagesize</td>\n'))
1128         w(' <td colspan=2 align="center" class="form-text"><input type="text" name=":pagesize" size="3" value="%s"></td>\n' % pagesize)
1129         w(' <td colspan=4></td>\n')
1130         w('</tr>\n')
1131         w('<tr class="location-bar">\n')
1132         w(_(' <td align="right" class="form-label">Start With</td>\n'))
1133         w(' <td colspan=2 align="center" class="form-text"><input type="text" name=":startwith" size="3" value="%s"></td>\n' % startwith)
1134         w(' <td colspan=3 align="center" valign="center"><input type="submit" name="Query" value="Redisplay"></td>\n')
1135         w(' <td></td>\n')
1136         w('</tr>\n')
1137         w('</table>\n')
1139     def sortby(self, sort_name, filterspec, columns, filter, group, sort, pagesize, startwith):
1140         l = []
1141         w = l.append
1142         for k, v in filterspec.items():
1143             k = urllib.quote(k)
1144             if type(v) == type([]):
1145                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
1146             else:
1147                 w('%s=%s'%(k, urllib.quote(v)))
1148         if columns:
1149             w(':columns=%s'%','.join(map(urllib.quote, columns)))
1150         if filter:
1151             w(':filter=%s'%','.join(map(urllib.quote, filter)))
1152         if group:
1153             w(':group=%s'%','.join(map(urllib.quote, group)))
1154         w(':pagesize=%s' % pagesize)
1155         w(':startwith=%s' % startwith)
1156         m = []
1157         s_dir = ''
1158         for name in sort:
1159             dir = name[0]
1160             if dir == '-':
1161                 name = name[1:]
1162             else:
1163                 dir = ''
1164             if sort_name == name:
1165                 if dir == '-':
1166                     s_dir = ''
1167                 else:
1168                     s_dir = '-'
1169             else:
1170                 m.append(dir+urllib.quote(name))
1171         m.insert(0, s_dir+urllib.quote(sort_name))
1172         # so things don't get completely out of hand, limit the sort to
1173         # two columns
1174         w(':sort=%s'%','.join(m[:2]))
1175         return '&'.join(l)
1177
1178 #   ITEM TEMPLATES
1180 class ItemTemplateReplace:
1181     '''Regular-expression based parser that turns the template into HTML. 
1182     '''
1183     def __init__(self, globals, locals, cl, nodeid):
1184         self.globals = globals
1185         self.locals = locals
1186         self.cl = cl
1187         self.nodeid = nodeid
1189     replace=re.compile(
1190         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1191         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1192     def go(self, text):
1193         newtext = self.replace.sub(self, text)
1194         self.globals = self.locals = self.cl = None
1195         return newtext
1197     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1198         if m.group('name'):
1199             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1200                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1201                     self.nodeid)
1202                 return replace.go(m.group('text'))
1203             else:
1204                 return ''
1205         if m.group('display'):
1206             command = m.group('command')
1207             return eval(command, self.globals, self.locals)
1208         return '*** unhandled match: %s'%str(m.groupdict())
1211 class ItemTemplate(TemplateFunctions):
1212     '''Templating functionality specifically for item (node) display
1213     '''
1214     def __init__(self, client, templates, classname):
1215         TemplateFunctions.__init__(self)
1216         self.client = client
1217         self.instance = client.instance
1218         self.templates = templates
1219         self.classname = classname
1221         # derived
1222         self.db = self.client.db
1223         self.cl = self.db.classes[self.classname]
1224         self.properties = self.cl.getprops()
1226     def clear(self):
1227         self.db = self.cl = self.properties = None
1228         TemplateFunctions.clear(self)
1229         
1230     def render(self, nodeid):
1231         self.nodeid = nodeid
1233         if (self.properties.has_key('type') and
1234                 self.properties.has_key('content')):
1235             pass
1236             # XXX we really want to return this as a downloadable...
1237             #  currently I handle this at a higher level by detecting 'file'
1238             #  designators...
1240         w = self.client.write
1241         w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1242             self.classname, nodeid))
1243         s = open(os.path.join(self.templates, self.classname+'.item')).read()
1244         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1245         w(replace.go(s))
1246         w('</form>')
1247         
1248         self.clear()
1251 class NewItemTemplate(TemplateFunctions):
1252     '''Templating functionality specifically for NEW item (node) display
1253     '''
1254     def __init__(self, client, templates, classname):
1255         TemplateFunctions.__init__(self)
1256         self.client = client
1257         self.instance = client.instance
1258         self.templates = templates
1259         self.classname = classname
1261         # derived
1262         self.db = self.client.db
1263         self.cl = self.db.classes[self.classname]
1264         self.properties = self.cl.getprops()
1266     def clear(self):
1267         self.db = self.cl = None
1268         TemplateFunctions.clear(self)
1269         
1270     def render(self, form):
1271         self.form = form
1272         w = self.client.write
1273         c = self.classname
1274         try:
1275             s = open(os.path.join(self.templates, c+'.newitem')).read()
1276         except IOError:
1277             s = open(os.path.join(self.templates, c+'.item')).read()
1278         w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1279         for key in form.keys():
1280             if key[0] == ':':
1281                 value = form[key].value
1282                 if type(value) != type([]): value = [value]
1283                 for value in value:
1284                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
1285         replace = ItemTemplateReplace(self.globals, locals(), None, None)
1286         w(replace.go(s))
1287         w('</form>')
1288         
1289         self.clear()
1292 # $Log: not supported by cvs2svn $
1293 # Revision 1.97  2002/07/09 05:20:09  richard
1294 #  . added email display function - mangles email addrs so they're not so easily
1295 #    scraped from the web
1297 # Revision 1.96  2002/07/09 04:19:09  richard
1298 # Added reindex command to roundup-admin.
1299 # Fixed reindex on first access.
1300 # Also fixed reindexing of entries that change.
1302 # Revision 1.95  2002/07/08 15:32:06  gmcm
1303 # Pagination of index pages.
1304 # New search form.
1306 # Revision 1.94  2002/06/27 15:38:53  gmcm
1307 # Fix the cycles (a clear method, called after render, that removes
1308 # the bound methods from the globals dict).
1309 # Use cl.filter instead of cl.list followed by sortfunc. For some
1310 # backends (Metakit), filter can sort at C speeds, cutting >10 secs
1311 # off of filling in the <select...> box for assigned_to when you
1312 # have 600+ users.
1314 # Revision 1.93  2002/06/27 12:05:25  gmcm
1315 # Default labelprops to id.
1316 # In history, make sure there's a .item before making a link / multilink into an href.
1317 # Also in history, cgi.escape String properties.
1318 # Clean up some of the reference cycles.
1320 # Revision 1.92  2002/06/11 04:57:04  richard
1321 # Added optional additional property to display in a Multilink form menu.
1323 # Revision 1.91  2002/05/31 00:08:02  richard
1324 # can now just display a link/multilink id - useful for stylesheet stuff
1326 # Revision 1.90  2002/05/25 07:16:24  rochecompaan
1327 # Merged search_indexing-branch with HEAD
1329 # Revision 1.89  2002/05/15 06:34:47  richard
1330 # forgot to fix the templating for last change
1332 # Revision 1.88  2002/04/24 08:34:35  rochecompaan
1333 # Sorting was applied to all nodes of the MultiLink class instead of
1334 # the nodes that are actually linked to in the "field" template
1335 # function.  This adds about 20+ seconds in the display of an issue if
1336 # your database has a 1000 or more issue in it.
1338 # Revision 1.87  2002/04/03 06:12:46  richard
1339 # Fix for date properties as labels.
1341 # Revision 1.86  2002/04/03 05:54:31  richard
1342 # Fixed serialisation problem by moving the serialisation step out of the
1343 # hyperdb.Class (get, set) into the hyperdb.Database.
1345 # Also fixed htmltemplate after the showid changes I made yesterday.
1347 # Unit tests for all of the above written.
1349 # Revision 1.85  2002/04/02 01:40:58  richard
1350 #  . link() htmltemplate function now has a "showid" option for links and
1351 #    multilinks. When true, it only displays the linked node id as the anchor
1352 #    text. The link value is displayed as a tooltip using the title anchor
1353 #    attribute.
1355 # Revision 1.84.2.2  2002/04/20 13:23:32  rochecompaan
1356 # We now have a separate search page for nodes.  Search links for
1357 # different classes can be customized in instance_config similar to
1358 # index links.
1360 # Revision 1.84.2.1  2002/04/19 19:54:42  rochecompaan
1361 # cgi_client.py
1362 #     removed search link for the time being
1363 #     moved rendering of matches to htmltemplate
1364 # hyperdb.py
1365 #     filtering of nodes on full text search incorporated in filter method
1366 # roundupdb.py
1367 #     added paramater to call of filter method
1368 # roundup_indexer.py
1369 #     added search method to RoundupIndexer class
1371 # Revision 1.84  2002/03/29 19:41:48  rochecompaan
1372 #  . Fixed display of mutlilink properties when using the template
1373 #    functions, menu and plain.
1375 # Revision 1.83  2002/02/27 04:14:31  richard
1376 # Ran it through pychecker, made fixes
1378 # Revision 1.82  2002/02/21 23:11:45  richard
1379 #  . fixed some problems in date calculations (calendar.py doesn't handle over-
1380 #    and under-flow). Also, hour/minute/second intervals may now be more than
1381 #    99 each.
1383 # Revision 1.81  2002/02/21 07:21:38  richard
1384 # docco
1386 # Revision 1.80  2002/02/21 07:19:08  richard
1387 # ... and label, width and height control for extra flavour!
1389 # Revision 1.79  2002/02/21 06:57:38  richard
1390 #  . Added popup help for classes using the classhelp html template function.
1391 #    - add <display call="classhelp('priority', 'id,name,description')">
1392 #      to an item page, and it generates a link to a popup window which displays
1393 #      the id, name and description for the priority class. The description
1394 #      field won't exist in most installations, but it will be added to the
1395 #      default templates.
1397 # Revision 1.78  2002/02/21 06:23:00  richard
1398 # *** empty log message ***
1400 # Revision 1.77  2002/02/20 05:05:29  richard
1401 #  . Added simple editing for classes that don't define a templated interface.
1402 #    - access using the admin "class list" interface
1403 #    - limited to admin-only
1404 #    - requires the csv module from object-craft (url given if it's missing)
1406 # Revision 1.76  2002/02/16 09:10:52  richard
1407 # oops
1409 # Revision 1.75  2002/02/16 08:43:23  richard
1410 #  . #517906 ] Attribute order in "View customisation"
1412 # Revision 1.74  2002/02/16 08:39:42  richard
1413 #  . #516854 ] "My Issues" and redisplay
1415 # Revision 1.73  2002/02/15 07:08:44  richard
1416 #  . Alternate email addresses are now available for users. See the MIGRATION
1417 #    file for info on how to activate the feature.
1419 # Revision 1.72  2002/02/14 23:39:18  richard
1420 # . All forms now have "double-submit" protection when Javascript is enabled
1421 #   on the client-side.
1423 # Revision 1.71  2002/01/23 06:15:24  richard
1424 # real (non-string, duh) sorting of lists by node id
1426 # Revision 1.70  2002/01/23 05:47:57  richard
1427 # more HTML template cleanup and unit tests
1429 # Revision 1.69  2002/01/23 05:10:27  richard
1430 # More HTML template cleanup and unit tests.
1431 #  - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1432 #    templates, but link(is_download=1) will still work for existing templates]
1434 # Revision 1.68  2002/01/22 22:55:28  richard
1435 #  . htmltemplate list() wasn't sorting...
1437 # Revision 1.67  2002/01/22 22:46:22  richard
1438 # more htmltemplate cleanups and unit tests
1440 # Revision 1.66  2002/01/22 06:35:40  richard
1441 # more htmltemplate tests and cleanup
1443 # Revision 1.65  2002/01/22 00:12:06  richard
1444 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1445 # off the implementation of some of the functions so they behave sanely.
1447 # Revision 1.64  2002/01/21 03:25:59  richard
1448 # oops
1450 # Revision 1.63  2002/01/21 02:59:10  richard
1451 # Fixed up the HTML display of history so valid links are actually displayed.
1452 # Oh for some unit tests! :(
1454 # Revision 1.62  2002/01/18 08:36:12  grubert
1455 #  . add nowrap to history table date cell i.e. <td nowrap ...
1457 # Revision 1.61  2002/01/17 23:04:53  richard
1458 #  . much nicer history display (actualy real handling of property types etc)
1460 # Revision 1.60  2002/01/17 08:48:19  grubert
1461 #  . display superseder as html link in history.
1463 # Revision 1.59  2002/01/17 07:58:24  grubert
1464 #  . display links a html link in history.
1466 # Revision 1.58  2002/01/15 00:50:03  richard
1467 # #502949 ] index view for non-issues and redisplay
1469 # Revision 1.57  2002/01/14 23:31:21  richard
1470 # reverted the change that had plain() hyperlinking the link displays -
1471 # that's what link() is for!
1473 # Revision 1.56  2002/01/14 07:04:36  richard
1474 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1475 #    the linked node's page.
1476 #    ... this allows a display very similar to bugzilla's where you can actually
1477 #    find out information about the linked node.
1479 # Revision 1.55  2002/01/14 06:45:03  richard
1480 #  . #502953 ] nosy-like treatment of other multilinks
1481 #    ... had to revert most of the previous change to the multilink field
1482 #    display... not good.
1484 # Revision 1.54  2002/01/14 05:16:51  richard
1485 # The submit buttons need a name attribute or mozilla won't submit without a
1486 # file upload. Yeah, that's bloody obscure. Grr.
1488 # Revision 1.53  2002/01/14 04:03:32  richard
1489 # How about that ... date fields have never worked ...
1491 # Revision 1.52  2002/01/14 02:20:14  richard
1492 #  . changed all config accesses so they access either the instance or the
1493 #    config attriubute on the db. This means that all config is obtained from
1494 #    instance_config instead of the mish-mash of classes. This will make
1495 #    switching to a ConfigParser setup easier too, I hope.
1497 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1498 # 0.5.0 switch, I hope!)
1500 # Revision 1.51  2002/01/10 10:02:15  grubert
1501 # In do_history: replace "." in date by " " so html wraps more sensible.
1502 # Should this be done in date's string converter ?
1504 # Revision 1.50  2002/01/05 02:35:10  richard
1505 # I18N'ification
1507 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1508 # Features added:
1509 #  .  Multilink properties are now displayed as comma separated values in
1510 #     a textbox
1511 #  .  The add user link is now only visible to the admin user
1512 #  .  Modified the mail gateway to reject submissions from unknown
1513 #     addresses if ANONYMOUS_ACCESS is denied
1515 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1516 # Bugs fixed:
1517 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1518 #     lost somewhere
1519 #   . Internet Explorer submits full path for filename - we now strip away
1520 #     the path
1521 # Features added:
1522 #   . Link and multilink properties are now displayed sorted in the cgi
1523 #     interface
1525 # Revision 1.47  2001/11/26 22:55:56  richard
1526 # Feature:
1527 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1528 #    the instance.
1529 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1530 #    signature info in e-mails.
1531 #  . Some more flexibility in the mail gateway and more error handling.
1532 #  . Login now takes you to the page you back to the were denied access to.
1534 # Fixed:
1535 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1537 # Revision 1.46  2001/11/24 00:53:12  jhermann
1538 # "except:" is bad, bad , bad!
1540 # Revision 1.45  2001/11/22 15:46:42  jhermann
1541 # Added module docstrings to all modules.
1543 # Revision 1.44  2001/11/21 23:35:45  jhermann
1544 # Added globbing for win32, and sample marking in a 2nd file to test it
1546 # Revision 1.43  2001/11/21 04:04:43  richard
1547 # *sigh* more missing value handling
1549 # Revision 1.42  2001/11/21 03:40:54  richard
1550 # more new property handling
1552 # Revision 1.41  2001/11/15 10:26:01  richard
1553 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1555 # Revision 1.40  2001/11/03 01:56:51  richard
1556 # More HTML compliance fixes. This will probably fix the Netscape problem
1557 # too.
1559 # Revision 1.39  2001/11/03 01:43:47  richard
1560 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1562 # Revision 1.38  2001/10/31 06:58:51  richard
1563 # Added the wrap="hard" attribute to the textarea of the note field so the
1564 # messages wrap sanely.
1566 # Revision 1.37  2001/10/31 06:24:35  richard
1567 # Added do_stext to htmltemplate, thanks Brad Clements.
1569 # Revision 1.36  2001/10/28 22:51:38  richard
1570 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1572 # Revision 1.35  2001/10/24 00:04:41  richard
1573 # Removed the "infinite authentication loop", thanks Roch'e
1575 # Revision 1.34  2001/10/23 22:56:36  richard
1576 # Bugfix in filter "widget" placement, thanks Roch'e
1578 # Revision 1.33  2001/10/23 01:00:18  richard
1579 # Re-enabled login and registration access after lopping them off via
1580 # disabling access for anonymous users.
1581 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1582 # a couple of bugs while I was there. Probably introduced a couple, but
1583 # things seem to work OK at the moment.
1585 # Revision 1.32  2001/10/22 03:25:01  richard
1586 # Added configuration for:
1587 #  . anonymous user access and registration (deny/allow)
1588 #  . filter "widget" location on index page (top, bottom, both)
1589 # Updated some documentation.
1591 # Revision 1.31  2001/10/21 07:26:35  richard
1592 # feature #473127: Filenames. I modified the file.index and htmltemplate
1593 #  source so that the filename is used in the link and the creation
1594 #  information is displayed.
1596 # Revision 1.30  2001/10/21 04:44:50  richard
1597 # bug #473124: UI inconsistency with Link fields.
1598 #    This also prompted me to fix a fairly long-standing usability issue -
1599 #    that of being able to turn off certain filters.
1601 # Revision 1.29  2001/10/21 00:17:56  richard
1602 # CGI interface view customisation section may now be hidden (patch from
1603 #  Roch'e Compaan.)
1605 # Revision 1.28  2001/10/21 00:00:16  richard
1606 # Fixed Checklist function - wasn't always working on a list.
1608 # Revision 1.27  2001/10/20 12:13:44  richard
1609 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1611 # Revision 1.26  2001/10/14 10:55:00  richard
1612 # Handle empty strings in HTML template Link function
1614 # Revision 1.25  2001/10/09 07:25:59  richard
1615 # Added the Password property type. See "pydoc roundup.password" for
1616 # implementation details. Have updated some of the documentation too.
1618 # Revision 1.24  2001/09/27 06:45:58  richard
1619 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1620 # on the plain() template function to escape the text for HTML.
1622 # Revision 1.23  2001/09/10 09:47:18  richard
1623 # Fixed bug in the generation of links to Link/Multilink in indexes.
1624 #   (thanks Hubert Hoegl)
1625 # Added AssignedTo to the "classic" schema's item page.
1627 # Revision 1.22  2001/08/30 06:01:17  richard
1628 # Fixed missing import in mailgw :(
1630 # Revision 1.21  2001/08/16 07:34:59  richard
1631 # better CGI text searching - but hidden filter fields are disappearing...
1633 # Revision 1.20  2001/08/15 23:43:18  richard
1634 # Fixed some isFooTypes that I missed.
1635 # Refactored some code in the CGI code.
1637 # Revision 1.19  2001/08/12 06:32:36  richard
1638 # using isinstance(blah, Foo) now instead of isFooType
1640 # Revision 1.18  2001/08/07 00:24:42  richard
1641 # stupid typo
1643 # Revision 1.17  2001/08/07 00:15:51  richard
1644 # Added the copyright/license notice to (nearly) all files at request of
1645 # Bizar Software.
1647 # Revision 1.16  2001/08/01 03:52:23  richard
1648 # Checklist was using wrong name.
1650 # Revision 1.15  2001/07/30 08:12:17  richard
1651 # Added time logging and file uploading to the templates.
1653 # Revision 1.14  2001/07/30 06:17:45  richard
1654 # Features:
1655 #  . Added ability for cgi newblah forms to indicate that the new node
1656 #    should be linked somewhere.
1657 # Fixed:
1658 #  . Fixed the agument handling for the roundup-admin find command.
1659 #  . Fixed handling of summary when no note supplied for newblah. Again.
1660 #  . Fixed detection of no form in htmltemplate Field display.
1662 # Revision 1.13  2001/07/30 02:37:53  richard
1663 # Temporary measure until we have decent schema migration.
1665 # Revision 1.12  2001/07/30 01:24:33  richard
1666 # Handles new node display now.
1668 # Revision 1.11  2001/07/29 09:31:35  richard
1669 # oops
1671 # Revision 1.10  2001/07/29 09:28:23  richard
1672 # Fixed sorting by clicking on column headings.
1674 # Revision 1.9  2001/07/29 08:27:40  richard
1675 # Fixed handling of passed-in values in form elements (ie. during a
1676 # drill-down)
1678 # Revision 1.8  2001/07/29 07:01:39  richard
1679 # Added vim command to all source so that we don't get no steenkin' tabs :)
1681 # Revision 1.7  2001/07/29 05:36:14  richard
1682 # Cleanup of the link label generation.
1684 # Revision 1.6  2001/07/29 04:06:42  richard
1685 # Fixed problem in link display when Link value is None.
1687 # Revision 1.5  2001/07/28 08:17:09  richard
1688 # fixed use of stylesheet
1690 # Revision 1.4  2001/07/28 07:59:53  richard
1691 # Replaced errno integers with their module values.
1692 # De-tabbed templatebuilder.py
1694 # Revision 1.3  2001/07/25 03:39:47  richard
1695 # Hrm - displaying links to classes that don't specify a key property. I've
1696 # got it defaulting to 'name', then 'title' and then a "random" property (first
1697 # one returned by getprops().keys().
1698 # Needs to be moved onto the Class I think...
1700 # Revision 1.2  2001/07/22 12:09:32  richard
1701 # Final commit of Grande Splite
1703 # Revision 1.1  2001/07/22 11:58:35  richard
1704 # More Grande Splite
1707 # vim: set filetype=python ts=4 sw=4 et si