Code

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