Code

8892dd9d5f21dad012883c7264000eae5ee7afdc
[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.51 2002-01-10 10:02:15 grubert 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(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" value="Submit Changes">')
472         elif self.form is not None:
473             return _('<input type="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.templates = templates
510         self.classname = classname
512         # derived
513         self.db = self.client.db
514         self.cl = self.db.classes[self.classname]
515         self.properties = self.cl.getprops()
517         TemplateFunctions.__init__(self)
519     col_re=re.compile(r'<property\s+name="([^>]+)">')
520     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
521             show_display_form=1, nodeids=None, show_customization=1):
522         self.filterspec = filterspec
524         w = self.client.write
526         # get the filter template
527         try:
528             filter_template = open(os.path.join(self.templates,
529                 self.classname+'.filter')).read()
530             all_filters = self.col_re.findall(filter_template)
531         except IOError, error:
532             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
533             filter_template = None
534             all_filters = []
536         # XXX deviate from spec here ...
537         # load the index section template and figure the default columns from it
538         template = open(os.path.join(self.templates,
539             self.classname+'.index')).read()
540         all_columns = self.col_re.findall(template)
541         if not columns:
542             columns = []
543             for name in all_columns:
544                 columns.append(name)
545         else:
546             # re-sort columns to be the same order as all_columns
547             l = []
548             for name in all_columns:
549                 if name in columns:
550                     l.append(name)
551             columns = l
553         # display the filter section
554         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
555                 self.client.FILTER_POSITION in ('top and bottom', 'top')):
556             w('<form action="index">\n')
557             self.filter_section(filter_template, filter, columns, group,
558                 all_filters, all_columns, show_customization)
559             # make sure that the sorting doesn't get lost either
560             if sort:
561                 w('<input type="hidden" name=":sort" value="%s">'%
562                     ','.join(sort))
563             w('</form>\n')
566         # now display the index section
567         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
568         w('<tr class="list-header">\n')
569         for name in columns:
570             cname = name.capitalize()
571             if show_display_form:
572                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
573                 anchor = "%s?%s"%(self.classname, sb)
574                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
575                     anchor, cname))
576             else:
577                 w('<td><span class="list-header">%s</span></td>\n'%cname)
578         w('</tr>\n')
580         # this stuff is used for group headings - optimise the group names
581         old_group = None
582         group_names = []
583         if group:
584             for name in group:
585                 if name[0] == '-': group_names.append(name[1:])
586                 else: group_names.append(name)
588         # now actually loop through all the nodes we get from the filter and
589         # apply the template
590         if nodeids is None:
591             nodeids = self.cl.filter(filterspec, sort, group)
592         for nodeid in nodeids:
593             # check for a group heading
594             if group_names:
595                 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
596                 if this_group != old_group:
597                     l = []
598                     for name in group_names:
599                         prop = self.properties[name]
600                         if isinstance(prop, hyperdb.Link):
601                             group_cl = self.db.classes[prop.classname]
602                             key = group_cl.getkey()
603                             value = self.cl.get(nodeid, name)
604                             if value is None:
605                                 l.append(_('[unselected %(classname)s]')%{
606                                     'classname': prop.classname})
607                             else:
608                                 l.append(group_cl.get(self.cl.get(nodeid,
609                                     name), key))
610                         elif isinstance(prop, hyperdb.Multilink):
611                             group_cl = self.db.classes[prop.classname]
612                             key = group_cl.getkey()
613                             for value in self.cl.get(nodeid, name):
614                                 l.append(group_cl.get(value, key))
615                         else:
616                             value = self.cl.get(nodeid, name, _('[no value]'))
617                             if value is None:
618                                 value = _('[empty %(name)s]')%locals()
619                             else:
620                                 value = str(value)
621                             l.append(value)
622                     w('<tr class="section-bar">'
623                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
624                         len(columns), ', '.join(l)))
625                     old_group = this_group
627             # display this node's row
628             replace = IndexTemplateReplace(self.globals, locals(), columns)
629             self.nodeid = nodeid
630             w(replace.go(template))
631             self.nodeid = None
633         w('</table>')
635         # display the filter section
636         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
637                 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
638             w('<form action="index">\n')
639             self.filter_section(filter_template, filter, columns, group,
640                 all_filters, all_columns, show_customization)
641             # make sure that the sorting doesn't get lost either
642             if sort:
643                 w('<input type="hidden" name=":sort" value="%s">'%
644                     ','.join(sort))
645             w('</form>\n')
648     def filter_section(self, template, filter, columns, group, all_filters,
649             all_columns, show_customization):
651         w = self.client.write
653         # wrap the template in a single table to ensure the whole widget
654         # is displayed at once
655         w('<table><tr><td>')
657         if template and filter:
658             # display the filter section
659             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
660             w('<tr class="location-bar">')
661             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
662             w('</tr>')
663             replace = IndexTemplateReplace(self.globals, locals(), filter)
664             w(replace.go(template))
665             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
666             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
667             w('</table>')
669         # now add in the filter/columns/group/etc config table form
670         w('<input type="hidden" name="show_customization" value="%s">' %
671             show_customization )
672         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
673         names = []
674         for name in self.properties.keys():
675             if name in all_filters or name in all_columns:
676                 names.append(name)
677         if show_customization:
678             action = '-'
679         else:
680             action = '+'
681             # hide the values for filters, columns and grouping in the form
682             # if the customization widget is not visible
683             for name in names:
684                 if all_filters and name in filter:
685                     w('<input type="hidden" name=":filter" value="%s">' % name)
686                 if all_columns and name in columns:
687                     w('<input type="hidden" name=":columns" value="%s">' % name)
688                 if all_columns and name in group:
689                     w('<input type="hidden" name=":group" value="%s">' % name)
691         # TODO: The widget style can go into the stylesheet
692         w(_('<th align="left" colspan=%s>'
693           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
694           'customisation...</th></tr>\n')%(len(names)+1, action))
696         if not show_customization:
697             w('</table>\n')
698             return
700         w('<tr class="location-bar"><th>&nbsp;</th>')
701         for name in names:
702             w('<th>%s</th>'%name.capitalize())
703         w('</tr>\n')
705         # Filter
706         if all_filters:
707             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
708             for name in names:
709                 if name not in all_filters:
710                     w('<td>&nbsp;</td>')
711                     continue
712                 if name in filter: checked=' checked'
713                 else: checked=''
714                 w('<td align=middle>\n')
715                 w(' <input type="checkbox" name=":filter" value="%s" '
716                   '%s></td>\n'%(name, checked))
717             w('</tr>\n')
719         # Columns
720         if all_columns:
721             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
722             for name in names:
723                 if name not in all_columns:
724                     w('<td>&nbsp;</td>')
725                     continue
726                 if name in columns: checked=' checked'
727                 else: checked=''
728                 w('<td align=middle>\n')
729                 w(' <input type="checkbox" name=":columns" value="%s"'
730                   '%s></td>\n'%(name, checked))
731             w('</tr>\n')
733             # Grouping
734             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
735             for name in names:
736                 prop = self.properties[name]
737                 if name not in all_columns:
738                     w('<td>&nbsp;</td>')
739                     continue
740                 if name in group: checked=' checked'
741                 else: checked=''
742                 w('<td align=middle>\n')
743                 w(' <input type="checkbox" name=":group" value="%s"'
744                   '%s></td>\n'%(name, checked))
745             w('</tr>\n')
747         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
748         w('<td colspan="%s">'%len(names))
749         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
750         w('</tr>\n')
751         w('</table>\n')
753         # and the outer table
754         w('</td></tr></table>')
757     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
758         l = []
759         w = l.append
760         for k, v in filterspec.items():
761             k = urllib.quote(k)
762             if type(v) == type([]):
763                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
764             else:
765                 w('%s=%s'%(k, urllib.quote(v)))
766         if columns:
767             w(':columns=%s'%','.join(map(urllib.quote, columns)))
768         if filter:
769             w(':filter=%s'%','.join(map(urllib.quote, filter)))
770         if group:
771             w(':group=%s'%','.join(map(urllib.quote, group)))
772         m = []
773         s_dir = ''
774         for name in sort:
775             dir = name[0]
776             if dir == '-':
777                 name = name[1:]
778             else:
779                 dir = ''
780             if sort_name == name:
781                 if dir == '-':
782                     s_dir = ''
783                 else:
784                     s_dir = '-'
785             else:
786                 m.append(dir+urllib.quote(name))
787         m.insert(0, s_dir+urllib.quote(sort_name))
788         # so things don't get completely out of hand, limit the sort to
789         # two columns
790         w(':sort=%s'%','.join(m[:2]))
791         return '&'.join(l)
794 #   ITEM TEMPLATES
796 class ItemTemplateReplace:
797     def __init__(self, globals, locals, cl, nodeid):
798         self.globals = globals
799         self.locals = locals
800         self.cl = cl
801         self.nodeid = nodeid
803     replace=re.compile(
804         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
805         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
806     def go(self, text):
807         return self.replace.sub(self, text)
809     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
810         if m.group('name'):
811             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
812                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
813                     self.nodeid)
814                 return replace.go(m.group('text'))
815             else:
816                 return ''
817         if m.group('display'):
818             command = m.group('command')
819             return eval(command, self.globals, self.locals)
820         print '*** unhandled match', m.groupdict()
823 class ItemTemplate(TemplateFunctions):
824     def __init__(self, client, templates, classname):
825         self.client = client
826         self.templates = templates
827         self.classname = classname
829         # derived
830         self.db = self.client.db
831         self.cl = self.db.classes[self.classname]
832         self.properties = self.cl.getprops()
834         TemplateFunctions.__init__(self)
836     def render(self, nodeid):
837         self.nodeid = nodeid
839         if (self.properties.has_key('type') and
840                 self.properties.has_key('content')):
841             pass
842             # XXX we really want to return this as a downloadable...
843             #  currently I handle this at a higher level by detecting 'file'
844             #  designators...
846         w = self.client.write
847         w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
848             self.classname, nodeid))
849         s = open(os.path.join(self.templates, self.classname+'.item')).read()
850         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
851         w(replace.go(s))
852         w('</form>')
855 class NewItemTemplate(TemplateFunctions):
856     def __init__(self, client, templates, classname):
857         self.client = client
858         self.templates = templates
859         self.classname = classname
861         # derived
862         self.db = self.client.db
863         self.cl = self.db.classes[self.classname]
864         self.properties = self.cl.getprops()
866         TemplateFunctions.__init__(self)
868     def render(self, form):
869         self.form = form
870         w = self.client.write
871         c = self.classname
872         try:
873             s = open(os.path.join(self.templates, c+'.newitem')).read()
874         except IOError:
875             s = open(os.path.join(self.templates, c+'.item')).read()
876         w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
877         for key in form.keys():
878             if key[0] == ':':
879                 value = form[key].value
880                 if type(value) != type([]): value = [value]
881                 for value in value:
882                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
883         replace = ItemTemplateReplace(self.globals, locals(), None, None)
884         w(replace.go(s))
885         w('</form>')
888 # $Log: not supported by cvs2svn $
889 # Revision 1.50  2002/01/05 02:35:10  richard
890 # I18N'ification
892 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
893 # Features added:
894 #  .  Multilink properties are now displayed as comma separated values in
895 #     a textbox
896 #  .  The add user link is now only visible to the admin user
897 #  .  Modified the mail gateway to reject submissions from unknown
898 #     addresses if ANONYMOUS_ACCESS is denied
900 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
901 # Bugs fixed:
902 #   . Exception handling in hyperdb for strings-that-look-like numbers got
903 #     lost somewhere
904 #   . Internet Explorer submits full path for filename - we now strip away
905 #     the path
906 # Features added:
907 #   . Link and multilink properties are now displayed sorted in the cgi
908 #     interface
910 # Revision 1.47  2001/11/26 22:55:56  richard
911 # Feature:
912 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
913 #    the instance.
914 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
915 #    signature info in e-mails.
916 #  . Some more flexibility in the mail gateway and more error handling.
917 #  . Login now takes you to the page you back to the were denied access to.
919 # Fixed:
920 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
922 # Revision 1.46  2001/11/24 00:53:12  jhermann
923 # "except:" is bad, bad , bad!
925 # Revision 1.45  2001/11/22 15:46:42  jhermann
926 # Added module docstrings to all modules.
928 # Revision 1.44  2001/11/21 23:35:45  jhermann
929 # Added globbing for win32, and sample marking in a 2nd file to test it
931 # Revision 1.43  2001/11/21 04:04:43  richard
932 # *sigh* more missing value handling
934 # Revision 1.42  2001/11/21 03:40:54  richard
935 # more new property handling
937 # Revision 1.41  2001/11/15 10:26:01  richard
938 #  . missing "return" in filter_section (thanks Roch'e Compaan)
940 # Revision 1.40  2001/11/03 01:56:51  richard
941 # More HTML compliance fixes. This will probably fix the Netscape problem
942 # too.
944 # Revision 1.39  2001/11/03 01:43:47  richard
945 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
947 # Revision 1.38  2001/10/31 06:58:51  richard
948 # Added the wrap="hard" attribute to the textarea of the note field so the
949 # messages wrap sanely.
951 # Revision 1.37  2001/10/31 06:24:35  richard
952 # Added do_stext to htmltemplate, thanks Brad Clements.
954 # Revision 1.36  2001/10/28 22:51:38  richard
955 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
957 # Revision 1.35  2001/10/24 00:04:41  richard
958 # Removed the "infinite authentication loop", thanks Roch'e
960 # Revision 1.34  2001/10/23 22:56:36  richard
961 # Bugfix in filter "widget" placement, thanks Roch'e
963 # Revision 1.33  2001/10/23 01:00:18  richard
964 # Re-enabled login and registration access after lopping them off via
965 # disabling access for anonymous users.
966 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
967 # a couple of bugs while I was there. Probably introduced a couple, but
968 # things seem to work OK at the moment.
970 # Revision 1.32  2001/10/22 03:25:01  richard
971 # Added configuration for:
972 #  . anonymous user access and registration (deny/allow)
973 #  . filter "widget" location on index page (top, bottom, both)
974 # Updated some documentation.
976 # Revision 1.31  2001/10/21 07:26:35  richard
977 # feature #473127: Filenames. I modified the file.index and htmltemplate
978 #  source so that the filename is used in the link and the creation
979 #  information is displayed.
981 # Revision 1.30  2001/10/21 04:44:50  richard
982 # bug #473124: UI inconsistency with Link fields.
983 #    This also prompted me to fix a fairly long-standing usability issue -
984 #    that of being able to turn off certain filters.
986 # Revision 1.29  2001/10/21 00:17:56  richard
987 # CGI interface view customisation section may now be hidden (patch from
988 #  Roch'e Compaan.)
990 # Revision 1.28  2001/10/21 00:00:16  richard
991 # Fixed Checklist function - wasn't always working on a list.
993 # Revision 1.27  2001/10/20 12:13:44  richard
994 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
996 # Revision 1.26  2001/10/14 10:55:00  richard
997 # Handle empty strings in HTML template Link function
999 # Revision 1.25  2001/10/09 07:25:59  richard
1000 # Added the Password property type. See "pydoc roundup.password" for
1001 # implementation details. Have updated some of the documentation too.
1003 # Revision 1.24  2001/09/27 06:45:58  richard
1004 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1005 # on the plain() template function to escape the text for HTML.
1007 # Revision 1.23  2001/09/10 09:47:18  richard
1008 # Fixed bug in the generation of links to Link/Multilink in indexes.
1009 #   (thanks Hubert Hoegl)
1010 # Added AssignedTo to the "classic" schema's item page.
1012 # Revision 1.22  2001/08/30 06:01:17  richard
1013 # Fixed missing import in mailgw :(
1015 # Revision 1.21  2001/08/16 07:34:59  richard
1016 # better CGI text searching - but hidden filter fields are disappearing...
1018 # Revision 1.20  2001/08/15 23:43:18  richard
1019 # Fixed some isFooTypes that I missed.
1020 # Refactored some code in the CGI code.
1022 # Revision 1.19  2001/08/12 06:32:36  richard
1023 # using isinstance(blah, Foo) now instead of isFooType
1025 # Revision 1.18  2001/08/07 00:24:42  richard
1026 # stupid typo
1028 # Revision 1.17  2001/08/07 00:15:51  richard
1029 # Added the copyright/license notice to (nearly) all files at request of
1030 # Bizar Software.
1032 # Revision 1.16  2001/08/01 03:52:23  richard
1033 # Checklist was using wrong name.
1035 # Revision 1.15  2001/07/30 08:12:17  richard
1036 # Added time logging and file uploading to the templates.
1038 # Revision 1.14  2001/07/30 06:17:45  richard
1039 # Features:
1040 #  . Added ability for cgi newblah forms to indicate that the new node
1041 #    should be linked somewhere.
1042 # Fixed:
1043 #  . Fixed the agument handling for the roundup-admin find command.
1044 #  . Fixed handling of summary when no note supplied for newblah. Again.
1045 #  . Fixed detection of no form in htmltemplate Field display.
1047 # Revision 1.13  2001/07/30 02:37:53  richard
1048 # Temporary measure until we have decent schema migration.
1050 # Revision 1.12  2001/07/30 01:24:33  richard
1051 # Handles new node display now.
1053 # Revision 1.11  2001/07/29 09:31:35  richard
1054 # oops
1056 # Revision 1.10  2001/07/29 09:28:23  richard
1057 # Fixed sorting by clicking on column headings.
1059 # Revision 1.9  2001/07/29 08:27:40  richard
1060 # Fixed handling of passed-in values in form elements (ie. during a
1061 # drill-down)
1063 # Revision 1.8  2001/07/29 07:01:39  richard
1064 # Added vim command to all source so that we don't get no steenkin' tabs :)
1066 # Revision 1.7  2001/07/29 05:36:14  richard
1067 # Cleanup of the link label generation.
1069 # Revision 1.6  2001/07/29 04:06:42  richard
1070 # Fixed problem in link display when Link value is None.
1072 # Revision 1.5  2001/07/28 08:17:09  richard
1073 # fixed use of stylesheet
1075 # Revision 1.4  2001/07/28 07:59:53  richard
1076 # Replaced errno integers with their module values.
1077 # De-tabbed templatebuilder.py
1079 # Revision 1.3  2001/07/25 03:39:47  richard
1080 # Hrm - displaying links to classes that don't specify a key property. I've
1081 # got it defaulting to 'name', then 'title' and then a "random" property (first
1082 # one returned by getprops().keys().
1083 # Needs to be moved onto the Class I think...
1085 # Revision 1.2  2001/07/22 12:09:32  richard
1086 # Final commit of Grande Splite
1088 # Revision 1.1  2001/07/22 11:58:35  richard
1089 # More Grande Splite
1092 # vim: set filetype=python ts=4 sw=4 et si