Code

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