Code

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