Code

"except:" is bad, bad , bad!
[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.46 2001-11-24 00:53:12 jhermann Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno
26 import hyperdb, date, password
28 # This imports the StructureText functionality for the do_stext function
29 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
30 try:
31     from StructuredText.StructuredText import HTML as StructuredText
32 except ImportError:
33     StructuredText = None
35 class TemplateFunctions:
36     def __init__(self):
37         self.form = None
38         self.nodeid = None
39         self.filterspec = None
40         self.globals = {}
41         for key in TemplateFunctions.__dict__.keys():
42             if key[:3] == 'do_':
43                 self.globals[key[3:]] = getattr(self, key)
45     def do_plain(self, property, escape=0):
46         ''' display a String property directly;
48             display a Date property in a specified time zone with an option to
49             omit the time from the date stamp;
51             for a Link or Multilink property, display the key strings of the
52             linked nodes (or the ids if the linked class has no key property)
53         '''
54         if not self.nodeid and self.form is None:
55             return '[Field: not called from item]'
56         propclass = self.properties[property]
57         if self.nodeid:
58             # make sure the property is a valid one
59             # TODO: this tests, but we should handle the exception
60             prop_test = self.cl.getprops()[property]
62             # get the value for this property
63             try:
64                 value = self.cl.get(self.nodeid, property)
65             except KeyError:
66                 # a KeyError here means that the node doesn't have a value
67                 # for the specified property
68                 if isinstance(propclass, hyperdb.Multilink): value = []
69                 else: value = ''
70         else:
71             # TODO: pull the value from the form
72             if isinstance(propclass, hyperdb.Multilink): value = []
73             else: value = ''
74         if isinstance(propclass, hyperdb.String):
75             if value is None: value = ''
76             else: value = str(value)
77         elif isinstance(propclass, hyperdb.Password):
78             if value is None: value = ''
79             else: value = '*encrypted*'
80         elif isinstance(propclass, hyperdb.Date):
81             value = str(value)
82         elif isinstance(propclass, hyperdb.Interval):
83             value = str(value)
84         elif isinstance(propclass, hyperdb.Link):
85             linkcl = self.db.classes[propclass.classname]
86             k = linkcl.labelprop()
87             if value: value = str(linkcl.get(value, k))
88             else: value = '[unselected]'
89         elif isinstance(propclass, hyperdb.Multilink):
90             linkcl = self.db.classes[propclass.classname]
91             k = linkcl.labelprop()
92             value = ', '.join([linkcl.get(i, k) for i in value])
93         else:
94             s = _('Plain: bad propclass "%(propclass)s"')%locals()
95         if escape:
96             value = cgi.escape(value)
97         return value
99     def do_stext(self, property, escape=0):
100         '''Render as structured text using the StructuredText module
101            (see above for details)
102         '''
103         s = self.do_plain(property, escape=escape)
104         if not StructuredText:
105             return s
106         return StructuredText(s,level=1,header=0)
108     def do_field(self, property, size=None, height=None, showid=0):
109         ''' display a property like the plain displayer, but in a text field
110             to be edited
111         '''
112         if not self.nodeid and self.form is None and self.filterspec is None:
113             return _('[Field: not called from item]')
114         propclass = self.properties[property]
115         if self.nodeid:
116             value = self.cl.get(self.nodeid, property, None)
117             # TODO: remove this from the code ... it's only here for
118             # handling schema changes, and they should be handled outside
119             # of this code...
120             if isinstance(propclass, hyperdb.Multilink) and value is None:
121                 value = []
122         elif self.filterspec is not None:
123             if isinstance(propclass, hyperdb.Multilink):
124                 value = self.filterspec.get(property, [])
125             else:
126                 value = self.filterspec.get(property, '')
127         else:
128             # TODO: pull the value from the form
129             if isinstance(propclass, hyperdb.Multilink): value = []
130             else: value = ''
131         if (isinstance(propclass, hyperdb.String) or
132                 isinstance(propclass, hyperdb.Date) or
133                 isinstance(propclass, hyperdb.Interval)):
134             size = size or 30
135             if value is None:
136                 value = ''
137             else:
138                 value = cgi.escape(value)
139                 value = '"'.join(value.split('"'))
140             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
141         elif isinstance(propclass, hyperdb.Password):
142             size = size or 30
143             s = '<input type="password" name="%s" size="%s">'%(property, size)
144         elif isinstance(propclass, hyperdb.Link):
145             linkcl = self.db.classes[propclass.classname]
146             l = ['<select name="%s">'%property]
147             k = linkcl.labelprop()
148             if value is None:
149                 s = 'selected '
150             else:
151                 s = ''
152             l.append('<option %svalue="-1">- no selection -</option>'%s)
153             for optionid in linkcl.list():
154                 option = linkcl.get(optionid, k)
155                 s = ''
156                 if optionid == value:
157                     s = 'selected '
158                 if showid:
159                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
160                 else:
161                     lab = option
162                 if size is not None and len(lab) > size:
163                     lab = lab[:size-3] + '...'
164                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
165             l.append('</select>')
166             s = '\n'.join(l)
167         elif isinstance(propclass, hyperdb.Multilink):
168             linkcl = self.db.classes[propclass.classname]
169             list = linkcl.list()
170             height = height or min(len(list), 7)
171             l = ['<select multiple name="%s" size="%s">'%(property, height)]
172             k = linkcl.labelprop()
173             for optionid in list:
174                 option = linkcl.get(optionid, k)
175                 s = ''
176                 if optionid in value:
177                     s = 'selected '
178                 if showid:
179                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
180                 else:
181                     lab = option
182                 if size is not None and len(lab) > size:
183                     lab = lab[:size-3] + '...'
184                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
185             l.append('</select>')
186             s = '\n'.join(l)
187         else:
188             s = 'Plain: bad propclass "%s"'%propclass
189         return s
191     def do_menu(self, property, size=None, height=None, showid=0):
192         ''' for a Link property, display a menu of the available choices
193         '''
194         propclass = self.properties[property]
195         if self.nodeid:
196             value = self.cl.get(self.nodeid, property)
197         else:
198             # TODO: pull the value from the form
199             if isinstance(propclass, hyperdb.Multilink): value = []
200             else: value = None
201         if isinstance(propclass, hyperdb.Link):
202             linkcl = self.db.classes[propclass.classname]
203             l = ['<select name="%s">'%property]
204             k = linkcl.labelprop()
205             s = ''
206             if value is None:
207                 s = 'selected '
208             l.append('<option %svalue="-1">- no selection -</option>'%s)
209             for optionid in linkcl.list():
210                 option = linkcl.get(optionid, k)
211                 s = ''
212                 if optionid == value:
213                     s = 'selected '
214                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
215             l.append('</select>')
216             return '\n'.join(l)
217         if isinstance(propclass, hyperdb.Multilink):
218             linkcl = self.db.classes[propclass.classname]
219             list = linkcl.list()
220             height = height or min(len(list), 7)
221             l = ['<select multiple name="%s" size="%s">'%(property, height)]
222             k = linkcl.labelprop()
223             for optionid in list:
224                 option = linkcl.get(optionid, k)
225                 s = ''
226                 if optionid in value:
227                     s = 'selected '
228                 if showid:
229                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
230                 else:
231                     lab = option
232                 if size is not None and len(lab) > size:
233                     lab = lab[:size-3] + '...'
234                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
235             l.append('</select>')
236             return '\n'.join(l)
237         return '[Menu: not a link]'
239     #XXX deviates from spec
240     def do_link(self, property=None, is_download=0):
241         '''For a Link or Multilink property, display the names of the linked
242            nodes, hyperlinked to the item views on those nodes.
243            For other properties, link to this node with the property as the
244            text.
246            If is_download is true, append the property value to the generated
247            URL so that the link may be used as a download link and the
248            downloaded file name is correct.
249         '''
250         if not self.nodeid and self.form is None:
251             return '[Link: not called from item]'
252         propclass = self.properties[property]
253         if self.nodeid:
254             value = self.cl.get(self.nodeid, property)
255         else:
256             if isinstance(propclass, hyperdb.Multilink): value = []
257             elif isinstance(propclass, hyperdb.Link): value = None
258             else: value = ''
259         if isinstance(propclass, hyperdb.Link):
260             linkname = propclass.classname
261             if value is None: return '[no %s]'%property.capitalize()
262             linkcl = self.db.classes[linkname]
263             k = linkcl.labelprop()
264             linkvalue = linkcl.get(value, k)
265             if is_download:
266                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
267                     linkvalue, linkvalue)
268             else:
269                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
270         if isinstance(propclass, hyperdb.Multilink):
271             linkname = propclass.classname
272             linkcl = self.db.classes[linkname]
273             k = linkcl.labelprop()
274             if not value : return '[no %s]'%property.capitalize()
275             l = []
276             for value in value:
277                 linkvalue = linkcl.get(value, k)
278                 if is_download:
279                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
280                         linkvalue, linkvalue))
281                 else:
282                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
283                         linkvalue))
284             return ', '.join(l)
285         if isinstance(propclass, hyperdb.String):
286             if value == '': value = '[no %s]'%property.capitalize()
287         if is_download:
288             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
289                 value, value)
290         else:
291             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
293     def do_count(self, property, **args):
294         ''' for a Multilink property, display a count of the number of links in
295             the list
296         '''
297         if not self.nodeid:
298             return '[Count: not called from item]'
299         propclass = self.properties[property]
300         value = self.cl.get(self.nodeid, property)
301         if isinstance(propclass, hyperdb.Multilink):
302             return str(len(value))
303         return '[Count: not a Multilink]'
305     # XXX pretty is definitely new ;)
306     def do_reldate(self, property, pretty=0):
307         ''' display a Date property in terms of an interval relative to the
308             current date (e.g. "+ 3w", "- 2d").
310             with the 'pretty' flag, make it pretty
311         '''
312         if not self.nodeid and self.form is None:
313             return '[Reldate: not called from item]'
314         propclass = self.properties[property]
315         if isinstance(not propclass, hyperdb.Date):
316             return '[Reldate: not a Date]'
317         if self.nodeid:
318             value = self.cl.get(self.nodeid, property)
319         else:
320             value = date.Date('.')
321         interval = value - date.Date('.')
322         if pretty:
323             if not self.nodeid:
324                 return 'now'
325             pretty = interval.pretty()
326             if pretty is None:
327                 pretty = value.pretty()
328             return pretty
329         return str(interval)
331     def do_download(self, property, **args):
332         ''' show a Link("file") or Multilink("file") property using links that
333             allow you to download files
334         '''
335         if not self.nodeid:
336             return '[Download: not called from item]'
337         propclass = self.properties[property]
338         value = self.cl.get(self.nodeid, property)
339         if isinstance(propclass, hyperdb.Link):
340             linkcl = self.db.classes[propclass.classname]
341             linkvalue = linkcl.get(value, k)
342             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
343         if isinstance(propclass, hyperdb.Multilink):
344             linkcl = self.db.classes[propclass.classname]
345             l = []
346             for value in value:
347                 linkvalue = linkcl.get(value, k)
348                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
349             return ', '.join(l)
350         return '[Download: not a link]'
353     def do_checklist(self, property, **args):
354         ''' for a Link or Multilink property, display checkboxes for the
355             available choices to permit filtering
356         '''
357         propclass = self.properties[property]
358         if (not isinstance(propclass, hyperdb.Link) and not
359                 isinstance(propclass, hyperdb.Multilink)):
360             return '[Checklist: not a link]'
362         # get our current checkbox state
363         if self.nodeid:
364             # get the info from the node - make sure it's a list
365             if isinstance(propclass, hyperdb.Link):
366                 value = [self.cl.get(self.nodeid, property)]
367             else:
368                 value = self.cl.get(self.nodeid, property)
369         elif self.filterspec is not None:
370             # get the state from the filter specification (always a list)
371             value = self.filterspec.get(property, [])
372         else:
373             # it's a new node, so there's no state
374             value = []
376         # so we can map to the linked node's "lable" property
377         linkcl = self.db.classes[propclass.classname]
378         l = []
379         k = linkcl.labelprop()
380         for optionid in linkcl.list():
381             option = linkcl.get(optionid, k)
382             if optionid in value or option in value:
383                 checked = 'checked'
384             else:
385                 checked = ''
386             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
387                 option, checked, property, option))
389         # for Links, allow the "unselected" option too
390         if isinstance(propclass, hyperdb.Link):
391             if value is None or '-1' in value:
392                 checked = 'checked'
393             else:
394                 checked = ''
395             l.append('[unselected]:<input type="checkbox" %s name="%s" '
396                 'value="-1">'%(checked, property))
397         return '\n'.join(l)
399     def do_note(self, rows=5, cols=80):
400         ''' display a "note" field, which is a text area for entering a note to
401             go along with a change. 
402         '''
403         # TODO: pull the value from the form
404         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
405             '</textarea>'%(rows, cols)
407     # XXX new function
408     def do_list(self, property, reverse=0):
409         ''' list the items specified by property using the standard index for
410             the class
411         '''
412         propcl = self.properties[property]
413         if not isinstance(propcl, hyperdb.Multilink):
414             return '[List: not a Multilink]'
415         value = self.cl.get(self.nodeid, property)
416         if reverse:
417             value.reverse()
419         # render the sub-index into a string
420         fp = StringIO.StringIO()
421         try:
422             write_save = self.client.write
423             self.client.write = fp.write
424             index = IndexTemplate(self.client, self.templates, propcl.classname)
425             index.render(nodeids=value, show_display_form=0)
426         finally:
427             self.client.write = write_save
429         return fp.getvalue()
431     # XXX new function
432     def do_history(self, **args):
433         ''' list the history of the item
434         '''
435         if self.nodeid is None:
436             return "[History: node doesn't exist]"
438         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
439             '<tr class="list-header">',
440             '<td><span class="list-item"><strong>Date</strong></span></td>',
441             '<td><span class="list-item"><strong>User</strong></span></td>',
442             '<td><span class="list-item"><strong>Action</strong></span></td>',
443             '<td><span class="list-item"><strong>Args</strong></span></td>']
445         for id, date, user, action, args in self.cl.history(self.nodeid):
446             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
447                date, user, action, args))
448         l.append('</table>')
449         return '\n'.join(l)
451     # XXX new function
452     def do_submit(self):
453         ''' add a submit button for the item
454         '''
455         if self.nodeid:
456             return '<input type="submit" value="Submit Changes">'
457         elif self.form is not None:
458             return '<input type="submit" value="Submit New Entry">'
459         else:
460             return '[Submit: not called from item]'
464 #   INDEX TEMPLATES
466 class IndexTemplateReplace:
467     def __init__(self, globals, locals, props):
468         self.globals = globals
469         self.locals = locals
470         self.props = props
472     replace=re.compile(
473         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
474         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
475     def go(self, text):
476         return self.replace.sub(self, text)
478     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
479         if m.group('name'):
480             if m.group('name') in self.props:
481                 text = m.group('text')
482                 replace = IndexTemplateReplace(self.globals, {}, self.props)
483                 return replace.go(m.group('text'))
484             else:
485                 return ''
486         if m.group('display'):
487             command = m.group('command')
488             return eval(command, self.globals, self.locals)
489         print '*** unhandled match', m.groupdict()
491 class IndexTemplate(TemplateFunctions):
492     def __init__(self, client, templates, classname):
493         self.client = client
494         self.templates = templates
495         self.classname = classname
497         # derived
498         self.db = self.client.db
499         self.cl = self.db.classes[self.classname]
500         self.properties = self.cl.getprops()
502         TemplateFunctions.__init__(self)
504     col_re=re.compile(r'<property\s+name="([^>]+)">')
505     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
506             show_display_form=1, nodeids=None, show_customization=1):
507         self.filterspec = filterspec
509         w = self.client.write
511         # get the filter template
512         try:
513             filter_template = open(os.path.join(self.templates,
514                 self.classname+'.filter')).read()
515             all_filters = self.col_re.findall(filter_template)
516         except IOError, error:
517             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
518             filter_template = None
519             all_filters = []
521         # XXX deviate from spec here ...
522         # load the index section template and figure the default columns from it
523         template = open(os.path.join(self.templates,
524             self.classname+'.index')).read()
525         all_columns = self.col_re.findall(template)
526         if not columns:
527             columns = []
528             for name in all_columns:
529                 columns.append(name)
530         else:
531             # re-sort columns to be the same order as all_columns
532             l = []
533             for name in all_columns:
534                 if name in columns:
535                     l.append(name)
536             columns = l
538         # display the filter section
539         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
540                 self.client.FILTER_POSITION in ('top and bottom', 'top')):
541             w('<form action="index">\n')
542             self.filter_section(filter_template, filter, columns, group,
543                 all_filters, all_columns, show_customization)
544             # make sure that the sorting doesn't get lost either
545             if sort:
546                 w('<input type="hidden" name=":sort" value="%s">'%
547                     ','.join(sort))
548             w('</form>\n')
551         # now display the index section
552         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
553         w('<tr class="list-header">\n')
554         for name in columns:
555             cname = name.capitalize()
556             if show_display_form:
557                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
558                 anchor = "%s?%s"%(self.classname, sb)
559                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
560                     anchor, cname))
561             else:
562                 w('<td><span class="list-header">%s</span></td>\n'%cname)
563         w('</tr>\n')
565         # this stuff is used for group headings - optimise the group names
566         old_group = None
567         group_names = []
568         if group:
569             for name in group:
570                 if name[0] == '-': group_names.append(name[1:])
571                 else: group_names.append(name)
573         # now actually loop through all the nodes we get from the filter and
574         # apply the template
575         if nodeids is None:
576             nodeids = self.cl.filter(filterspec, sort, group)
577         for nodeid in nodeids:
578             # check for a group heading
579             if group_names:
580                 this_group = [self.cl.get(nodeid, name, '[no value]') for name in group_names]
581                 if this_group != old_group:
582                     l = []
583                     for name in group_names:
584                         prop = self.properties[name]
585                         if isinstance(prop, hyperdb.Link):
586                             group_cl = self.db.classes[prop.classname]
587                             key = group_cl.getkey()
588                             value = self.cl.get(nodeid, name)
589                             if value is None:
590                                 l.append('[unselected %s]'%prop.classname)
591                             else:
592                                 l.append(group_cl.get(self.cl.get(nodeid,
593                                     name), key))
594                         elif isinstance(prop, hyperdb.Multilink):
595                             group_cl = self.db.classes[prop.classname]
596                             key = group_cl.getkey()
597                             for value in self.cl.get(nodeid, name):
598                                 l.append(group_cl.get(value, key))
599                         else:
600                             value = self.cl.get(nodeid, name, '[no value]')
601                             if value is None:
602                                 value = '[empty %s]'%name
603                             else:
604                                 value = str(value)
605                             l.append(value)
606                     w('<tr class="section-bar">'
607                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
608                         len(columns), ', '.join(l)))
609                     old_group = this_group
611             # display this node's row
612             replace = IndexTemplateReplace(self.globals, locals(), columns)
613             self.nodeid = nodeid
614             w(replace.go(template))
615             self.nodeid = None
617         w('</table>')
619         # display the filter section
620         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
621                 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
622             w('<form action="index">\n')
623             self.filter_section(filter_template, filter, columns, group,
624                 all_filters, all_columns, show_customization)
625             # make sure that the sorting doesn't get lost either
626             if sort:
627                 w('<input type="hidden" name=":sort" value="%s">'%
628                     ','.join(sort))
629             w('</form>\n')
632     def filter_section(self, template, filter, columns, group, all_filters,
633             all_columns, show_customization):
635         w = self.client.write
637         if template and filter:
638             # display the filter section
639             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
640             w('<tr class="location-bar">')
641             w(' <th align="left" colspan="2">Filter specification...</th>')
642             w('</tr>')
643             replace = IndexTemplateReplace(self.globals, locals(), filter)
644             w(replace.go(template))
645             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
646             w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
647             w('</table>')
649         # now add in the filter/columns/group/etc config table form
650         w('<input type="hidden" name="show_customization" value="%s">' %
651             show_customization )
652         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
653         names = []
654         for name in self.properties.keys():
655             if name in all_filters or name in all_columns:
656                 names.append(name)
657         if show_customization:
658             action = '-'
659         else:
660             action = '+'
661             # hide the values for filters, columns and grouping in the form
662             # if the customization widget is not visible
663             for name in names:
664                 if all_filters and name in filter:
665                     w('<input type="hidden" name=":filter" value="%s">' % name)
666                 if all_columns and name in columns:
667                     w('<input type="hidden" name=":columns" value="%s">' % name)
668                 if all_columns and name in group:
669                     w('<input type="hidden" name=":group" value="%s">' % name)
671         # TODO: The widget style can go into the stylesheet
672         w('<th align="left" colspan=%s>'
673           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
674           'customisation...</th></tr>\n'%(len(names)+1, action))
676         if not show_customization:
677             w('</table>\n')
678             return
680         w('<tr class="location-bar"><th>&nbsp;</th>')
681         for name in names:
682             w('<th>%s</th>'%name.capitalize())
683         w('</tr>\n')
685         # Filter
686         if all_filters:
687             w('<tr><th width="1%" align=right class="location-bar">'
688               'Filters</th>\n')
689             for name in names:
690                 if name not in all_filters:
691                     w('<td>&nbsp;</td>')
692                     continue
693                 if name in filter: checked=' checked'
694                 else: checked=''
695                 w('<td align=middle>\n')
696                 w(' <input type="checkbox" name=":filter" value="%s" '
697                   '%s></td>\n'%(name, checked))
698             w('</tr>\n')
700         # Columns
701         if all_columns:
702             w('<tr><th width="1%" align=right class="location-bar">'
703               'Columns</th>\n')
704             for name in names:
705                 if name not in all_columns:
706                     w('<td>&nbsp;</td>')
707                     continue
708                 if name in columns: checked=' checked'
709                 else: checked=''
710                 w('<td align=middle>\n')
711                 w(' <input type="checkbox" name=":columns" value="%s"'
712                   '%s></td>\n'%(name, checked))
713             w('</tr>\n')
715             # Grouping
716             w('<tr><th width="1%" align=right class="location-bar">'
717               'Grouping</th>\n')
718             for name in names:
719                 prop = self.properties[name]
720                 if name not in all_columns:
721                     w('<td>&nbsp;</td>')
722                     continue
723                 if name in group: checked=' checked'
724                 else: checked=''
725                 w('<td align=middle>\n')
726                 w(' <input type="checkbox" name=":group" value="%s"'
727                   '%s></td>\n'%(name, checked))
728             w('</tr>\n')
730         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
731         w('<td colspan="%s">'%len(names))
732         w('<input type="submit" name="action" value="Redisplay"></td>')
733         w('</tr>\n')
734         w('</table>\n')
737     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
738         l = []
739         w = l.append
740         for k, v in filterspec.items():
741             k = urllib.quote(k)
742             if type(v) == type([]):
743                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
744             else:
745                 w('%s=%s'%(k, urllib.quote(v)))
746         if columns:
747             w(':columns=%s'%','.join(map(urllib.quote, columns)))
748         if filter:
749             w(':filter=%s'%','.join(map(urllib.quote, filter)))
750         if group:
751             w(':group=%s'%','.join(map(urllib.quote, group)))
752         m = []
753         s_dir = ''
754         for name in sort:
755             dir = name[0]
756             if dir == '-':
757                 name = name[1:]
758             else:
759                 dir = ''
760             if sort_name == name:
761                 if dir == '-':
762                     s_dir = ''
763                 else:
764                     s_dir = '-'
765             else:
766                 m.append(dir+urllib.quote(name))
767         m.insert(0, s_dir+urllib.quote(sort_name))
768         # so things don't get completely out of hand, limit the sort to
769         # two columns
770         w(':sort=%s'%','.join(m[:2]))
771         return '&'.join(l)
774 #   ITEM TEMPLATES
776 class ItemTemplateReplace:
777     def __init__(self, globals, locals, cl, nodeid):
778         self.globals = globals
779         self.locals = locals
780         self.cl = cl
781         self.nodeid = nodeid
783     replace=re.compile(
784         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
785         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
786     def go(self, text):
787         return self.replace.sub(self, text)
789     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
790         if m.group('name'):
791             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
792                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
793                     self.nodeid)
794                 return replace.go(m.group('text'))
795             else:
796                 return ''
797         if m.group('display'):
798             command = m.group('command')
799             return eval(command, self.globals, self.locals)
800         print '*** unhandled match', m.groupdict()
803 class ItemTemplate(TemplateFunctions):
804     def __init__(self, client, templates, classname):
805         self.client = client
806         self.templates = templates
807         self.classname = classname
809         # derived
810         self.db = self.client.db
811         self.cl = self.db.classes[self.classname]
812         self.properties = self.cl.getprops()
814         TemplateFunctions.__init__(self)
816     def render(self, nodeid):
817         self.nodeid = nodeid
819         if (self.properties.has_key('type') and
820                 self.properties.has_key('content')):
821             pass
822             # XXX we really want to return this as a downloadable...
823             #  currently I handle this at a higher level by detecting 'file'
824             #  designators...
826         w = self.client.write
827         w('<form action="%s%s">'%(self.classname, nodeid))
828         s = open(os.path.join(self.templates, self.classname+'.item')).read()
829         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
830         w(replace.go(s))
831         w('</form>')
834 class NewItemTemplate(TemplateFunctions):
835     def __init__(self, client, templates, classname):
836         self.client = client
837         self.templates = templates
838         self.classname = classname
840         # derived
841         self.db = self.client.db
842         self.cl = self.db.classes[self.classname]
843         self.properties = self.cl.getprops()
845         TemplateFunctions.__init__(self)
847     def render(self, form):
848         self.form = form
849         w = self.client.write
850         c = self.classname
851         try:
852             s = open(os.path.join(self.templates, c+'.newitem')).read()
853         except IOError:
854             s = open(os.path.join(self.templates, c+'.item')).read()
855         w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
856         for key in form.keys():
857             if key[0] == ':':
858                 value = form[key].value
859                 if type(value) != type([]): value = [value]
860                 for value in value:
861                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
862         replace = ItemTemplateReplace(self.globals, locals(), None, None)
863         w(replace.go(s))
864         w('</form>')
867 # $Log: not supported by cvs2svn $
868 # Revision 1.45  2001/11/22 15:46:42  jhermann
869 # Added module docstrings to all modules.
871 # Revision 1.44  2001/11/21 23:35:45  jhermann
872 # Added globbing for win32, and sample marking in a 2nd file to test it
874 # Revision 1.43  2001/11/21 04:04:43  richard
875 # *sigh* more missing value handling
877 # Revision 1.42  2001/11/21 03:40:54  richard
878 # more new property handling
880 # Revision 1.41  2001/11/15 10:26:01  richard
881 #  . missing "return" in filter_section (thanks Roch'e Compaan)
883 # Revision 1.40  2001/11/03 01:56:51  richard
884 # More HTML compliance fixes. This will probably fix the Netscape problem
885 # too.
887 # Revision 1.39  2001/11/03 01:43:47  richard
888 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
890 # Revision 1.38  2001/10/31 06:58:51  richard
891 # Added the wrap="hard" attribute to the textarea of the note field so the
892 # messages wrap sanely.
894 # Revision 1.37  2001/10/31 06:24:35  richard
895 # Added do_stext to htmltemplate, thanks Brad Clements.
897 # Revision 1.36  2001/10/28 22:51:38  richard
898 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
900 # Revision 1.35  2001/10/24 00:04:41  richard
901 # Removed the "infinite authentication loop", thanks Roch'e
903 # Revision 1.34  2001/10/23 22:56:36  richard
904 # Bugfix in filter "widget" placement, thanks Roch'e
906 # Revision 1.33  2001/10/23 01:00:18  richard
907 # Re-enabled login and registration access after lopping them off via
908 # disabling access for anonymous users.
909 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
910 # a couple of bugs while I was there. Probably introduced a couple, but
911 # things seem to work OK at the moment.
913 # Revision 1.32  2001/10/22 03:25:01  richard
914 # Added configuration for:
915 #  . anonymous user access and registration (deny/allow)
916 #  . filter "widget" location on index page (top, bottom, both)
917 # Updated some documentation.
919 # Revision 1.31  2001/10/21 07:26:35  richard
920 # feature #473127: Filenames. I modified the file.index and htmltemplate
921 #  source so that the filename is used in the link and the creation
922 #  information is displayed.
924 # Revision 1.30  2001/10/21 04:44:50  richard
925 # bug #473124: UI inconsistency with Link fields.
926 #    This also prompted me to fix a fairly long-standing usability issue -
927 #    that of being able to turn off certain filters.
929 # Revision 1.29  2001/10/21 00:17:56  richard
930 # CGI interface view customisation section may now be hidden (patch from
931 #  Roch'e Compaan.)
933 # Revision 1.28  2001/10/21 00:00:16  richard
934 # Fixed Checklist function - wasn't always working on a list.
936 # Revision 1.27  2001/10/20 12:13:44  richard
937 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
939 # Revision 1.26  2001/10/14 10:55:00  richard
940 # Handle empty strings in HTML template Link function
942 # Revision 1.25  2001/10/09 07:25:59  richard
943 # Added the Password property type. See "pydoc roundup.password" for
944 # implementation details. Have updated some of the documentation too.
946 # Revision 1.24  2001/09/27 06:45:58  richard
947 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
948 # on the plain() template function to escape the text for HTML.
950 # Revision 1.23  2001/09/10 09:47:18  richard
951 # Fixed bug in the generation of links to Link/Multilink in indexes.
952 #   (thanks Hubert Hoegl)
953 # Added AssignedTo to the "classic" schema's item page.
955 # Revision 1.22  2001/08/30 06:01:17  richard
956 # Fixed missing import in mailgw :(
958 # Revision 1.21  2001/08/16 07:34:59  richard
959 # better CGI text searching - but hidden filter fields are disappearing...
961 # Revision 1.20  2001/08/15 23:43:18  richard
962 # Fixed some isFooTypes that I missed.
963 # Refactored some code in the CGI code.
965 # Revision 1.19  2001/08/12 06:32:36  richard
966 # using isinstance(blah, Foo) now instead of isFooType
968 # Revision 1.18  2001/08/07 00:24:42  richard
969 # stupid typo
971 # Revision 1.17  2001/08/07 00:15:51  richard
972 # Added the copyright/license notice to (nearly) all files at request of
973 # Bizar Software.
975 # Revision 1.16  2001/08/01 03:52:23  richard
976 # Checklist was using wrong name.
978 # Revision 1.15  2001/07/30 08:12:17  richard
979 # Added time logging and file uploading to the templates.
981 # Revision 1.14  2001/07/30 06:17:45  richard
982 # Features:
983 #  . Added ability for cgi newblah forms to indicate that the new node
984 #    should be linked somewhere.
985 # Fixed:
986 #  . Fixed the agument handling for the roundup-admin find command.
987 #  . Fixed handling of summary when no note supplied for newblah. Again.
988 #  . Fixed detection of no form in htmltemplate Field display.
990 # Revision 1.13  2001/07/30 02:37:53  richard
991 # Temporary measure until we have decent schema migration.
993 # Revision 1.12  2001/07/30 01:24:33  richard
994 # Handles new node display now.
996 # Revision 1.11  2001/07/29 09:31:35  richard
997 # oops
999 # Revision 1.10  2001/07/29 09:28:23  richard
1000 # Fixed sorting by clicking on column headings.
1002 # Revision 1.9  2001/07/29 08:27:40  richard
1003 # Fixed handling of passed-in values in form elements (ie. during a
1004 # drill-down)
1006 # Revision 1.8  2001/07/29 07:01:39  richard
1007 # Added vim command to all source so that we don't get no steenkin' tabs :)
1009 # Revision 1.7  2001/07/29 05:36:14  richard
1010 # Cleanup of the link label generation.
1012 # Revision 1.6  2001/07/29 04:06:42  richard
1013 # Fixed problem in link display when Link value is None.
1015 # Revision 1.5  2001/07/28 08:17:09  richard
1016 # fixed use of stylesheet
1018 # Revision 1.4  2001/07/28 07:59:53  richard
1019 # Replaced errno integers with their module values.
1020 # De-tabbed templatebuilder.py
1022 # Revision 1.3  2001/07/25 03:39:47  richard
1023 # Hrm - displaying links to classes that don't specify a key property. I've
1024 # got it defaulting to 'name', then 'title' and then a "random" property (first
1025 # one returned by getprops().keys().
1026 # Needs to be moved onto the Class I think...
1028 # Revision 1.2  2001/07/22 12:09:32  richard
1029 # Final commit of Grande Splite
1031 # Revision 1.1  2001/07/22 11:58:35  richard
1032 # More Grande Splite
1035 # vim: set filetype=python ts=4 sw=4 et si