Code

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