Code

e913dc2968968b72f59d1d32bed61fcb204a5929
[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.72 2002-02-14 23:39:18 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 = cgi.escape(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]')
458         value = self.determine_value(property)
459         if not value:
460             return ''
462         # sort, possibly revers and then re-stringify
463         value = map(int, value)
464         value.sort()
465         if reverse:
466             value.reverse()
467         value = map(str, value)
469         # render the sub-index into a string
470         fp = StringIO.StringIO()
471         try:
472             write_save = self.client.write
473             self.client.write = fp.write
474             index = IndexTemplate(self.client, self.templates, propcl.classname)
475             index.render(nodeids=value, show_display_form=0)
476         finally:
477             self.client.write = write_save
479         return fp.getvalue()
481     # XXX new function
482     def do_history(self, direction='descending'):
483         ''' list the history of the item
485             If "direction" is 'descending' then the most recent event will
486             be displayed first. If it is 'ascending' then the oldest event
487             will be displayed first.
488         '''
489         if self.nodeid is None:
490             return _("[History: node doesn't exist]")
492         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
493             '<tr class="list-header">',
494             _('<th align=left><span class="list-item">Date</span></th>'),
495             _('<th align=left><span class="list-item">User</span></th>'),
496             _('<th align=left><span class="list-item">Action</span></th>'),
497             _('<th align=left><span class="list-item">Args</span></th>'),
498             '</tr>']
500         comments = {}
501         history = self.cl.history(self.nodeid)
502         history.sort()
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 onSubmit="return submit_once()" 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 onSubmit="return submit_once()" 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 onSubmit="return submit_once()" 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 onSubmit="return submit_once()" 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.71  2002/01/23 06:15:24  richard
1047 # real (non-string, duh) sorting of lists by node id
1049 # Revision 1.70  2002/01/23 05:47:57  richard
1050 # more HTML template cleanup and unit tests
1052 # Revision 1.69  2002/01/23 05:10:27  richard
1053 # More HTML template cleanup and unit tests.
1054 #  - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1055 #    templates, but link(is_download=1) will still work for existing templates]
1057 # Revision 1.68  2002/01/22 22:55:28  richard
1058 #  . htmltemplate list() wasn't sorting...
1060 # Revision 1.67  2002/01/22 22:46:22  richard
1061 # more htmltemplate cleanups and unit tests
1063 # Revision 1.66  2002/01/22 06:35:40  richard
1064 # more htmltemplate tests and cleanup
1066 # Revision 1.65  2002/01/22 00:12:06  richard
1067 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1068 # off the implementation of some of the functions so they behave sanely.
1070 # Revision 1.64  2002/01/21 03:25:59  richard
1071 # oops
1073 # Revision 1.63  2002/01/21 02:59:10  richard
1074 # Fixed up the HTML display of history so valid links are actually displayed.
1075 # Oh for some unit tests! :(
1077 # Revision 1.62  2002/01/18 08:36:12  grubert
1078 #  . add nowrap to history table date cell i.e. <td nowrap ...
1080 # Revision 1.61  2002/01/17 23:04:53  richard
1081 #  . much nicer history display (actualy real handling of property types etc)
1083 # Revision 1.60  2002/01/17 08:48:19  grubert
1084 #  . display superseder as html link in history.
1086 # Revision 1.59  2002/01/17 07:58:24  grubert
1087 #  . display links a html link in history.
1089 # Revision 1.58  2002/01/15 00:50:03  richard
1090 # #502949 ] index view for non-issues and redisplay
1092 # Revision 1.57  2002/01/14 23:31:21  richard
1093 # reverted the change that had plain() hyperlinking the link displays -
1094 # that's what link() is for!
1096 # Revision 1.56  2002/01/14 07:04:36  richard
1097 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1098 #    the linked node's page.
1099 #    ... this allows a display very similar to bugzilla's where you can actually
1100 #    find out information about the linked node.
1102 # Revision 1.55  2002/01/14 06:45:03  richard
1103 #  . #502953 ] nosy-like treatment of other multilinks
1104 #    ... had to revert most of the previous change to the multilink field
1105 #    display... not good.
1107 # Revision 1.54  2002/01/14 05:16:51  richard
1108 # The submit buttons need a name attribute or mozilla won't submit without a
1109 # file upload. Yeah, that's bloody obscure. Grr.
1111 # Revision 1.53  2002/01/14 04:03:32  richard
1112 # How about that ... date fields have never worked ...
1114 # Revision 1.52  2002/01/14 02:20:14  richard
1115 #  . changed all config accesses so they access either the instance or the
1116 #    config attriubute on the db. This means that all config is obtained from
1117 #    instance_config instead of the mish-mash of classes. This will make
1118 #    switching to a ConfigParser setup easier too, I hope.
1120 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1121 # 0.5.0 switch, I hope!)
1123 # Revision 1.51  2002/01/10 10:02:15  grubert
1124 # In do_history: replace "." in date by " " so html wraps more sensible.
1125 # Should this be done in date's string converter ?
1127 # Revision 1.50  2002/01/05 02:35:10  richard
1128 # I18N'ification
1130 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1131 # Features added:
1132 #  .  Multilink properties are now displayed as comma separated values in
1133 #     a textbox
1134 #  .  The add user link is now only visible to the admin user
1135 #  .  Modified the mail gateway to reject submissions from unknown
1136 #     addresses if ANONYMOUS_ACCESS is denied
1138 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1139 # Bugs fixed:
1140 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1141 #     lost somewhere
1142 #   . Internet Explorer submits full path for filename - we now strip away
1143 #     the path
1144 # Features added:
1145 #   . Link and multilink properties are now displayed sorted in the cgi
1146 #     interface
1148 # Revision 1.47  2001/11/26 22:55:56  richard
1149 # Feature:
1150 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1151 #    the instance.
1152 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1153 #    signature info in e-mails.
1154 #  . Some more flexibility in the mail gateway and more error handling.
1155 #  . Login now takes you to the page you back to the were denied access to.
1157 # Fixed:
1158 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1160 # Revision 1.46  2001/11/24 00:53:12  jhermann
1161 # "except:" is bad, bad , bad!
1163 # Revision 1.45  2001/11/22 15:46:42  jhermann
1164 # Added module docstrings to all modules.
1166 # Revision 1.44  2001/11/21 23:35:45  jhermann
1167 # Added globbing for win32, and sample marking in a 2nd file to test it
1169 # Revision 1.43  2001/11/21 04:04:43  richard
1170 # *sigh* more missing value handling
1172 # Revision 1.42  2001/11/21 03:40:54  richard
1173 # more new property handling
1175 # Revision 1.41  2001/11/15 10:26:01  richard
1176 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1178 # Revision 1.40  2001/11/03 01:56:51  richard
1179 # More HTML compliance fixes. This will probably fix the Netscape problem
1180 # too.
1182 # Revision 1.39  2001/11/03 01:43:47  richard
1183 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1185 # Revision 1.38  2001/10/31 06:58:51  richard
1186 # Added the wrap="hard" attribute to the textarea of the note field so the
1187 # messages wrap sanely.
1189 # Revision 1.37  2001/10/31 06:24:35  richard
1190 # Added do_stext to htmltemplate, thanks Brad Clements.
1192 # Revision 1.36  2001/10/28 22:51:38  richard
1193 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1195 # Revision 1.35  2001/10/24 00:04:41  richard
1196 # Removed the "infinite authentication loop", thanks Roch'e
1198 # Revision 1.34  2001/10/23 22:56:36  richard
1199 # Bugfix in filter "widget" placement, thanks Roch'e
1201 # Revision 1.33  2001/10/23 01:00:18  richard
1202 # Re-enabled login and registration access after lopping them off via
1203 # disabling access for anonymous users.
1204 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1205 # a couple of bugs while I was there. Probably introduced a couple, but
1206 # things seem to work OK at the moment.
1208 # Revision 1.32  2001/10/22 03:25:01  richard
1209 # Added configuration for:
1210 #  . anonymous user access and registration (deny/allow)
1211 #  . filter "widget" location on index page (top, bottom, both)
1212 # Updated some documentation.
1214 # Revision 1.31  2001/10/21 07:26:35  richard
1215 # feature #473127: Filenames. I modified the file.index and htmltemplate
1216 #  source so that the filename is used in the link and the creation
1217 #  information is displayed.
1219 # Revision 1.30  2001/10/21 04:44:50  richard
1220 # bug #473124: UI inconsistency with Link fields.
1221 #    This also prompted me to fix a fairly long-standing usability issue -
1222 #    that of being able to turn off certain filters.
1224 # Revision 1.29  2001/10/21 00:17:56  richard
1225 # CGI interface view customisation section may now be hidden (patch from
1226 #  Roch'e Compaan.)
1228 # Revision 1.28  2001/10/21 00:00:16  richard
1229 # Fixed Checklist function - wasn't always working on a list.
1231 # Revision 1.27  2001/10/20 12:13:44  richard
1232 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1234 # Revision 1.26  2001/10/14 10:55:00  richard
1235 # Handle empty strings in HTML template Link function
1237 # Revision 1.25  2001/10/09 07:25:59  richard
1238 # Added the Password property type. See "pydoc roundup.password" for
1239 # implementation details. Have updated some of the documentation too.
1241 # Revision 1.24  2001/09/27 06:45:58  richard
1242 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1243 # on the plain() template function to escape the text for HTML.
1245 # Revision 1.23  2001/09/10 09:47:18  richard
1246 # Fixed bug in the generation of links to Link/Multilink in indexes.
1247 #   (thanks Hubert Hoegl)
1248 # Added AssignedTo to the "classic" schema's item page.
1250 # Revision 1.22  2001/08/30 06:01:17  richard
1251 # Fixed missing import in mailgw :(
1253 # Revision 1.21  2001/08/16 07:34:59  richard
1254 # better CGI text searching - but hidden filter fields are disappearing...
1256 # Revision 1.20  2001/08/15 23:43:18  richard
1257 # Fixed some isFooTypes that I missed.
1258 # Refactored some code in the CGI code.
1260 # Revision 1.19  2001/08/12 06:32:36  richard
1261 # using isinstance(blah, Foo) now instead of isFooType
1263 # Revision 1.18  2001/08/07 00:24:42  richard
1264 # stupid typo
1266 # Revision 1.17  2001/08/07 00:15:51  richard
1267 # Added the copyright/license notice to (nearly) all files at request of
1268 # Bizar Software.
1270 # Revision 1.16  2001/08/01 03:52:23  richard
1271 # Checklist was using wrong name.
1273 # Revision 1.15  2001/07/30 08:12:17  richard
1274 # Added time logging and file uploading to the templates.
1276 # Revision 1.14  2001/07/30 06:17:45  richard
1277 # Features:
1278 #  . Added ability for cgi newblah forms to indicate that the new node
1279 #    should be linked somewhere.
1280 # Fixed:
1281 #  . Fixed the agument handling for the roundup-admin find command.
1282 #  . Fixed handling of summary when no note supplied for newblah. Again.
1283 #  . Fixed detection of no form in htmltemplate Field display.
1285 # Revision 1.13  2001/07/30 02:37:53  richard
1286 # Temporary measure until we have decent schema migration.
1288 # Revision 1.12  2001/07/30 01:24:33  richard
1289 # Handles new node display now.
1291 # Revision 1.11  2001/07/29 09:31:35  richard
1292 # oops
1294 # Revision 1.10  2001/07/29 09:28:23  richard
1295 # Fixed sorting by clicking on column headings.
1297 # Revision 1.9  2001/07/29 08:27:40  richard
1298 # Fixed handling of passed-in values in form elements (ie. during a
1299 # drill-down)
1301 # Revision 1.8  2001/07/29 07:01:39  richard
1302 # Added vim command to all source so that we don't get no steenkin' tabs :)
1304 # Revision 1.7  2001/07/29 05:36:14  richard
1305 # Cleanup of the link label generation.
1307 # Revision 1.6  2001/07/29 04:06:42  richard
1308 # Fixed problem in link display when Link value is None.
1310 # Revision 1.5  2001/07/28 08:17:09  richard
1311 # fixed use of stylesheet
1313 # Revision 1.4  2001/07/28 07:59:53  richard
1314 # Replaced errno integers with their module values.
1315 # De-tabbed templatebuilder.py
1317 # Revision 1.3  2001/07/25 03:39:47  richard
1318 # Hrm - displaying links to classes that don't specify a key property. I've
1319 # got it defaulting to 'name', then 'title' and then a "random" property (first
1320 # one returned by getprops().keys().
1321 # Needs to be moved onto the Class I think...
1323 # Revision 1.2  2001/07/22 12:09:32  richard
1324 # Final commit of Grande Splite
1326 # Revision 1.1  2001/07/22 11:58:35  richard
1327 # More Grande Splite
1330 # vim: set filetype=python ts=4 sw=4 et si