Code

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