Code

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