Code

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