Code

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