Code

Wrote more unit tests for htmltemplate, and while I was at it, I polished
[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.65 2002-01-22 00:12:06 richard Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno
26 import hyperdb, date, password
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 TemplateFunctions:
37     def __init__(self):
38         self.form = None
39         self.nodeid = None
40         self.filterspec = None
41         self.globals = {}
42         for key in TemplateFunctions.__dict__.keys():
43             if key[:3] == 'do_':
44                 self.globals[key[3:]] = getattr(self, key)
46     def do_plain(self, property, escape=0):
47         ''' display a String property directly;
49             display a Date property in a specified time zone with an option to
50             omit the time from the date stamp;
52             for a Link or Multilink property, display the key strings of the
53             linked nodes (or the ids if the linked class has no key property)
54         '''
55         if not self.nodeid and self.form is None:
56             return _('[Field: not called from item]')
57         propclass = self.properties[property]
58         if self.nodeid:
59             # make sure the property is a valid one
60             # TODO: this tests, but we should handle the exception
61             prop_test = self.cl.getprops()[property]
63             # get the value for this property
64             try:
65                 value = self.cl.get(self.nodeid, property)
66             except KeyError:
67                 # a KeyError here means that the node doesn't have a value
68                 # for the specified property
69                 if isinstance(propclass, hyperdb.Multilink): value = []
70                 else: value = ''
71         else:
72             # TODO: pull the value from the form
73             if isinstance(propclass, hyperdb.Multilink): value = []
74             else: value = ''
75         if isinstance(propclass, hyperdb.String):
76             if value is None: value = ''
77             else: value = str(value)
78         elif isinstance(propclass, hyperdb.Password):
79             if value is None: value = ''
80             else: value = _('*encrypted*')
81         elif isinstance(propclass, hyperdb.Date):
82             # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
83             value = str(value)
84         elif isinstance(propclass, hyperdb.Interval):
85             value = str(value)
86         elif isinstance(propclass, hyperdb.Link):
87             linkcl = self.db.classes[propclass.classname]
88             k = linkcl.labelprop()
89             if value:
90                 value = linkcl.get(value, k)
91             else:
92                 value = _('[unselected]')
93         elif isinstance(propclass, hyperdb.Multilink):
94             linkcl = self.db.classes[propclass.classname]
95             k = linkcl.labelprop()
96             value = ', '.join(value)
97         else:
98             s = _('Plain: bad propclass "%(propclass)s"')%locals()
99         if escape:
100             value = cgi.escape(value)
101         return value
103     def do_stext(self, property, escape=0):
104         '''Render as structured text using the StructuredText module
105            (see above for details)
106         '''
107         s = self.do_plain(property, escape=escape)
108         if not StructuredText:
109             return s
110         return StructuredText(s,level=1,header=0)
112     def determine_value(self, property):
113         '''determine the value of a property using the node, form or
114            filterspec
115         '''
116         propclass = self.properties[property]
117         if self.nodeid:
118             value = self.cl.get(self.nodeid, property, None)
119             if isinstance(propclass, hyperdb.Multilink) and value is None:
120                 return []
121             return value
122         elif self.filterspec is not None:
123             if isinstance(propclass, hyperdb.Multilink):
124                 return self.filterspec.get(property, [])
125             else:
126                 return self.filterspec.get(property, '')
127         # TODO: pull the value from the form
128         if isinstance(propclass, hyperdb.Multilink):
129             return []
130         else:
131             return ''
133     def make_sort_function(self, classname):
134         '''Make a sort function for a given class
135         '''
136         linkcl = self.db.classes[classname]
137         if linkcl.getprops().has_key('order'):
138             sort_on = 'order'
139         else:
140             sort_on = linkcl.labelprop()
141         def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
142             return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
143         return sortfunc
145     def do_field(self, property, size=None, showid=0):
146         ''' display a property like the plain displayer, but in a text field
147             to be edited
149             Note: if you would prefer an option list style display for
150             link or multilink editing, use menu().
151         '''
152         if not self.nodeid and self.form is None and self.filterspec is None:
153             return _('[Field: not called from item]')
155         if size is None:
156             size = 30
158         propclass = self.properties[property]
160         # get the value
161         value = self.determine_value(property)
163         # now display
164         if (isinstance(propclass, hyperdb.String) or
165                 isinstance(propclass, hyperdb.Date) or
166                 isinstance(propclass, hyperdb.Interval)):
167             if value is None:
168                 value = ''
169             else:
170                 value = cgi.escape(str(value))
171                 value = '"'.join(value.split('"'))
172             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
173         elif isinstance(propclass, hyperdb.Password):
174             s = '<input type="password" name="%s" size="%s">'%(property, size)
175         elif isinstance(propclass, hyperdb.Link):
176             sortfunc = self.make_sort_function(propclass.classname)
177             linkcl = self.db.classes[propclass.classname]
178             options = linkcl.list()
179             options.sort(sortfunc)
180             # TODO: make this a field display, not a menu one!
181             l = ['<select name="%s">'%property]
182             k = linkcl.labelprop()
183             if value is None:
184                 s = 'selected '
185             else:
186                 s = ''
187             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
188             for optionid in options:
189                 option = linkcl.get(optionid, k)
190                 s = ''
191                 if optionid == value:
192                     s = 'selected '
193                 if showid:
194                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
195                 else:
196                     lab = option
197                 if size is not None and len(lab) > size:
198                     lab = lab[:size-3] + '...'
199                 lab = cgi.escape(lab)
200                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
201             l.append('</select>')
202             s = '\n'.join(l)
203         elif isinstance(propclass, hyperdb.Multilink):
204             sortfunc = self.make_sort_function(propclass.classname)
205             linkcl = self.db.classes[propclass.classname]
206             list = linkcl.list()
207             list.sort(sortfunc)
208             l = []
209             # map the id to the label property
210             if not showid:
211                 k = linkcl.labelprop()
212                 value = [linkcl.get(v, k) for v in value]
213             value = cgi.escape(','.join(value))
214             s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
215         else:
216             s = _('Plain: bad propclass "%(propclass)s"')%locals()
217         return s
219     def do_menu(self, property, size=None, height=None, showid=0):
220         ''' for a Link property, display a menu of the available choices
221         '''
222         if not self.nodeid and self.form is None and self.filterspec is None:
223             return _('[Field: not called from item]')
225         propclass = self.properties[property]
227         # make sure this is a link property
228         if not (isinstance(propclass, hyperdb.Link) or
229                 isinstance(propclass, hyperdb.Multilink)):
230             return _('[Menu: not a link]')
232         # sort function
233         sortfunc = self.make_sort_function(propclass.classname)
235         # get the value
236         value = self.determine_value(property)
238         # display
239         if isinstance(propclass, hyperdb.Link):
240             linkcl = self.db.classes[propclass.classname]
241             l = ['<select name="%s">'%property]
242             k = linkcl.labelprop()
243             s = ''
244             if value is None:
245                 s = 'selected '
246             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
247             options = linkcl.list()
248             options.sort(sortfunc)
249             for optionid in options:
250                 option = linkcl.get(optionid, k)
251                 s = ''
252                 if optionid == value:
253                     s = 'selected '
254                 if showid:
255                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
256                 else:
257                     lab = option
258                 if size is not None and len(lab) > size:
259                     lab = lab[:size-3] + '...'
260                 lab = cgi.escape(lab)
261                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
262             l.append('</select>')
263             return '\n'.join(l)
264         if isinstance(propclass, hyperdb.Multilink):
265             linkcl = self.db.classes[propclass.classname]
266             options = linkcl.list()
267             options.sort(sortfunc)
268             height = height or min(len(options), 7)
269             l = ['<select multiple name="%s" size="%s">'%(property, height)]
270             k = linkcl.labelprop()
271             for optionid in options:
272                 option = linkcl.get(optionid, k)
273                 s = ''
274                 if optionid in value:
275                     s = 'selected '
276                 if showid:
277                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
278                 else:
279                     lab = option
280                 if size is not None and len(lab) > size:
281                     lab = lab[:size-3] + '...'
282                 lab = cgi.escape(lab)
283                 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
284                     lab))
285             l.append('</select>')
286             return '\n'.join(l)
287         return _('[Menu: not a link]')
289     #XXX deviates from spec
290     def do_link(self, property=None, is_download=0):
291         '''For a Link or Multilink property, display the names of the linked
292            nodes, hyperlinked to the item views on those nodes.
293            For other properties, link to this node with the property as the
294            text.
296            If is_download is true, append the property value to the generated
297            URL so that the link may be used as a download link and the
298            downloaded file name is correct.
299         '''
300         if not self.nodeid and self.form is None:
301             return _('[Link: not called from item]')
302         propclass = self.properties[property]
303         if self.nodeid:
304             value = self.cl.get(self.nodeid, property)
305         else:
306             if isinstance(propclass, hyperdb.Multilink): value = []
307             elif isinstance(propclass, hyperdb.Link): value = None
308             else: value = ''
309         if isinstance(propclass, hyperdb.Link):
310             linkname = propclass.classname
311             if value is None: return '[no %s]'%property.capitalize()
312             linkcl = self.db.classes[linkname]
313             k = linkcl.labelprop()
314             linkvalue = linkcl.get(value, k)
315             if is_download:
316                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
317                     linkvalue, linkvalue)
318             else:
319                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
320         if isinstance(propclass, hyperdb.Multilink):
321             linkname = propclass.classname
322             linkcl = self.db.classes[linkname]
323             k = linkcl.labelprop()
324             if not value:
325                 return _('[no %(propname)s]')%{'propname': property.capitalize()}
326             l = []
327             for value in value:
328                 linkvalue = linkcl.get(value, k)
329                 if is_download:
330                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
331                         linkvalue, linkvalue))
332                 else:
333                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
334                         linkvalue))
335             return ', '.join(l)
336         if isinstance(propclass, hyperdb.String) and value == '':
337             return _('[no %(propname)s]')%{'propname': property.capitalize()}
338         if is_download:
339             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
340                 value, value)
341         else:
342             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
344     def do_count(self, property, **args):
345         ''' for a Multilink property, display a count of the number of links in
346             the list
347         '''
348         if not self.nodeid:
349             return _('[Count: not called from item]')
350         propclass = self.properties[property]
351         value = self.cl.get(self.nodeid, property)
352         if isinstance(propclass, hyperdb.Multilink):
353             return str(len(value))
354         return _('[Count: not a Multilink]')
356     # XXX pretty is definitely new ;)
357     def do_reldate(self, property, pretty=0):
358         ''' display a Date property in terms of an interval relative to the
359             current date (e.g. "+ 3w", "- 2d").
361             with the 'pretty' flag, make it pretty
362         '''
363         if not self.nodeid and self.form is None:
364             return _('[Reldate: not called from item]')
365         propclass = self.properties[property]
366         if isinstance(not propclass, hyperdb.Date):
367             return _('[Reldate: not a Date]')
368         if self.nodeid:
369             value = self.cl.get(self.nodeid, property)
370         else:
371             value = date.Date('.')
372         interval = value - date.Date('.')
373         if pretty:
374             if not self.nodeid:
375                 return _('now')
376             pretty = interval.pretty()
377             if pretty is None:
378                 pretty = value.pretty()
379             return pretty
380         return str(interval)
382     def do_download(self, property, **args):
383         ''' show a Link("file") or Multilink("file") property using links that
384             allow you to download files
385         '''
386         if not self.nodeid:
387             return _('[Download: not called from item]')
388         propclass = self.properties[property]
389         value = self.cl.get(self.nodeid, property)
390         if isinstance(propclass, hyperdb.Link):
391             linkcl = self.db.classes[propclass.classname]
392             linkvalue = linkcl.get(value, k)
393             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
394         if isinstance(propclass, hyperdb.Multilink):
395             linkcl = self.db.classes[propclass.classname]
396             l = []
397             for value in value:
398                 linkvalue = linkcl.get(value, k)
399                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
400             return ', '.join(l)
401         return _('[Download: not a link]')
404     def do_checklist(self, property, **args):
405         ''' for a Link or Multilink property, display checkboxes for the
406             available choices to permit filtering
407         '''
408         propclass = self.properties[property]
409         if (not isinstance(propclass, hyperdb.Link) and not
410                 isinstance(propclass, hyperdb.Multilink)):
411             return _('[Checklist: not a link]')
413         # get our current checkbox state
414         if self.nodeid:
415             # get the info from the node - make sure it's a list
416             if isinstance(propclass, hyperdb.Link):
417                 value = [self.cl.get(self.nodeid, property)]
418             else:
419                 value = self.cl.get(self.nodeid, property)
420         elif self.filterspec is not None:
421             # get the state from the filter specification (always a list)
422             value = self.filterspec.get(property, [])
423         else:
424             # it's a new node, so there's no state
425             value = []
427         # so we can map to the linked node's "lable" property
428         linkcl = self.db.classes[propclass.classname]
429         l = []
430         k = linkcl.labelprop()
431         for optionid in linkcl.list():
432             option = linkcl.get(optionid, k)
433             if optionid in value or option in value:
434                 checked = 'checked'
435             else:
436                 checked = ''
437             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
438                 option, checked, property, option))
440         # for Links, allow the "unselected" option too
441         if isinstance(propclass, hyperdb.Link):
442             if value is None or '-1' in value:
443                 checked = 'checked'
444             else:
445                 checked = ''
446             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
447                 'value="-1">')%(checked, property))
448         return '\n'.join(l)
450     def do_note(self, rows=5, cols=80):
451         ''' display a "note" field, which is a text area for entering a note to
452             go along with a change. 
453         '''
454         # TODO: pull the value from the form
455         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
456             '</textarea>'%(rows, cols)
458     # XXX new function
459     def do_list(self, property, reverse=0):
460         ''' list the items specified by property using the standard index for
461             the class
462         '''
463         propcl = self.properties[property]
464         if not isinstance(propcl, hyperdb.Multilink):
465             return _('[List: not a Multilink]')
466         value = self.cl.get(self.nodeid, property)
467         if reverse:
468             value.reverse()
470         # render the sub-index into a string
471         fp = StringIO.StringIO()
472         try:
473             write_save = self.client.write
474             self.client.write = fp.write
475             index = IndexTemplate(self.client, self.templates, propcl.classname)
476             index.render(nodeids=value, show_display_form=0)
477         finally:
478             self.client.write = write_save
480         return fp.getvalue()
482     # XXX new function
483     def do_history(self, direction='descending'):
484         ''' list the history of the item
486             If "direction" is 'descending' then the most recent event will
487             be displayed first. If it is 'ascending' then the oldest event
488             will be displayed first.
489         '''
490         if self.nodeid is None:
491             return _("[History: node doesn't exist]")
493         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
494             '<tr class="list-header">',
495             _('<th align=left><span class="list-item">Date</span></th>'),
496             _('<th align=left><span class="list-item">User</span></th>'),
497             _('<th align=left><span class="list-item">Action</span></th>'),
498             _('<th align=left><span class="list-item">Args</span></th>'),
499             '</tr>']
501         comments = {}
502         history = self.cl.history(self.nodeid)
503         if direction == 'descending':
504             history.reverse()
505         for id, evt_date, user, action, args in history:
506             date_s = str(evt_date).replace("."," ")
507             arg_s = ''
508             if action == 'link' and type(args) == type(()):
509                 if len(args) == 3:
510                     linkcl, linkid, key = args
511                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
512                         linkcl, linkid, key)
513                 else:
514                     arg_s = str(arg)
516             elif action == 'unlink' and type(args) == type(()):
517                 if len(args) == 3:
518                     linkcl, linkid, key = args
519                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
520                         linkcl, linkid, key)
521                 else:
522                     arg_s = str(arg)
524             elif type(args) == type({}):
525                 cell = []
526                 for k in args.keys():
527                     # try to get the relevant property and treat it
528                     # specially
529                     try:
530                         prop = self.properties[k]
531                     except:
532                         prop = None
533                     if prop is not None:
534                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
535                                 isinstance(prop, hyperdb.Link)):
536                             # figure what the link class is
537                             classname = prop.classname
538                             try:
539                                 linkcl = self.db.classes[classname]
540                             except KeyError, message:
541                                 labelprop = None
542                                 comments[classname] = _('''The linked class
543                                     %(classname)s no longer exists''')%locals()
544                             labelprop = linkcl.labelprop()
546                         if isinstance(prop, hyperdb.Multilink) and \
547                                 len(args[k]) > 0:
548                             ml = []
549                             for linkid in args[k]:
550                                 label = classname + linkid
551                                 # if we have a label property, try to use it
552                                 # TODO: test for node existence even when
553                                 # there's no labelprop!
554                                 try:
555                                     if labelprop is not None:
556                                         label = linkcl.get(linkid, labelprop)
557                                 except IndexError:
558                                     comments['no_link'] = _('''<strike>The
559                                         linked node no longer
560                                         exists</strike>''')
561                                     ml.append('<strike>%s</strike>'%label)
562                                 else:
563                                     ml.append('<a href="%s%s">%s</a>'%(
564                                         classname, linkid, label))
565                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
566                         elif isinstance(prop, hyperdb.Link) and args[k]:
567                             label = classname + args[k]
568                             # if we have a label property, try to use it
569                             # TODO: test for node existence even when
570                             # there's no labelprop!
571                             if labelprop is not None:
572                                 try:
573                                     label = linkcl.get(args[k], labelprop)
574                                 except IndexError:
575                                     comments['no_link'] = _('''<strike>The
576                                         linked node no longer
577                                         exists</strike>''')
578                                     cell.append(' <strike>%s</strike>,\n'%label)
579                                     # "flag" this is done .... euwww
580                                     label = None
581                             if label is not None:
582                                 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
583                                     classname, args[k], label))
585                         elif isinstance(prop, hyperdb.Date) and args[k]:
586                             d = date.Date(args[k])
587                             cell.append('%s: %s'%(k, str(d)))
589                         elif isinstance(prop, hyperdb.Interval) and args[k]:
590                             d = date.Interval(args[k])
591                             cell.append('%s: %s'%(k, str(d)))
593                         elif not args[k]:
594                             cell.append('%s: (no value)\n'%k)
596                         else:
597                             cell.append('%s: %s\n'%(k, str(args[k])))
598                     else:
599                         # property no longer exists
600                         comments['no_exist'] = _('''<em>The indicated property
601                             no longer exists</em>''')
602                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
603                 arg_s = '<br />'.join(cell)
604             else:
605                 # unkown event!!
606                 comments['unknown'] = _('''<strong><em>This event is not
607                     handled by the history display!</em></strong>''')
608                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
609             date_s = date_s.replace(' ', '&nbsp;')
610             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
611                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
612                 user, action, arg_s))
613         if comments:
614             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
615         for entry in comments.values():
616             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
617         l.append('</table>')
618         return '\n'.join(l)
620     # XXX new function
621     def do_submit(self):
622         ''' add a submit button for the item
623         '''
624         if self.nodeid:
625             return _('<input type="submit" name="submit" value="Submit Changes">')
626         elif self.form is not None:
627             return _('<input type="submit" name="submit" value="Submit New Entry">')
628         else:
629             return _('[Submit: not called from item]')
633 #   INDEX TEMPLATES
635 class IndexTemplateReplace:
636     def __init__(self, globals, locals, props):
637         self.globals = globals
638         self.locals = locals
639         self.props = props
641     replace=re.compile(
642         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
643         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
644     def go(self, text):
645         return self.replace.sub(self, text)
647     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
648         if m.group('name'):
649             if m.group('name') in self.props:
650                 text = m.group('text')
651                 replace = IndexTemplateReplace(self.globals, {}, self.props)
652                 return replace.go(m.group('text'))
653             else:
654                 return ''
655         if m.group('display'):
656             command = m.group('command')
657             return eval(command, self.globals, self.locals)
658         print '*** unhandled match', m.groupdict()
660 class IndexTemplate(TemplateFunctions):
661     def __init__(self, client, templates, classname):
662         self.client = client
663         self.instance = client.instance
664         self.templates = templates
665         self.classname = classname
667         # derived
668         self.db = self.client.db
669         self.cl = self.db.classes[self.classname]
670         self.properties = self.cl.getprops()
672         TemplateFunctions.__init__(self)
674     col_re=re.compile(r'<property\s+name="([^>]+)">')
675     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
676             show_display_form=1, nodeids=None, show_customization=1):
677         self.filterspec = filterspec
679         w = self.client.write
681         # get the filter template
682         try:
683             filter_template = open(os.path.join(self.templates,
684                 self.classname+'.filter')).read()
685             all_filters = self.col_re.findall(filter_template)
686         except IOError, error:
687             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
688             filter_template = None
689             all_filters = []
691         # XXX deviate from spec here ...
692         # load the index section template and figure the default columns from it
693         template = open(os.path.join(self.templates,
694             self.classname+'.index')).read()
695         all_columns = self.col_re.findall(template)
696         if not columns:
697             columns = []
698             for name in all_columns:
699                 columns.append(name)
700         else:
701             # re-sort columns to be the same order as all_columns
702             l = []
703             for name in all_columns:
704                 if name in columns:
705                     l.append(name)
706             columns = l
708         # display the filter section
709         if (show_display_form and 
710                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
711             w('<form action="%s">\n'%self.classname)
712             self.filter_section(filter_template, filter, columns, group,
713                 all_filters, all_columns, show_customization)
714             # make sure that the sorting doesn't get lost either
715             if sort:
716                 w('<input type="hidden" name=":sort" value="%s">'%
717                     ','.join(sort))
718             w('</form>\n')
721         # now display the index section
722         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
723         w('<tr class="list-header">\n')
724         for name in columns:
725             cname = name.capitalize()
726             if show_display_form:
727                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
728                 anchor = "%s?%s"%(self.classname, sb)
729                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
730                     anchor, cname))
731             else:
732                 w('<td><span class="list-header">%s</span></td>\n'%cname)
733         w('</tr>\n')
735         # this stuff is used for group headings - optimise the group names
736         old_group = None
737         group_names = []
738         if group:
739             for name in group:
740                 if name[0] == '-': group_names.append(name[1:])
741                 else: group_names.append(name)
743         # now actually loop through all the nodes we get from the filter and
744         # apply the template
745         if nodeids is None:
746             nodeids = self.cl.filter(filterspec, sort, group)
747         for nodeid in nodeids:
748             # check for a group heading
749             if group_names:
750                 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
751                 if this_group != old_group:
752                     l = []
753                     for name in group_names:
754                         prop = self.properties[name]
755                         if isinstance(prop, hyperdb.Link):
756                             group_cl = self.db.classes[prop.classname]
757                             key = group_cl.getkey()
758                             value = self.cl.get(nodeid, name)
759                             if value is None:
760                                 l.append(_('[unselected %(classname)s]')%{
761                                     'classname': prop.classname})
762                             else:
763                                 l.append(group_cl.get(self.cl.get(nodeid,
764                                     name), key))
765                         elif isinstance(prop, hyperdb.Multilink):
766                             group_cl = self.db.classes[prop.classname]
767                             key = group_cl.getkey()
768                             for value in self.cl.get(nodeid, name):
769                                 l.append(group_cl.get(value, key))
770                         else:
771                             value = self.cl.get(nodeid, name, _('[no value]'))
772                             if value is None:
773                                 value = _('[empty %(name)s]')%locals()
774                             else:
775                                 value = str(value)
776                             l.append(value)
777                     w('<tr class="section-bar">'
778                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
779                         len(columns), ', '.join(l)))
780                     old_group = this_group
782             # display this node's row
783             replace = IndexTemplateReplace(self.globals, locals(), columns)
784             self.nodeid = nodeid
785             w(replace.go(template))
786             self.nodeid = None
788         w('</table>')
790         # display the filter section
791         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
792                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
793             w('<form action="%s">\n'%self.classname)
794             self.filter_section(filter_template, filter, columns, group,
795                 all_filters, all_columns, show_customization)
796             # make sure that the sorting doesn't get lost either
797             if sort:
798                 w('<input type="hidden" name=":sort" value="%s">'%
799                     ','.join(sort))
800             w('</form>\n')
803     def filter_section(self, template, filter, columns, group, all_filters,
804             all_columns, show_customization):
806         w = self.client.write
808         # wrap the template in a single table to ensure the whole widget
809         # is displayed at once
810         w('<table><tr><td>')
812         if template and filter:
813             # display the filter section
814             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
815             w('<tr class="location-bar">')
816             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
817             w('</tr>')
818             replace = IndexTemplateReplace(self.globals, locals(), filter)
819             w(replace.go(template))
820             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
821             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
822             w('</table>')
824         # now add in the filter/columns/group/etc config table form
825         w('<input type="hidden" name="show_customization" value="%s">' %
826             show_customization )
827         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
828         names = []
829         for name in self.properties.keys():
830             if name in all_filters or name in all_columns:
831                 names.append(name)
832         if show_customization:
833             action = '-'
834         else:
835             action = '+'
836             # hide the values for filters, columns and grouping in the form
837             # if the customization widget is not visible
838             for name in names:
839                 if all_filters and name in filter:
840                     w('<input type="hidden" name=":filter" value="%s">' % name)
841                 if all_columns and name in columns:
842                     w('<input type="hidden" name=":columns" value="%s">' % name)
843                 if all_columns and name in group:
844                     w('<input type="hidden" name=":group" value="%s">' % name)
846         # TODO: The widget style can go into the stylesheet
847         w(_('<th align="left" colspan=%s>'
848           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
849           'customisation...</th></tr>\n')%(len(names)+1, action))
851         if not show_customization:
852             w('</table>\n')
853             return
855         w('<tr class="location-bar"><th>&nbsp;</th>')
856         for name in names:
857             w('<th>%s</th>'%name.capitalize())
858         w('</tr>\n')
860         # Filter
861         if all_filters:
862             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
863             for name in names:
864                 if name not in all_filters:
865                     w('<td>&nbsp;</td>')
866                     continue
867                 if name in filter: checked=' checked'
868                 else: checked=''
869                 w('<td align=middle>\n')
870                 w(' <input type="checkbox" name=":filter" value="%s" '
871                   '%s></td>\n'%(name, checked))
872             w('</tr>\n')
874         # Columns
875         if all_columns:
876             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
877             for name in names:
878                 if name not in all_columns:
879                     w('<td>&nbsp;</td>')
880                     continue
881                 if name in columns: checked=' checked'
882                 else: checked=''
883                 w('<td align=middle>\n')
884                 w(' <input type="checkbox" name=":columns" value="%s"'
885                   '%s></td>\n'%(name, checked))
886             w('</tr>\n')
888             # Grouping
889             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
890             for name in names:
891                 prop = self.properties[name]
892                 if name not in all_columns:
893                     w('<td>&nbsp;</td>')
894                     continue
895                 if name in group: checked=' checked'
896                 else: checked=''
897                 w('<td align=middle>\n')
898                 w(' <input type="checkbox" name=":group" value="%s"'
899                   '%s></td>\n'%(name, checked))
900             w('</tr>\n')
902         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
903         w('<td colspan="%s">'%len(names))
904         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
905         w('</tr>\n')
906         w('</table>\n')
908         # and the outer table
909         w('</td></tr></table>')
912     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
913         l = []
914         w = l.append
915         for k, v in filterspec.items():
916             k = urllib.quote(k)
917             if type(v) == type([]):
918                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
919             else:
920                 w('%s=%s'%(k, urllib.quote(v)))
921         if columns:
922             w(':columns=%s'%','.join(map(urllib.quote, columns)))
923         if filter:
924             w(':filter=%s'%','.join(map(urllib.quote, filter)))
925         if group:
926             w(':group=%s'%','.join(map(urllib.quote, group)))
927         m = []
928         s_dir = ''
929         for name in sort:
930             dir = name[0]
931             if dir == '-':
932                 name = name[1:]
933             else:
934                 dir = ''
935             if sort_name == name:
936                 if dir == '-':
937                     s_dir = ''
938                 else:
939                     s_dir = '-'
940             else:
941                 m.append(dir+urllib.quote(name))
942         m.insert(0, s_dir+urllib.quote(sort_name))
943         # so things don't get completely out of hand, limit the sort to
944         # two columns
945         w(':sort=%s'%','.join(m[:2]))
946         return '&'.join(l)
949 #   ITEM TEMPLATES
951 class ItemTemplateReplace:
952     def __init__(self, globals, locals, cl, nodeid):
953         self.globals = globals
954         self.locals = locals
955         self.cl = cl
956         self.nodeid = nodeid
958     replace=re.compile(
959         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
960         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
961     def go(self, text):
962         return self.replace.sub(self, text)
964     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
965         if m.group('name'):
966             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
967                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
968                     self.nodeid)
969                 return replace.go(m.group('text'))
970             else:
971                 return ''
972         if m.group('display'):
973             command = m.group('command')
974             return eval(command, self.globals, self.locals)
975         print '*** unhandled match', m.groupdict()
978 class ItemTemplate(TemplateFunctions):
979     def __init__(self, client, templates, classname):
980         self.client = client
981         self.instance = client.instance
982         self.templates = templates
983         self.classname = classname
985         # derived
986         self.db = self.client.db
987         self.cl = self.db.classes[self.classname]
988         self.properties = self.cl.getprops()
990         TemplateFunctions.__init__(self)
992     def render(self, nodeid):
993         self.nodeid = nodeid
995         if (self.properties.has_key('type') and
996                 self.properties.has_key('content')):
997             pass
998             # XXX we really want to return this as a downloadable...
999             #  currently I handle this at a higher level by detecting 'file'
1000             #  designators...
1002         w = self.client.write
1003         w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
1004             self.classname, nodeid))
1005         s = open(os.path.join(self.templates, self.classname+'.item')).read()
1006         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1007         w(replace.go(s))
1008         w('</form>')
1011 class NewItemTemplate(TemplateFunctions):
1012     def __init__(self, client, templates, classname):
1013         self.client = client
1014         self.instance = client.instance
1015         self.templates = templates
1016         self.classname = classname
1018         # derived
1019         self.db = self.client.db
1020         self.cl = self.db.classes[self.classname]
1021         self.properties = self.cl.getprops()
1023         TemplateFunctions.__init__(self)
1025     def render(self, form):
1026         self.form = form
1027         w = self.client.write
1028         c = self.classname
1029         try:
1030             s = open(os.path.join(self.templates, c+'.newitem')).read()
1031         except IOError:
1032             s = open(os.path.join(self.templates, c+'.item')).read()
1033         w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
1034         for key in form.keys():
1035             if key[0] == ':':
1036                 value = form[key].value
1037                 if type(value) != type([]): value = [value]
1038                 for value in value:
1039                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
1040         replace = ItemTemplateReplace(self.globals, locals(), None, None)
1041         w(replace.go(s))
1042         w('</form>')
1045 # $Log: not supported by cvs2svn $
1046 # Revision 1.64  2002/01/21 03:25:59  richard
1047 # oops
1049 # Revision 1.63  2002/01/21 02:59:10  richard
1050 # Fixed up the HTML display of history so valid links are actually displayed.
1051 # Oh for some unit tests! :(
1053 # Revision 1.62  2002/01/18 08:36:12  grubert
1054 #  . add nowrap to history table date cell i.e. <td nowrap ...
1056 # Revision 1.61  2002/01/17 23:04:53  richard
1057 #  . much nicer history display (actualy real handling of property types etc)
1059 # Revision 1.60  2002/01/17 08:48:19  grubert
1060 #  . display superseder as html link in history.
1062 # Revision 1.59  2002/01/17 07:58:24  grubert
1063 #  . display links a html link in history.
1065 # Revision 1.58  2002/01/15 00:50:03  richard
1066 # #502949 ] index view for non-issues and redisplay
1068 # Revision 1.57  2002/01/14 23:31:21  richard
1069 # reverted the change that had plain() hyperlinking the link displays -
1070 # that's what link() is for!
1072 # Revision 1.56  2002/01/14 07:04:36  richard
1073 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1074 #    the linked node's page.
1075 #    ... this allows a display very similar to bugzilla's where you can actually
1076 #    find out information about the linked node.
1078 # Revision 1.55  2002/01/14 06:45:03  richard
1079 #  . #502953 ] nosy-like treatment of other multilinks
1080 #    ... had to revert most of the previous change to the multilink field
1081 #    display... not good.
1083 # Revision 1.54  2002/01/14 05:16:51  richard
1084 # The submit buttons need a name attribute or mozilla won't submit without a
1085 # file upload. Yeah, that's bloody obscure. Grr.
1087 # Revision 1.53  2002/01/14 04:03:32  richard
1088 # How about that ... date fields have never worked ...
1090 # Revision 1.52  2002/01/14 02:20:14  richard
1091 #  . changed all config accesses so they access either the instance or the
1092 #    config attriubute on the db. This means that all config is obtained from
1093 #    instance_config instead of the mish-mash of classes. This will make
1094 #    switching to a ConfigParser setup easier too, I hope.
1096 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1097 # 0.5.0 switch, I hope!)
1099 # Revision 1.51  2002/01/10 10:02:15  grubert
1100 # In do_history: replace "." in date by " " so html wraps more sensible.
1101 # Should this be done in date's string converter ?
1103 # Revision 1.50  2002/01/05 02:35:10  richard
1104 # I18N'ification
1106 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1107 # Features added:
1108 #  .  Multilink properties are now displayed as comma separated values in
1109 #     a textbox
1110 #  .  The add user link is now only visible to the admin user
1111 #  .  Modified the mail gateway to reject submissions from unknown
1112 #     addresses if ANONYMOUS_ACCESS is denied
1114 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1115 # Bugs fixed:
1116 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1117 #     lost somewhere
1118 #   . Internet Explorer submits full path for filename - we now strip away
1119 #     the path
1120 # Features added:
1121 #   . Link and multilink properties are now displayed sorted in the cgi
1122 #     interface
1124 # Revision 1.47  2001/11/26 22:55:56  richard
1125 # Feature:
1126 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1127 #    the instance.
1128 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1129 #    signature info in e-mails.
1130 #  . Some more flexibility in the mail gateway and more error handling.
1131 #  . Login now takes you to the page you back to the were denied access to.
1133 # Fixed:
1134 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1136 # Revision 1.46  2001/11/24 00:53:12  jhermann
1137 # "except:" is bad, bad , bad!
1139 # Revision 1.45  2001/11/22 15:46:42  jhermann
1140 # Added module docstrings to all modules.
1142 # Revision 1.44  2001/11/21 23:35:45  jhermann
1143 # Added globbing for win32, and sample marking in a 2nd file to test it
1145 # Revision 1.43  2001/11/21 04:04:43  richard
1146 # *sigh* more missing value handling
1148 # Revision 1.42  2001/11/21 03:40:54  richard
1149 # more new property handling
1151 # Revision 1.41  2001/11/15 10:26:01  richard
1152 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1154 # Revision 1.40  2001/11/03 01:56:51  richard
1155 # More HTML compliance fixes. This will probably fix the Netscape problem
1156 # too.
1158 # Revision 1.39  2001/11/03 01:43:47  richard
1159 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1161 # Revision 1.38  2001/10/31 06:58:51  richard
1162 # Added the wrap="hard" attribute to the textarea of the note field so the
1163 # messages wrap sanely.
1165 # Revision 1.37  2001/10/31 06:24:35  richard
1166 # Added do_stext to htmltemplate, thanks Brad Clements.
1168 # Revision 1.36  2001/10/28 22:51:38  richard
1169 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1171 # Revision 1.35  2001/10/24 00:04:41  richard
1172 # Removed the "infinite authentication loop", thanks Roch'e
1174 # Revision 1.34  2001/10/23 22:56:36  richard
1175 # Bugfix in filter "widget" placement, thanks Roch'e
1177 # Revision 1.33  2001/10/23 01:00:18  richard
1178 # Re-enabled login and registration access after lopping them off via
1179 # disabling access for anonymous users.
1180 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1181 # a couple of bugs while I was there. Probably introduced a couple, but
1182 # things seem to work OK at the moment.
1184 # Revision 1.32  2001/10/22 03:25:01  richard
1185 # Added configuration for:
1186 #  . anonymous user access and registration (deny/allow)
1187 #  . filter "widget" location on index page (top, bottom, both)
1188 # Updated some documentation.
1190 # Revision 1.31  2001/10/21 07:26:35  richard
1191 # feature #473127: Filenames. I modified the file.index and htmltemplate
1192 #  source so that the filename is used in the link and the creation
1193 #  information is displayed.
1195 # Revision 1.30  2001/10/21 04:44:50  richard
1196 # bug #473124: UI inconsistency with Link fields.
1197 #    This also prompted me to fix a fairly long-standing usability issue -
1198 #    that of being able to turn off certain filters.
1200 # Revision 1.29  2001/10/21 00:17:56  richard
1201 # CGI interface view customisation section may now be hidden (patch from
1202 #  Roch'e Compaan.)
1204 # Revision 1.28  2001/10/21 00:00:16  richard
1205 # Fixed Checklist function - wasn't always working on a list.
1207 # Revision 1.27  2001/10/20 12:13:44  richard
1208 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1210 # Revision 1.26  2001/10/14 10:55:00  richard
1211 # Handle empty strings in HTML template Link function
1213 # Revision 1.25  2001/10/09 07:25:59  richard
1214 # Added the Password property type. See "pydoc roundup.password" for
1215 # implementation details. Have updated some of the documentation too.
1217 # Revision 1.24  2001/09/27 06:45:58  richard
1218 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1219 # on the plain() template function to escape the text for HTML.
1221 # Revision 1.23  2001/09/10 09:47:18  richard
1222 # Fixed bug in the generation of links to Link/Multilink in indexes.
1223 #   (thanks Hubert Hoegl)
1224 # Added AssignedTo to the "classic" schema's item page.
1226 # Revision 1.22  2001/08/30 06:01:17  richard
1227 # Fixed missing import in mailgw :(
1229 # Revision 1.21  2001/08/16 07:34:59  richard
1230 # better CGI text searching - but hidden filter fields are disappearing...
1232 # Revision 1.20  2001/08/15 23:43:18  richard
1233 # Fixed some isFooTypes that I missed.
1234 # Refactored some code in the CGI code.
1236 # Revision 1.19  2001/08/12 06:32:36  richard
1237 # using isinstance(blah, Foo) now instead of isFooType
1239 # Revision 1.18  2001/08/07 00:24:42  richard
1240 # stupid typo
1242 # Revision 1.17  2001/08/07 00:15:51  richard
1243 # Added the copyright/license notice to (nearly) all files at request of
1244 # Bizar Software.
1246 # Revision 1.16  2001/08/01 03:52:23  richard
1247 # Checklist was using wrong name.
1249 # Revision 1.15  2001/07/30 08:12:17  richard
1250 # Added time logging and file uploading to the templates.
1252 # Revision 1.14  2001/07/30 06:17:45  richard
1253 # Features:
1254 #  . Added ability for cgi newblah forms to indicate that the new node
1255 #    should be linked somewhere.
1256 # Fixed:
1257 #  . Fixed the agument handling for the roundup-admin find command.
1258 #  . Fixed handling of summary when no note supplied for newblah. Again.
1259 #  . Fixed detection of no form in htmltemplate Field display.
1261 # Revision 1.13  2001/07/30 02:37:53  richard
1262 # Temporary measure until we have decent schema migration.
1264 # Revision 1.12  2001/07/30 01:24:33  richard
1265 # Handles new node display now.
1267 # Revision 1.11  2001/07/29 09:31:35  richard
1268 # oops
1270 # Revision 1.10  2001/07/29 09:28:23  richard
1271 # Fixed sorting by clicking on column headings.
1273 # Revision 1.9  2001/07/29 08:27:40  richard
1274 # Fixed handling of passed-in values in form elements (ie. during a
1275 # drill-down)
1277 # Revision 1.8  2001/07/29 07:01:39  richard
1278 # Added vim command to all source so that we don't get no steenkin' tabs :)
1280 # Revision 1.7  2001/07/29 05:36:14  richard
1281 # Cleanup of the link label generation.
1283 # Revision 1.6  2001/07/29 04:06:42  richard
1284 # Fixed problem in link display when Link value is None.
1286 # Revision 1.5  2001/07/28 08:17:09  richard
1287 # fixed use of stylesheet
1289 # Revision 1.4  2001/07/28 07:59:53  richard
1290 # Replaced errno integers with their module values.
1291 # De-tabbed templatebuilder.py
1293 # Revision 1.3  2001/07/25 03:39:47  richard
1294 # Hrm - displaying links to classes that don't specify a key property. I've
1295 # got it defaulting to 'name', then 'title' and then a "random" property (first
1296 # one returned by getprops().keys().
1297 # Needs to be moved onto the Class I think...
1299 # Revision 1.2  2001/07/22 12:09:32  richard
1300 # Final commit of Grande Splite
1302 # Revision 1.1  2001/07/22 11:58:35  richard
1303 # More Grande Splite
1306 # vim: set filetype=python ts=4 sw=4 et si