Code

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