Code

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