Code

159041051d006b97e815c3a22349bbb18902bbf0
[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.69 2002-01-23 05:10:27 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]')
303         # get the value
304         value = self.determine_value(property)
305         if not value:
306             return _('[no %(propname)s]')%{'propname':property.capitalize()}
308         propclass = self.properties[property]
309         if isinstance(propclass, hyperdb.Link):
310             linkname = propclass.classname
311             linkcl = self.db.classes[linkname]
312             k = linkcl.labelprop()
313             linkvalue = cgi.escape(linkcl.get(value, k))
314             if is_download:
315                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
316                     linkvalue, linkvalue)
317             else:
318                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
319         if isinstance(propclass, hyperdb.Multilink):
320             linkname = propclass.classname
321             linkcl = self.db.classes[linkname]
322             k = linkcl.labelprop()
323             l = []
324             for value in value:
325                 linkvalue = cgi.escape(linkcl.get(value, k))
326                 if is_download:
327                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
328                         linkvalue, linkvalue))
329                 else:
330                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
331                         linkvalue))
332             return ', '.join(l)
333         if is_download:
334             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
335                 value, value)
336         else:
337             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
339     def do_count(self, property, **args):
340         ''' for a Multilink property, display a count of the number of links in
341             the list
342         '''
343         if not self.nodeid:
344             return _('[Count: not called from item]')
346         propclass = self.properties[property]
347         if not isinstance(propclass, hyperdb.Multilink):
348             return _('[Count: not a Multilink]')
350         # figure the length then...
351         value = self.cl.get(self.nodeid, property)
352         return str(len(value))
354     # XXX pretty is definitely new ;)
355     def do_reldate(self, property, pretty=0):
356         ''' display a Date property in terms of an interval relative to the
357             current date (e.g. "+ 3w", "- 2d").
359             with the 'pretty' flag, make it pretty
360         '''
361         if not self.nodeid and self.form is None:
362             return _('[Reldate: not called from item]')
364         propclass = self.properties[property]
365         if not isinstance(propclass, hyperdb.Date):
366             return _('[Reldate: not a Date]')
368         if self.nodeid:
369             value = self.cl.get(self.nodeid, property)
370         else:
371             return ''
372         if not value:
373             return ''
375         # figure the interval
376         interval = value - date.Date('.')
377         if pretty:
378             if not self.nodeid:
379                 return _('now')
380             pretty = interval.pretty()
381             if pretty is None:
382                 pretty = value.pretty()
383             return pretty
384         return str(interval)
386     def do_download(self, property, **args):
387         ''' show a Link("file") or Multilink("file") property using links that
388             allow you to download files
389         '''
390         if not self.nodeid:
391             return _('[Download: not called from item]')
392         return self.do_link(property, is_download=1)
395     def do_checklist(self, property, **args):
396         ''' for a Link or Multilink property, display checkboxes for the
397             available choices to permit filtering
398         '''
399         propclass = self.properties[property]
400         if (not isinstance(propclass, hyperdb.Link) and not
401                 isinstance(propclass, hyperdb.Multilink)):
402             return _('[Checklist: not a link]')
404         # get our current checkbox state
405         if self.nodeid:
406             # get the info from the node - make sure it's a list
407             if isinstance(propclass, hyperdb.Link):
408                 value = [self.cl.get(self.nodeid, property)]
409             else:
410                 value = self.cl.get(self.nodeid, property)
411         elif self.filterspec is not None:
412             # get the state from the filter specification (always a list)
413             value = self.filterspec.get(property, [])
414         else:
415             # it's a new node, so there's no state
416             value = []
418         # so we can map to the linked node's "lable" property
419         linkcl = self.db.classes[propclass.classname]
420         l = []
421         k = linkcl.labelprop()
422         for optionid in linkcl.list():
423             option = linkcl.get(optionid, k)
424             if optionid in value or option in value:
425                 checked = 'checked'
426             else:
427                 checked = ''
428             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
429                 option, checked, property, option))
431         # for Links, allow the "unselected" option too
432         if isinstance(propclass, hyperdb.Link):
433             if value is None or '-1' in value:
434                 checked = 'checked'
435             else:
436                 checked = ''
437             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
438                 'value="-1">')%(checked, property))
439         return '\n'.join(l)
441     def do_note(self, rows=5, cols=80):
442         ''' display a "note" field, which is a text area for entering a note to
443             go along with a change. 
444         '''
445         # TODO: pull the value from the form
446         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
447             '</textarea>'%(rows, cols)
449     # XXX new function
450     def do_list(self, property, reverse=0):
451         ''' list the items specified by property using the standard index for
452             the class
453         '''
454         propcl = self.properties[property]
455         if not isinstance(propcl, hyperdb.Multilink):
456             return _('[List: not a Multilink]')
457         value = self.cl.get(self.nodeid, property)
458         value.sort()
459         if reverse:
460             value.reverse()
462         # render the sub-index into a string
463         fp = StringIO.StringIO()
464         try:
465             write_save = self.client.write
466             self.client.write = fp.write
467             index = IndexTemplate(self.client, self.templates, propcl.classname)
468             index.render(nodeids=value, show_display_form=0)
469         finally:
470             self.client.write = write_save
472         return fp.getvalue()
474     # XXX new function
475     def do_history(self, direction='descending'):
476         ''' list the history of the item
478             If "direction" is 'descending' then the most recent event will
479             be displayed first. If it is 'ascending' then the oldest event
480             will be displayed first.
481         '''
482         if self.nodeid is None:
483             return _("[History: node doesn't exist]")
485         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
486             '<tr class="list-header">',
487             _('<th align=left><span class="list-item">Date</span></th>'),
488             _('<th align=left><span class="list-item">User</span></th>'),
489             _('<th align=left><span class="list-item">Action</span></th>'),
490             _('<th align=left><span class="list-item">Args</span></th>'),
491             '</tr>']
493         comments = {}
494         history = self.cl.history(self.nodeid)
495         history.sort()
496         if direction == 'descending':
497             history.reverse()
498         for id, evt_date, user, action, args in history:
499             date_s = str(evt_date).replace("."," ")
500             arg_s = ''
501             if action == 'link' and type(args) == type(()):
502                 if len(args) == 3:
503                     linkcl, linkid, key = args
504                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
505                         linkcl, linkid, key)
506                 else:
507                     arg_s = str(arg)
509             elif action == 'unlink' and type(args) == type(()):
510                 if len(args) == 3:
511                     linkcl, linkid, key = args
512                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
513                         linkcl, linkid, key)
514                 else:
515                     arg_s = str(arg)
517             elif type(args) == type({}):
518                 cell = []
519                 for k in args.keys():
520                     # try to get the relevant property and treat it
521                     # specially
522                     try:
523                         prop = self.properties[k]
524                     except:
525                         prop = None
526                     if prop is not None:
527                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
528                                 isinstance(prop, hyperdb.Link)):
529                             # figure what the link class is
530                             classname = prop.classname
531                             try:
532                                 linkcl = self.db.classes[classname]
533                             except KeyError, message:
534                                 labelprop = None
535                                 comments[classname] = _('''The linked class
536                                     %(classname)s no longer exists''')%locals()
537                             labelprop = linkcl.labelprop()
539                         if isinstance(prop, hyperdb.Multilink) and \
540                                 len(args[k]) > 0:
541                             ml = []
542                             for linkid in args[k]:
543                                 label = classname + linkid
544                                 # if we have a label property, try to use it
545                                 # TODO: test for node existence even when
546                                 # there's no labelprop!
547                                 try:
548                                     if labelprop is not None:
549                                         label = linkcl.get(linkid, labelprop)
550                                 except IndexError:
551                                     comments['no_link'] = _('''<strike>The
552                                         linked node no longer
553                                         exists</strike>''')
554                                     ml.append('<strike>%s</strike>'%label)
555                                 else:
556                                     ml.append('<a href="%s%s">%s</a>'%(
557                                         classname, linkid, label))
558                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
559                         elif isinstance(prop, hyperdb.Link) and args[k]:
560                             label = classname + args[k]
561                             # if we have a label property, try to use it
562                             # TODO: test for node existence even when
563                             # there's no labelprop!
564                             if labelprop is not None:
565                                 try:
566                                     label = linkcl.get(args[k], labelprop)
567                                 except IndexError:
568                                     comments['no_link'] = _('''<strike>The
569                                         linked node no longer
570                                         exists</strike>''')
571                                     cell.append(' <strike>%s</strike>,\n'%label)
572                                     # "flag" this is done .... euwww
573                                     label = None
574                             if label is not None:
575                                 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
576                                     classname, args[k], label))
578                         elif isinstance(prop, hyperdb.Date) and args[k]:
579                             d = date.Date(args[k])
580                             cell.append('%s: %s'%(k, str(d)))
582                         elif isinstance(prop, hyperdb.Interval) and args[k]:
583                             d = date.Interval(args[k])
584                             cell.append('%s: %s'%(k, str(d)))
586                         elif not args[k]:
587                             cell.append('%s: (no value)\n'%k)
589                         else:
590                             cell.append('%s: %s\n'%(k, str(args[k])))
591                     else:
592                         # property no longer exists
593                         comments['no_exist'] = _('''<em>The indicated property
594                             no longer exists</em>''')
595                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
596                 arg_s = '<br />'.join(cell)
597             else:
598                 # unkown event!!
599                 comments['unknown'] = _('''<strong><em>This event is not
600                     handled by the history display!</em></strong>''')
601                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
602             date_s = date_s.replace(' ', '&nbsp;')
603             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
604                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
605                 user, action, arg_s))
606         if comments:
607             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
608         for entry in comments.values():
609             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
610         l.append('</table>')
611         return '\n'.join(l)
613     # XXX new function
614     def do_submit(self):
615         ''' add a submit button for the item
616         '''
617         if self.nodeid:
618             return _('<input type="submit" name="submit" value="Submit Changes">')
619         elif self.form is not None:
620             return _('<input type="submit" name="submit" value="Submit New Entry">')
621         else:
622             return _('[Submit: not called from item]')
626 #   INDEX TEMPLATES
628 class IndexTemplateReplace:
629     def __init__(self, globals, locals, props):
630         self.globals = globals
631         self.locals = locals
632         self.props = props
634     replace=re.compile(
635         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
636         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
637     def go(self, text):
638         return self.replace.sub(self, text)
640     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
641         if m.group('name'):
642             if m.group('name') in self.props:
643                 text = m.group('text')
644                 replace = IndexTemplateReplace(self.globals, {}, self.props)
645                 return replace.go(m.group('text'))
646             else:
647                 return ''
648         if m.group('display'):
649             command = m.group('command')
650             return eval(command, self.globals, self.locals)
651         print '*** unhandled match', m.groupdict()
653 class IndexTemplate(TemplateFunctions):
654     def __init__(self, client, templates, classname):
655         self.client = client
656         self.instance = client.instance
657         self.templates = templates
658         self.classname = classname
660         # derived
661         self.db = self.client.db
662         self.cl = self.db.classes[self.classname]
663         self.properties = self.cl.getprops()
665         TemplateFunctions.__init__(self)
667     col_re=re.compile(r'<property\s+name="([^>]+)">')
668     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
669             show_display_form=1, nodeids=None, show_customization=1):
670         self.filterspec = filterspec
672         w = self.client.write
674         # get the filter template
675         try:
676             filter_template = open(os.path.join(self.templates,
677                 self.classname+'.filter')).read()
678             all_filters = self.col_re.findall(filter_template)
679         except IOError, error:
680             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
681             filter_template = None
682             all_filters = []
684         # XXX deviate from spec here ...
685         # load the index section template and figure the default columns from it
686         template = open(os.path.join(self.templates,
687             self.classname+'.index')).read()
688         all_columns = self.col_re.findall(template)
689         if not columns:
690             columns = []
691             for name in all_columns:
692                 columns.append(name)
693         else:
694             # re-sort columns to be the same order as all_columns
695             l = []
696             for name in all_columns:
697                 if name in columns:
698                     l.append(name)
699             columns = l
701         # display the filter section
702         if (show_display_form and 
703                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
704             w('<form action="%s">\n'%self.classname)
705             self.filter_section(filter_template, filter, columns, group,
706                 all_filters, all_columns, show_customization)
707             # make sure that the sorting doesn't get lost either
708             if sort:
709                 w('<input type="hidden" name=":sort" value="%s">'%
710                     ','.join(sort))
711             w('</form>\n')
714         # now display the index section
715         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
716         w('<tr class="list-header">\n')
717         for name in columns:
718             cname = name.capitalize()
719             if show_display_form:
720                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
721                 anchor = "%s?%s"%(self.classname, sb)
722                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
723                     anchor, cname))
724             else:
725                 w('<td><span class="list-header">%s</span></td>\n'%cname)
726         w('</tr>\n')
728         # this stuff is used for group headings - optimise the group names
729         old_group = None
730         group_names = []
731         if group:
732             for name in group:
733                 if name[0] == '-': group_names.append(name[1:])
734                 else: group_names.append(name)
736         # now actually loop through all the nodes we get from the filter and
737         # apply the template
738         if nodeids is None:
739             nodeids = self.cl.filter(filterspec, sort, group)
740         for nodeid in nodeids:
741             # check for a group heading
742             if group_names:
743                 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
744                 if this_group != old_group:
745                     l = []
746                     for name in group_names:
747                         prop = self.properties[name]
748                         if isinstance(prop, hyperdb.Link):
749                             group_cl = self.db.classes[prop.classname]
750                             key = group_cl.getkey()
751                             value = self.cl.get(nodeid, name)
752                             if value is None:
753                                 l.append(_('[unselected %(classname)s]')%{
754                                     'classname': prop.classname})
755                             else:
756                                 l.append(group_cl.get(self.cl.get(nodeid,
757                                     name), key))
758                         elif isinstance(prop, hyperdb.Multilink):
759                             group_cl = self.db.classes[prop.classname]
760                             key = group_cl.getkey()
761                             for value in self.cl.get(nodeid, name):
762                                 l.append(group_cl.get(value, key))
763                         else:
764                             value = self.cl.get(nodeid, name, _('[no value]'))
765                             if value is None:
766                                 value = _('[empty %(name)s]')%locals()
767                             else:
768                                 value = str(value)
769                             l.append(value)
770                     w('<tr class="section-bar">'
771                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
772                         len(columns), ', '.join(l)))
773                     old_group = this_group
775             # display this node's row
776             replace = IndexTemplateReplace(self.globals, locals(), columns)
777             self.nodeid = nodeid
778             w(replace.go(template))
779             self.nodeid = None
781         w('</table>')
783         # display the filter section
784         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
785                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
786             w('<form action="%s">\n'%self.classname)
787             self.filter_section(filter_template, filter, columns, group,
788                 all_filters, all_columns, show_customization)
789             # make sure that the sorting doesn't get lost either
790             if sort:
791                 w('<input type="hidden" name=":sort" value="%s">'%
792                     ','.join(sort))
793             w('</form>\n')
796     def filter_section(self, template, filter, columns, group, all_filters,
797             all_columns, show_customization):
799         w = self.client.write
801         # wrap the template in a single table to ensure the whole widget
802         # is displayed at once
803         w('<table><tr><td>')
805         if template and filter:
806             # display the filter section
807             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
808             w('<tr class="location-bar">')
809             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
810             w('</tr>')
811             replace = IndexTemplateReplace(self.globals, locals(), filter)
812             w(replace.go(template))
813             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
814             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
815             w('</table>')
817         # now add in the filter/columns/group/etc config table form
818         w('<input type="hidden" name="show_customization" value="%s">' %
819             show_customization )
820         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
821         names = []
822         for name in self.properties.keys():
823             if name in all_filters or name in all_columns:
824                 names.append(name)
825         if show_customization:
826             action = '-'
827         else:
828             action = '+'
829             # hide the values for filters, columns and grouping in the form
830             # if the customization widget is not visible
831             for name in names:
832                 if all_filters and name in filter:
833                     w('<input type="hidden" name=":filter" value="%s">' % name)
834                 if all_columns and name in columns:
835                     w('<input type="hidden" name=":columns" value="%s">' % name)
836                 if all_columns and name in group:
837                     w('<input type="hidden" name=":group" value="%s">' % name)
839         # TODO: The widget style can go into the stylesheet
840         w(_('<th align="left" colspan=%s>'
841           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
842           'customisation...</th></tr>\n')%(len(names)+1, action))
844         if not show_customization:
845             w('</table>\n')
846             return
848         w('<tr class="location-bar"><th>&nbsp;</th>')
849         for name in names:
850             w('<th>%s</th>'%name.capitalize())
851         w('</tr>\n')
853         # Filter
854         if all_filters:
855             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
856             for name in names:
857                 if name not in all_filters:
858                     w('<td>&nbsp;</td>')
859                     continue
860                 if name in filter: checked=' checked'
861                 else: checked=''
862                 w('<td align=middle>\n')
863                 w(' <input type="checkbox" name=":filter" value="%s" '
864                   '%s></td>\n'%(name, checked))
865             w('</tr>\n')
867         # Columns
868         if all_columns:
869             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
870             for name in names:
871                 if name not in all_columns:
872                     w('<td>&nbsp;</td>')
873                     continue
874                 if name in columns: checked=' checked'
875                 else: checked=''
876                 w('<td align=middle>\n')
877                 w(' <input type="checkbox" name=":columns" value="%s"'
878                   '%s></td>\n'%(name, checked))
879             w('</tr>\n')
881             # Grouping
882             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
883             for name in names:
884                 prop = self.properties[name]
885                 if name not in all_columns:
886                     w('<td>&nbsp;</td>')
887                     continue
888                 if name in group: checked=' checked'
889                 else: checked=''
890                 w('<td align=middle>\n')
891                 w(' <input type="checkbox" name=":group" value="%s"'
892                   '%s></td>\n'%(name, checked))
893             w('</tr>\n')
895         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
896         w('<td colspan="%s">'%len(names))
897         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
898         w('</tr>\n')
899         w('</table>\n')
901         # and the outer table
902         w('</td></tr></table>')
905     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
906         l = []
907         w = l.append
908         for k, v in filterspec.items():
909             k = urllib.quote(k)
910             if type(v) == type([]):
911                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
912             else:
913                 w('%s=%s'%(k, urllib.quote(v)))
914         if columns:
915             w(':columns=%s'%','.join(map(urllib.quote, columns)))
916         if filter:
917             w(':filter=%s'%','.join(map(urllib.quote, filter)))
918         if group:
919             w(':group=%s'%','.join(map(urllib.quote, group)))
920         m = []
921         s_dir = ''
922         for name in sort:
923             dir = name[0]
924             if dir == '-':
925                 name = name[1:]
926             else:
927                 dir = ''
928             if sort_name == name:
929                 if dir == '-':
930                     s_dir = ''
931                 else:
932                     s_dir = '-'
933             else:
934                 m.append(dir+urllib.quote(name))
935         m.insert(0, s_dir+urllib.quote(sort_name))
936         # so things don't get completely out of hand, limit the sort to
937         # two columns
938         w(':sort=%s'%','.join(m[:2]))
939         return '&'.join(l)
942 #   ITEM TEMPLATES
944 class ItemTemplateReplace:
945     def __init__(self, globals, locals, cl, nodeid):
946         self.globals = globals
947         self.locals = locals
948         self.cl = cl
949         self.nodeid = nodeid
951     replace=re.compile(
952         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
953         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
954     def go(self, text):
955         return self.replace.sub(self, text)
957     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
958         if m.group('name'):
959             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
960                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
961                     self.nodeid)
962                 return replace.go(m.group('text'))
963             else:
964                 return ''
965         if m.group('display'):
966             command = m.group('command')
967             return eval(command, self.globals, self.locals)
968         print '*** unhandled match', m.groupdict()
971 class ItemTemplate(TemplateFunctions):
972     def __init__(self, client, templates, classname):
973         self.client = client
974         self.instance = client.instance
975         self.templates = templates
976         self.classname = classname
978         # derived
979         self.db = self.client.db
980         self.cl = self.db.classes[self.classname]
981         self.properties = self.cl.getprops()
983         TemplateFunctions.__init__(self)
985     def render(self, nodeid):
986         self.nodeid = nodeid
988         if (self.properties.has_key('type') and
989                 self.properties.has_key('content')):
990             pass
991             # XXX we really want to return this as a downloadable...
992             #  currently I handle this at a higher level by detecting 'file'
993             #  designators...
995         w = self.client.write
996         w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
997             self.classname, nodeid))
998         s = open(os.path.join(self.templates, self.classname+'.item')).read()
999         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1000         w(replace.go(s))
1001         w('</form>')
1004 class NewItemTemplate(TemplateFunctions):
1005     def __init__(self, client, templates, classname):
1006         self.client = client
1007         self.instance = client.instance
1008         self.templates = templates
1009         self.classname = classname
1011         # derived
1012         self.db = self.client.db
1013         self.cl = self.db.classes[self.classname]
1014         self.properties = self.cl.getprops()
1016         TemplateFunctions.__init__(self)
1018     def render(self, form):
1019         self.form = form
1020         w = self.client.write
1021         c = self.classname
1022         try:
1023             s = open(os.path.join(self.templates, c+'.newitem')).read()
1024         except IOError:
1025             s = open(os.path.join(self.templates, c+'.item')).read()
1026         w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
1027         for key in form.keys():
1028             if key[0] == ':':
1029                 value = form[key].value
1030                 if type(value) != type([]): value = [value]
1031                 for value in value:
1032                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
1033         replace = ItemTemplateReplace(self.globals, locals(), None, None)
1034         w(replace.go(s))
1035         w('</form>')
1038 # $Log: not supported by cvs2svn $
1039 # Revision 1.68  2002/01/22 22:55:28  richard
1040 #  . htmltemplate list() wasn't sorting...
1042 # Revision 1.67  2002/01/22 22:46:22  richard
1043 # more htmltemplate cleanups and unit tests
1045 # Revision 1.66  2002/01/22 06:35:40  richard
1046 # more htmltemplate tests and cleanup
1048 # Revision 1.65  2002/01/22 00:12:06  richard
1049 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1050 # off the implementation of some of the functions so they behave sanely.
1052 # Revision 1.64  2002/01/21 03:25:59  richard
1053 # oops
1055 # Revision 1.63  2002/01/21 02:59:10  richard
1056 # Fixed up the HTML display of history so valid links are actually displayed.
1057 # Oh for some unit tests! :(
1059 # Revision 1.62  2002/01/18 08:36:12  grubert
1060 #  . add nowrap to history table date cell i.e. <td nowrap ...
1062 # Revision 1.61  2002/01/17 23:04:53  richard
1063 #  . much nicer history display (actualy real handling of property types etc)
1065 # Revision 1.60  2002/01/17 08:48:19  grubert
1066 #  . display superseder as html link in history.
1068 # Revision 1.59  2002/01/17 07:58:24  grubert
1069 #  . display links a html link in history.
1071 # Revision 1.58  2002/01/15 00:50:03  richard
1072 # #502949 ] index view for non-issues and redisplay
1074 # Revision 1.57  2002/01/14 23:31:21  richard
1075 # reverted the change that had plain() hyperlinking the link displays -
1076 # that's what link() is for!
1078 # Revision 1.56  2002/01/14 07:04:36  richard
1079 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1080 #    the linked node's page.
1081 #    ... this allows a display very similar to bugzilla's where you can actually
1082 #    find out information about the linked node.
1084 # Revision 1.55  2002/01/14 06:45:03  richard
1085 #  . #502953 ] nosy-like treatment of other multilinks
1086 #    ... had to revert most of the previous change to the multilink field
1087 #    display... not good.
1089 # Revision 1.54  2002/01/14 05:16:51  richard
1090 # The submit buttons need a name attribute or mozilla won't submit without a
1091 # file upload. Yeah, that's bloody obscure. Grr.
1093 # Revision 1.53  2002/01/14 04:03:32  richard
1094 # How about that ... date fields have never worked ...
1096 # Revision 1.52  2002/01/14 02:20:14  richard
1097 #  . changed all config accesses so they access either the instance or the
1098 #    config attriubute on the db. This means that all config is obtained from
1099 #    instance_config instead of the mish-mash of classes. This will make
1100 #    switching to a ConfigParser setup easier too, I hope.
1102 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1103 # 0.5.0 switch, I hope!)
1105 # Revision 1.51  2002/01/10 10:02:15  grubert
1106 # In do_history: replace "." in date by " " so html wraps more sensible.
1107 # Should this be done in date's string converter ?
1109 # Revision 1.50  2002/01/05 02:35:10  richard
1110 # I18N'ification
1112 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1113 # Features added:
1114 #  .  Multilink properties are now displayed as comma separated values in
1115 #     a textbox
1116 #  .  The add user link is now only visible to the admin user
1117 #  .  Modified the mail gateway to reject submissions from unknown
1118 #     addresses if ANONYMOUS_ACCESS is denied
1120 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1121 # Bugs fixed:
1122 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1123 #     lost somewhere
1124 #   . Internet Explorer submits full path for filename - we now strip away
1125 #     the path
1126 # Features added:
1127 #   . Link and multilink properties are now displayed sorted in the cgi
1128 #     interface
1130 # Revision 1.47  2001/11/26 22:55:56  richard
1131 # Feature:
1132 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1133 #    the instance.
1134 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1135 #    signature info in e-mails.
1136 #  . Some more flexibility in the mail gateway and more error handling.
1137 #  . Login now takes you to the page you back to the were denied access to.
1139 # Fixed:
1140 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1142 # Revision 1.46  2001/11/24 00:53:12  jhermann
1143 # "except:" is bad, bad , bad!
1145 # Revision 1.45  2001/11/22 15:46:42  jhermann
1146 # Added module docstrings to all modules.
1148 # Revision 1.44  2001/11/21 23:35:45  jhermann
1149 # Added globbing for win32, and sample marking in a 2nd file to test it
1151 # Revision 1.43  2001/11/21 04:04:43  richard
1152 # *sigh* more missing value handling
1154 # Revision 1.42  2001/11/21 03:40:54  richard
1155 # more new property handling
1157 # Revision 1.41  2001/11/15 10:26:01  richard
1158 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1160 # Revision 1.40  2001/11/03 01:56:51  richard
1161 # More HTML compliance fixes. This will probably fix the Netscape problem
1162 # too.
1164 # Revision 1.39  2001/11/03 01:43:47  richard
1165 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1167 # Revision 1.38  2001/10/31 06:58:51  richard
1168 # Added the wrap="hard" attribute to the textarea of the note field so the
1169 # messages wrap sanely.
1171 # Revision 1.37  2001/10/31 06:24:35  richard
1172 # Added do_stext to htmltemplate, thanks Brad Clements.
1174 # Revision 1.36  2001/10/28 22:51:38  richard
1175 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1177 # Revision 1.35  2001/10/24 00:04:41  richard
1178 # Removed the "infinite authentication loop", thanks Roch'e
1180 # Revision 1.34  2001/10/23 22:56:36  richard
1181 # Bugfix in filter "widget" placement, thanks Roch'e
1183 # Revision 1.33  2001/10/23 01:00:18  richard
1184 # Re-enabled login and registration access after lopping them off via
1185 # disabling access for anonymous users.
1186 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1187 # a couple of bugs while I was there. Probably introduced a couple, but
1188 # things seem to work OK at the moment.
1190 # Revision 1.32  2001/10/22 03:25:01  richard
1191 # Added configuration for:
1192 #  . anonymous user access and registration (deny/allow)
1193 #  . filter "widget" location on index page (top, bottom, both)
1194 # Updated some documentation.
1196 # Revision 1.31  2001/10/21 07:26:35  richard
1197 # feature #473127: Filenames. I modified the file.index and htmltemplate
1198 #  source so that the filename is used in the link and the creation
1199 #  information is displayed.
1201 # Revision 1.30  2001/10/21 04:44:50  richard
1202 # bug #473124: UI inconsistency with Link fields.
1203 #    This also prompted me to fix a fairly long-standing usability issue -
1204 #    that of being able to turn off certain filters.
1206 # Revision 1.29  2001/10/21 00:17:56  richard
1207 # CGI interface view customisation section may now be hidden (patch from
1208 #  Roch'e Compaan.)
1210 # Revision 1.28  2001/10/21 00:00:16  richard
1211 # Fixed Checklist function - wasn't always working on a list.
1213 # Revision 1.27  2001/10/20 12:13:44  richard
1214 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1216 # Revision 1.26  2001/10/14 10:55:00  richard
1217 # Handle empty strings in HTML template Link function
1219 # Revision 1.25  2001/10/09 07:25:59  richard
1220 # Added the Password property type. See "pydoc roundup.password" for
1221 # implementation details. Have updated some of the documentation too.
1223 # Revision 1.24  2001/09/27 06:45:58  richard
1224 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1225 # on the plain() template function to escape the text for HTML.
1227 # Revision 1.23  2001/09/10 09:47:18  richard
1228 # Fixed bug in the generation of links to Link/Multilink in indexes.
1229 #   (thanks Hubert Hoegl)
1230 # Added AssignedTo to the "classic" schema's item page.
1232 # Revision 1.22  2001/08/30 06:01:17  richard
1233 # Fixed missing import in mailgw :(
1235 # Revision 1.21  2001/08/16 07:34:59  richard
1236 # better CGI text searching - but hidden filter fields are disappearing...
1238 # Revision 1.20  2001/08/15 23:43:18  richard
1239 # Fixed some isFooTypes that I missed.
1240 # Refactored some code in the CGI code.
1242 # Revision 1.19  2001/08/12 06:32:36  richard
1243 # using isinstance(blah, Foo) now instead of isFooType
1245 # Revision 1.18  2001/08/07 00:24:42  richard
1246 # stupid typo
1248 # Revision 1.17  2001/08/07 00:15:51  richard
1249 # Added the copyright/license notice to (nearly) all files at request of
1250 # Bizar Software.
1252 # Revision 1.16  2001/08/01 03:52:23  richard
1253 # Checklist was using wrong name.
1255 # Revision 1.15  2001/07/30 08:12:17  richard
1256 # Added time logging and file uploading to the templates.
1258 # Revision 1.14  2001/07/30 06:17:45  richard
1259 # Features:
1260 #  . Added ability for cgi newblah forms to indicate that the new node
1261 #    should be linked somewhere.
1262 # Fixed:
1263 #  . Fixed the agument handling for the roundup-admin find command.
1264 #  . Fixed handling of summary when no note supplied for newblah. Again.
1265 #  . Fixed detection of no form in htmltemplate Field display.
1267 # Revision 1.13  2001/07/30 02:37:53  richard
1268 # Temporary measure until we have decent schema migration.
1270 # Revision 1.12  2001/07/30 01:24:33  richard
1271 # Handles new node display now.
1273 # Revision 1.11  2001/07/29 09:31:35  richard
1274 # oops
1276 # Revision 1.10  2001/07/29 09:28:23  richard
1277 # Fixed sorting by clicking on column headings.
1279 # Revision 1.9  2001/07/29 08:27:40  richard
1280 # Fixed handling of passed-in values in form elements (ie. during a
1281 # drill-down)
1283 # Revision 1.8  2001/07/29 07:01:39  richard
1284 # Added vim command to all source so that we don't get no steenkin' tabs :)
1286 # Revision 1.7  2001/07/29 05:36:14  richard
1287 # Cleanup of the link label generation.
1289 # Revision 1.6  2001/07/29 04:06:42  richard
1290 # Fixed problem in link display when Link value is None.
1292 # Revision 1.5  2001/07/28 08:17:09  richard
1293 # fixed use of stylesheet
1295 # Revision 1.4  2001/07/28 07:59:53  richard
1296 # Replaced errno integers with their module values.
1297 # De-tabbed templatebuilder.py
1299 # Revision 1.3  2001/07/25 03:39:47  richard
1300 # Hrm - displaying links to classes that don't specify a key property. I've
1301 # got it defaulting to 'name', then 'title' and then a "random" property (first
1302 # one returned by getprops().keys().
1303 # Needs to be moved onto the Class I think...
1305 # Revision 1.2  2001/07/22 12:09:32  richard
1306 # Final commit of Grande Splite
1308 # Revision 1.1  2001/07/22 11:58:35  richard
1309 # More Grande Splite
1312 # vim: set filetype=python ts=4 sw=4 et si