Code

Features added:
[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.49 2001-12-20 15:43:01 rochecompaan Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno
26 import hyperdb, date, password
28 # This imports the StructureText functionality for the do_stext function
29 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
30 try:
31     from StructuredText.StructuredText import HTML as StructuredText
32 except ImportError:
33     StructuredText = None
35 class TemplateFunctions:
36     def __init__(self):
37         self.form = None
38         self.nodeid = None
39         self.filterspec = None
40         self.globals = {}
41         for key in TemplateFunctions.__dict__.keys():
42             if key[:3] == 'do_':
43                 self.globals[key[3:]] = getattr(self, key)
45     def do_plain(self, property, escape=0):
46         ''' display a String property directly;
48             display a Date property in a specified time zone with an option to
49             omit the time from the date stamp;
51             for a Link or Multilink property, display the key strings of the
52             linked nodes (or the ids if the linked class has no key property)
53         '''
54         if not self.nodeid and self.form is None:
55             return '[Field: not called from item]'
56         propclass = self.properties[property]
57         if self.nodeid:
58             # make sure the property is a valid one
59             # TODO: this tests, but we should handle the exception
60             prop_test = self.cl.getprops()[property]
62             # get the value for this property
63             try:
64                 value = self.cl.get(self.nodeid, property)
65             except KeyError:
66                 # a KeyError here means that the node doesn't have a value
67                 # for the specified property
68                 if isinstance(propclass, hyperdb.Multilink): value = []
69                 else: value = ''
70         else:
71             # TODO: pull the value from the form
72             if isinstance(propclass, hyperdb.Multilink): value = []
73             else: value = ''
74         if isinstance(propclass, hyperdb.String):
75             if value is None: value = ''
76             else: value = str(value)
77         elif isinstance(propclass, hyperdb.Password):
78             if value is None: value = ''
79             else: value = '*encrypted*'
80         elif isinstance(propclass, hyperdb.Date):
81             value = str(value)
82         elif isinstance(propclass, hyperdb.Interval):
83             value = str(value)
84         elif isinstance(propclass, hyperdb.Link):
85             linkcl = self.db.classes[propclass.classname]
86             k = linkcl.labelprop()
87             if value: value = str(linkcl.get(value, k))
88             else: value = '[unselected]'
89         elif isinstance(propclass, hyperdb.Multilink):
90             linkcl = self.db.classes[propclass.classname]
91             k = linkcl.labelprop()
92             value = ', '.join([linkcl.get(i, k) for i in value])
93         else:
94             s = _('Plain: bad propclass "%(propclass)s"')%locals()
95         if escape:
96             value = cgi.escape(value)
97         return value
99     def do_stext(self, property, escape=0):
100         '''Render as structured text using the StructuredText module
101            (see above for details)
102         '''
103         s = self.do_plain(property, escape=escape)
104         if not StructuredText:
105             return s
106         return StructuredText(s,level=1,header=0)
108     def do_field(self, property, size=None, height=None, showid=0):
109         ''' display a property like the plain displayer, but in a text field
110             to be edited
111         '''
112         if not self.nodeid and self.form is None and self.filterspec is None:
113             return _('[Field: not called from item]')
114         propclass = self.properties[property]
115         if (isinstance(propclass, hyperdb.Link) or
116             isinstance(propclass, hyperdb.Multilink)):
117             linkcl = self.db.classes[propclass.classname]
118             def sortfunc(a, b, cl=linkcl):
119                 if cl.getprops().has_key('order'):
120                     sort_on = 'order'
121                 else:
122                     sort_on = cl.labelprop()
123                 r = cmp(cl.get(a, sort_on), cl.get(b, sort_on))
124                 return r
125         if self.nodeid:
126             value = self.cl.get(self.nodeid, property, None)
127             # TODO: remove this from the code ... it's only here for
128             # handling schema changes, and they should be handled outside
129             # of this code...
130             if isinstance(propclass, hyperdb.Multilink) and value is None:
131                 value = []
132         elif self.filterspec is not None:
133             if isinstance(propclass, hyperdb.Multilink):
134                 value = self.filterspec.get(property, [])
135             else:
136                 value = self.filterspec.get(property, '')
137         else:
138             # TODO: pull the value from the form
139             if isinstance(propclass, hyperdb.Multilink): value = []
140             else: value = ''
141         if (isinstance(propclass, hyperdb.String) or
142                 isinstance(propclass, hyperdb.Date) or
143                 isinstance(propclass, hyperdb.Interval)):
144             size = size or 30
145             if value is None:
146                 value = ''
147             else:
148                 value = cgi.escape(value)
149                 value = '"'.join(value.split('"'))
150             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
151         elif isinstance(propclass, hyperdb.Password):
152             size = size or 30
153             s = '<input type="password" name="%s" size="%s">'%(property, size)
154         elif isinstance(propclass, hyperdb.Link):
155             l = ['<select name="%s">'%property]
156             k = linkcl.labelprop()
157             if value is None:
158                 s = 'selected '
159             else:
160                 s = ''
161             l.append('<option %svalue="-1">- no selection -</option>'%s)
162             options = linkcl.list()
163             options.sort(sortfunc)
164             for optionid in options:
165                 option = linkcl.get(optionid, k)
166                 s = ''
167                 if optionid == value:
168                     s = 'selected '
169                 if showid:
170                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
171                 else:
172                     lab = option
173                 if size is not None and len(lab) > size:
174                     lab = lab[:size-3] + '...'
175                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
176             l.append('</select>')
177             s = '\n'.join(l)
178         elif isinstance(propclass, hyperdb.Multilink):
179             list = linkcl.list()
180             list.sort(sortfunc)
181             k = linkcl.labelprop()
182             l = []
183             # special treatment for nosy list
184             if property == 'nosy':
185                 input_value = []
186             else:
187                 input_value = value
188             for v in value:
189                 lab = linkcl.get(v, k)
190                 if property != 'nosy':
191                     l.append('<a href="issue%s">%s: %s</a>'%(v,v,lab))
192                 else:
193                     input_value.append(lab)
194             if size is None:
195                 size = '10'
196             l.insert(0,'<input name="%s" size="%s" value="%s">'%(property, 
197                 size, ','.join(input_value)))
198             s = "<br>\n".join(l)
199         else:
200             s = 'Plain: bad propclass "%s"'%propclass
201         return s
203     def do_menu(self, property, size=None, height=None, showid=0):
204         ''' for a Link property, display a menu of the available choices
205         '''
206         propclass = self.properties[property]
207         if self.nodeid:
208             value = self.cl.get(self.nodeid, property)
209         else:
210             # TODO: pull the value from the form
211             if isinstance(propclass, hyperdb.Multilink): value = []
212             else: value = None
213         if isinstance(propclass, hyperdb.Link):
214             linkcl = self.db.classes[propclass.classname]
215             l = ['<select name="%s">'%property]
216             k = linkcl.labelprop()
217             s = ''
218             if value is None:
219                 s = 'selected '
220             l.append('<option %svalue="-1">- no selection -</option>'%s)
221             for optionid in linkcl.list():
222                 option = linkcl.get(optionid, k)
223                 s = ''
224                 if optionid == value:
225                     s = 'selected '
226                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
227             l.append('</select>')
228             return '\n'.join(l)
229         if isinstance(propclass, hyperdb.Multilink):
230             linkcl = self.db.classes[propclass.classname]
231             list = linkcl.list()
232             height = height or min(len(list), 7)
233             l = ['<select multiple name="%s" size="%s">'%(property, height)]
234             k = linkcl.labelprop()
235             for optionid in list:
236                 option = linkcl.get(optionid, k)
237                 s = ''
238                 if optionid in value:
239                     s = 'selected '
240                 if showid:
241                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
242                 else:
243                     lab = option
244                 if size is not None and len(lab) > size:
245                     lab = lab[:size-3] + '...'
246                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
247             l.append('</select>')
248             return '\n'.join(l)
249         return '[Menu: not a link]'
251     #XXX deviates from spec
252     def do_link(self, property=None, is_download=0):
253         '''For a Link or Multilink property, display the names of the linked
254            nodes, hyperlinked to the item views on those nodes.
255            For other properties, link to this node with the property as the
256            text.
258            If is_download is true, append the property value to the generated
259            URL so that the link may be used as a download link and the
260            downloaded file name is correct.
261         '''
262         if not self.nodeid and self.form is None:
263             return '[Link: not called from item]'
264         propclass = self.properties[property]
265         if self.nodeid:
266             value = self.cl.get(self.nodeid, property)
267         else:
268             if isinstance(propclass, hyperdb.Multilink): value = []
269             elif isinstance(propclass, hyperdb.Link): value = None
270             else: value = ''
271         if isinstance(propclass, hyperdb.Link):
272             linkname = propclass.classname
273             if value is None: return '[no %s]'%property.capitalize()
274             linkcl = self.db.classes[linkname]
275             k = linkcl.labelprop()
276             linkvalue = linkcl.get(value, k)
277             if is_download:
278                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
279                     linkvalue, linkvalue)
280             else:
281                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
282         if isinstance(propclass, hyperdb.Multilink):
283             linkname = propclass.classname
284             linkcl = self.db.classes[linkname]
285             k = linkcl.labelprop()
286             if not value : return '[no %s]'%property.capitalize()
287             l = []
288             for value in value:
289                 linkvalue = linkcl.get(value, k)
290                 if is_download:
291                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
292                         linkvalue, linkvalue))
293                 else:
294                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
295                         linkvalue))
296             return ', '.join(l)
297         if isinstance(propclass, hyperdb.String):
298             if value == '': value = '[no %s]'%property.capitalize()
299         if is_download:
300             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
301                 value, value)
302         else:
303             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
305     def do_count(self, property, **args):
306         ''' for a Multilink property, display a count of the number of links in
307             the list
308         '''
309         if not self.nodeid:
310             return '[Count: not called from item]'
311         propclass = self.properties[property]
312         value = self.cl.get(self.nodeid, property)
313         if isinstance(propclass, hyperdb.Multilink):
314             return str(len(value))
315         return '[Count: not a Multilink]'
317     # XXX pretty is definitely new ;)
318     def do_reldate(self, property, pretty=0):
319         ''' display a Date property in terms of an interval relative to the
320             current date (e.g. "+ 3w", "- 2d").
322             with the 'pretty' flag, make it pretty
323         '''
324         if not self.nodeid and self.form is None:
325             return '[Reldate: not called from item]'
326         propclass = self.properties[property]
327         if isinstance(not propclass, hyperdb.Date):
328             return '[Reldate: not a Date]'
329         if self.nodeid:
330             value = self.cl.get(self.nodeid, property)
331         else:
332             value = date.Date('.')
333         interval = value - date.Date('.')
334         if pretty:
335             if not self.nodeid:
336                 return 'now'
337             pretty = interval.pretty()
338             if pretty is None:
339                 pretty = value.pretty()
340             return pretty
341         return str(interval)
343     def do_download(self, property, **args):
344         ''' show a Link("file") or Multilink("file") property using links that
345             allow you to download files
346         '''
347         if not self.nodeid:
348             return '[Download: not called from item]'
349         propclass = self.properties[property]
350         value = self.cl.get(self.nodeid, property)
351         if isinstance(propclass, hyperdb.Link):
352             linkcl = self.db.classes[propclass.classname]
353             linkvalue = linkcl.get(value, k)
354             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
355         if isinstance(propclass, hyperdb.Multilink):
356             linkcl = self.db.classes[propclass.classname]
357             l = []
358             for value in value:
359                 linkvalue = linkcl.get(value, k)
360                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
361             return ', '.join(l)
362         return '[Download: not a link]'
365     def do_checklist(self, property, **args):
366         ''' for a Link or Multilink property, display checkboxes for the
367             available choices to permit filtering
368         '''
369         propclass = self.properties[property]
370         if (not isinstance(propclass, hyperdb.Link) and not
371                 isinstance(propclass, hyperdb.Multilink)):
372             return '[Checklist: not a link]'
374         # get our current checkbox state
375         if self.nodeid:
376             # get the info from the node - make sure it's a list
377             if isinstance(propclass, hyperdb.Link):
378                 value = [self.cl.get(self.nodeid, property)]
379             else:
380                 value = self.cl.get(self.nodeid, property)
381         elif self.filterspec is not None:
382             # get the state from the filter specification (always a list)
383             value = self.filterspec.get(property, [])
384         else:
385             # it's a new node, so there's no state
386             value = []
388         # so we can map to the linked node's "lable" property
389         linkcl = self.db.classes[propclass.classname]
390         l = []
391         k = linkcl.labelprop()
392         for optionid in linkcl.list():
393             option = linkcl.get(optionid, k)
394             if optionid in value or option in value:
395                 checked = 'checked'
396             else:
397                 checked = ''
398             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
399                 option, checked, property, option))
401         # for Links, allow the "unselected" option too
402         if isinstance(propclass, hyperdb.Link):
403             if value is None or '-1' in value:
404                 checked = 'checked'
405             else:
406                 checked = ''
407             l.append('[unselected]:<input type="checkbox" %s name="%s" '
408                 'value="-1">'%(checked, property))
409         return '\n'.join(l)
411     def do_note(self, rows=5, cols=80):
412         ''' display a "note" field, which is a text area for entering a note to
413             go along with a change. 
414         '''
415         # TODO: pull the value from the form
416         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
417             '</textarea>'%(rows, cols)
419     # XXX new function
420     def do_list(self, property, reverse=0):
421         ''' list the items specified by property using the standard index for
422             the class
423         '''
424         propcl = self.properties[property]
425         if not isinstance(propcl, hyperdb.Multilink):
426             return '[List: not a Multilink]'
427         value = self.cl.get(self.nodeid, property)
428         if reverse:
429             value.reverse()
431         # render the sub-index into a string
432         fp = StringIO.StringIO()
433         try:
434             write_save = self.client.write
435             self.client.write = fp.write
436             index = IndexTemplate(self.client, self.templates, propcl.classname)
437             index.render(nodeids=value, show_display_form=0)
438         finally:
439             self.client.write = write_save
441         return fp.getvalue()
443     # XXX new function
444     def do_history(self, **args):
445         ''' list the history of the item
446         '''
447         if self.nodeid is None:
448             return "[History: node doesn't exist]"
450         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
451             '<tr class="list-header">',
452             '<td><span class="list-item"><strong>Date</strong></span></td>',
453             '<td><span class="list-item"><strong>User</strong></span></td>',
454             '<td><span class="list-item"><strong>Action</strong></span></td>',
455             '<td><span class="list-item"><strong>Args</strong></span></td>']
457         for id, date, user, action, args in self.cl.history(self.nodeid):
458             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
459                date, user, action, args))
460         l.append('</table>')
461         return '\n'.join(l)
463     # XXX new function
464     def do_submit(self):
465         ''' add a submit button for the item
466         '''
467         if self.nodeid:
468             return '<input type="submit" value="Submit Changes">'
469         elif self.form is not None:
470             return '<input type="submit" value="Submit New Entry">'
471         else:
472             return '[Submit: not called from item]'
476 #   INDEX TEMPLATES
478 class IndexTemplateReplace:
479     def __init__(self, globals, locals, props):
480         self.globals = globals
481         self.locals = locals
482         self.props = props
484     replace=re.compile(
485         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
486         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
487     def go(self, text):
488         return self.replace.sub(self, text)
490     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
491         if m.group('name'):
492             if m.group('name') in self.props:
493                 text = m.group('text')
494                 replace = IndexTemplateReplace(self.globals, {}, self.props)
495                 return replace.go(m.group('text'))
496             else:
497                 return ''
498         if m.group('display'):
499             command = m.group('command')
500             return eval(command, self.globals, self.locals)
501         print '*** unhandled match', m.groupdict()
503 class IndexTemplate(TemplateFunctions):
504     def __init__(self, client, templates, classname):
505         self.client = client
506         self.templates = templates
507         self.classname = classname
509         # derived
510         self.db = self.client.db
511         self.cl = self.db.classes[self.classname]
512         self.properties = self.cl.getprops()
514         TemplateFunctions.__init__(self)
516     col_re=re.compile(r'<property\s+name="([^>]+)">')
517     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
518             show_display_form=1, nodeids=None, show_customization=1):
519         self.filterspec = filterspec
521         w = self.client.write
523         # get the filter template
524         try:
525             filter_template = open(os.path.join(self.templates,
526                 self.classname+'.filter')).read()
527             all_filters = self.col_re.findall(filter_template)
528         except IOError, error:
529             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
530             filter_template = None
531             all_filters = []
533         # XXX deviate from spec here ...
534         # load the index section template and figure the default columns from it
535         template = open(os.path.join(self.templates,
536             self.classname+'.index')).read()
537         all_columns = self.col_re.findall(template)
538         if not columns:
539             columns = []
540             for name in all_columns:
541                 columns.append(name)
542         else:
543             # re-sort columns to be the same order as all_columns
544             l = []
545             for name in all_columns:
546                 if name in columns:
547                     l.append(name)
548             columns = l
550         # display the filter section
551         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
552                 self.client.FILTER_POSITION in ('top and bottom', 'top')):
553             w('<form action="index">\n')
554             self.filter_section(filter_template, filter, columns, group,
555                 all_filters, all_columns, show_customization)
556             # make sure that the sorting doesn't get lost either
557             if sort:
558                 w('<input type="hidden" name=":sort" value="%s">'%
559                     ','.join(sort))
560             w('</form>\n')
563         # now display the index section
564         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
565         w('<tr class="list-header">\n')
566         for name in columns:
567             cname = name.capitalize()
568             if show_display_form:
569                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
570                 anchor = "%s?%s"%(self.classname, sb)
571                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
572                     anchor, cname))
573             else:
574                 w('<td><span class="list-header">%s</span></td>\n'%cname)
575         w('</tr>\n')
577         # this stuff is used for group headings - optimise the group names
578         old_group = None
579         group_names = []
580         if group:
581             for name in group:
582                 if name[0] == '-': group_names.append(name[1:])
583                 else: group_names.append(name)
585         # now actually loop through all the nodes we get from the filter and
586         # apply the template
587         if nodeids is None:
588             nodeids = self.cl.filter(filterspec, sort, group)
589         for nodeid in nodeids:
590             # check for a group heading
591             if group_names:
592                 this_group = [self.cl.get(nodeid, name, '[no value]') for name in group_names]
593                 if this_group != old_group:
594                     l = []
595                     for name in group_names:
596                         prop = self.properties[name]
597                         if isinstance(prop, hyperdb.Link):
598                             group_cl = self.db.classes[prop.classname]
599                             key = group_cl.getkey()
600                             value = self.cl.get(nodeid, name)
601                             if value is None:
602                                 l.append('[unselected %s]'%prop.classname)
603                             else:
604                                 l.append(group_cl.get(self.cl.get(nodeid,
605                                     name), key))
606                         elif isinstance(prop, hyperdb.Multilink):
607                             group_cl = self.db.classes[prop.classname]
608                             key = group_cl.getkey()
609                             for value in self.cl.get(nodeid, name):
610                                 l.append(group_cl.get(value, key))
611                         else:
612                             value = self.cl.get(nodeid, name, '[no value]')
613                             if value is None:
614                                 value = '[empty %s]'%name
615                             else:
616                                 value = str(value)
617                             l.append(value)
618                     w('<tr class="section-bar">'
619                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
620                         len(columns), ', '.join(l)))
621                     old_group = this_group
623             # display this node's row
624             replace = IndexTemplateReplace(self.globals, locals(), columns)
625             self.nodeid = nodeid
626             w(replace.go(template))
627             self.nodeid = None
629         w('</table>')
631         # display the filter section
632         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
633                 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
634             w('<form action="index">\n')
635             self.filter_section(filter_template, filter, columns, group,
636                 all_filters, all_columns, show_customization)
637             # make sure that the sorting doesn't get lost either
638             if sort:
639                 w('<input type="hidden" name=":sort" value="%s">'%
640                     ','.join(sort))
641             w('</form>\n')
644     def filter_section(self, template, filter, columns, group, all_filters,
645             all_columns, show_customization):
647         w = self.client.write
649         # wrap the template in a single table to ensure the whole widget
650         # is displayed at once
651         w('<table><tr><td>')
653         if template and filter:
654             # display the filter section
655             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
656             w('<tr class="location-bar">')
657             w(' <th align="left" colspan="2">Filter specification...</th>')
658             w('</tr>')
659             replace = IndexTemplateReplace(self.globals, locals(), filter)
660             w(replace.go(template))
661             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
662             w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
663             w('</table>')
665         # now add in the filter/columns/group/etc config table form
666         w('<input type="hidden" name="show_customization" value="%s">' %
667             show_customization )
668         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
669         names = []
670         for name in self.properties.keys():
671             if name in all_filters or name in all_columns:
672                 names.append(name)
673         if show_customization:
674             action = '-'
675         else:
676             action = '+'
677             # hide the values for filters, columns and grouping in the form
678             # if the customization widget is not visible
679             for name in names:
680                 if all_filters and name in filter:
681                     w('<input type="hidden" name=":filter" value="%s">' % name)
682                 if all_columns and name in columns:
683                     w('<input type="hidden" name=":columns" value="%s">' % name)
684                 if all_columns and name in group:
685                     w('<input type="hidden" name=":group" value="%s">' % name)
687         # TODO: The widget style can go into the stylesheet
688         w('<th align="left" colspan=%s>'
689           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
690           'customisation...</th></tr>\n'%(len(names)+1, action))
692         if not show_customization:
693             w('</table>\n')
694             return
696         w('<tr class="location-bar"><th>&nbsp;</th>')
697         for name in names:
698             w('<th>%s</th>'%name.capitalize())
699         w('</tr>\n')
701         # Filter
702         if all_filters:
703             w('<tr><th width="1%" align=right class="location-bar">'
704               'Filters</th>\n')
705             for name in names:
706                 if name not in all_filters:
707                     w('<td>&nbsp;</td>')
708                     continue
709                 if name in filter: checked=' checked'
710                 else: checked=''
711                 w('<td align=middle>\n')
712                 w(' <input type="checkbox" name=":filter" value="%s" '
713                   '%s></td>\n'%(name, checked))
714             w('</tr>\n')
716         # Columns
717         if all_columns:
718             w('<tr><th width="1%" align=right class="location-bar">'
719               'Columns</th>\n')
720             for name in names:
721                 if name not in all_columns:
722                     w('<td>&nbsp;</td>')
723                     continue
724                 if name in columns: checked=' checked'
725                 else: checked=''
726                 w('<td align=middle>\n')
727                 w(' <input type="checkbox" name=":columns" value="%s"'
728                   '%s></td>\n'%(name, checked))
729             w('</tr>\n')
731             # Grouping
732             w('<tr><th width="1%" align=right class="location-bar">'
733               '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.48  2001/12/20 06:13:24  rochecompaan
889 # Bugs fixed:
890 #   . Exception handling in hyperdb for strings-that-look-like numbers got
891 #     lost somewhere
892 #   . Internet Explorer submits full path for filename - we now strip away
893 #     the path
894 # Features added:
895 #   . Link and multilink properties are now displayed sorted in the cgi
896 #     interface
898 # Revision 1.47  2001/11/26 22:55:56  richard
899 # Feature:
900 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
901 #    the instance.
902 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
903 #    signature info in e-mails.
904 #  . Some more flexibility in the mail gateway and more error handling.
905 #  . Login now takes you to the page you back to the were denied access to.
907 # Fixed:
908 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
910 # Revision 1.46  2001/11/24 00:53:12  jhermann
911 # "except:" is bad, bad , bad!
913 # Revision 1.45  2001/11/22 15:46:42  jhermann
914 # Added module docstrings to all modules.
916 # Revision 1.44  2001/11/21 23:35:45  jhermann
917 # Added globbing for win32, and sample marking in a 2nd file to test it
919 # Revision 1.43  2001/11/21 04:04:43  richard
920 # *sigh* more missing value handling
922 # Revision 1.42  2001/11/21 03:40:54  richard
923 # more new property handling
925 # Revision 1.41  2001/11/15 10:26:01  richard
926 #  . missing "return" in filter_section (thanks Roch'e Compaan)
928 # Revision 1.40  2001/11/03 01:56:51  richard
929 # More HTML compliance fixes. This will probably fix the Netscape problem
930 # too.
932 # Revision 1.39  2001/11/03 01:43:47  richard
933 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
935 # Revision 1.38  2001/10/31 06:58:51  richard
936 # Added the wrap="hard" attribute to the textarea of the note field so the
937 # messages wrap sanely.
939 # Revision 1.37  2001/10/31 06:24:35  richard
940 # Added do_stext to htmltemplate, thanks Brad Clements.
942 # Revision 1.36  2001/10/28 22:51:38  richard
943 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
945 # Revision 1.35  2001/10/24 00:04:41  richard
946 # Removed the "infinite authentication loop", thanks Roch'e
948 # Revision 1.34  2001/10/23 22:56:36  richard
949 # Bugfix in filter "widget" placement, thanks Roch'e
951 # Revision 1.33  2001/10/23 01:00:18  richard
952 # Re-enabled login and registration access after lopping them off via
953 # disabling access for anonymous users.
954 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
955 # a couple of bugs while I was there. Probably introduced a couple, but
956 # things seem to work OK at the moment.
958 # Revision 1.32  2001/10/22 03:25:01  richard
959 # Added configuration for:
960 #  . anonymous user access and registration (deny/allow)
961 #  . filter "widget" location on index page (top, bottom, both)
962 # Updated some documentation.
964 # Revision 1.31  2001/10/21 07:26:35  richard
965 # feature #473127: Filenames. I modified the file.index and htmltemplate
966 #  source so that the filename is used in the link and the creation
967 #  information is displayed.
969 # Revision 1.30  2001/10/21 04:44:50  richard
970 # bug #473124: UI inconsistency with Link fields.
971 #    This also prompted me to fix a fairly long-standing usability issue -
972 #    that of being able to turn off certain filters.
974 # Revision 1.29  2001/10/21 00:17:56  richard
975 # CGI interface view customisation section may now be hidden (patch from
976 #  Roch'e Compaan.)
978 # Revision 1.28  2001/10/21 00:00:16  richard
979 # Fixed Checklist function - wasn't always working on a list.
981 # Revision 1.27  2001/10/20 12:13:44  richard
982 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
984 # Revision 1.26  2001/10/14 10:55:00  richard
985 # Handle empty strings in HTML template Link function
987 # Revision 1.25  2001/10/09 07:25:59  richard
988 # Added the Password property type. See "pydoc roundup.password" for
989 # implementation details. Have updated some of the documentation too.
991 # Revision 1.24  2001/09/27 06:45:58  richard
992 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
993 # on the plain() template function to escape the text for HTML.
995 # Revision 1.23  2001/09/10 09:47:18  richard
996 # Fixed bug in the generation of links to Link/Multilink in indexes.
997 #   (thanks Hubert Hoegl)
998 # Added AssignedTo to the "classic" schema's item page.
1000 # Revision 1.22  2001/08/30 06:01:17  richard
1001 # Fixed missing import in mailgw :(
1003 # Revision 1.21  2001/08/16 07:34:59  richard
1004 # better CGI text searching - but hidden filter fields are disappearing...
1006 # Revision 1.20  2001/08/15 23:43:18  richard
1007 # Fixed some isFooTypes that I missed.
1008 # Refactored some code in the CGI code.
1010 # Revision 1.19  2001/08/12 06:32:36  richard
1011 # using isinstance(blah, Foo) now instead of isFooType
1013 # Revision 1.18  2001/08/07 00:24:42  richard
1014 # stupid typo
1016 # Revision 1.17  2001/08/07 00:15:51  richard
1017 # Added the copyright/license notice to (nearly) all files at request of
1018 # Bizar Software.
1020 # Revision 1.16  2001/08/01 03:52:23  richard
1021 # Checklist was using wrong name.
1023 # Revision 1.15  2001/07/30 08:12:17  richard
1024 # Added time logging and file uploading to the templates.
1026 # Revision 1.14  2001/07/30 06:17:45  richard
1027 # Features:
1028 #  . Added ability for cgi newblah forms to indicate that the new node
1029 #    should be linked somewhere.
1030 # Fixed:
1031 #  . Fixed the agument handling for the roundup-admin find command.
1032 #  . Fixed handling of summary when no note supplied for newblah. Again.
1033 #  . Fixed detection of no form in htmltemplate Field display.
1035 # Revision 1.13  2001/07/30 02:37:53  richard
1036 # Temporary measure until we have decent schema migration.
1038 # Revision 1.12  2001/07/30 01:24:33  richard
1039 # Handles new node display now.
1041 # Revision 1.11  2001/07/29 09:31:35  richard
1042 # oops
1044 # Revision 1.10  2001/07/29 09:28:23  richard
1045 # Fixed sorting by clicking on column headings.
1047 # Revision 1.9  2001/07/29 08:27:40  richard
1048 # Fixed handling of passed-in values in form elements (ie. during a
1049 # drill-down)
1051 # Revision 1.8  2001/07/29 07:01:39  richard
1052 # Added vim command to all source so that we don't get no steenkin' tabs :)
1054 # Revision 1.7  2001/07/29 05:36:14  richard
1055 # Cleanup of the link label generation.
1057 # Revision 1.6  2001/07/29 04:06:42  richard
1058 # Fixed problem in link display when Link value is None.
1060 # Revision 1.5  2001/07/28 08:17:09  richard
1061 # fixed use of stylesheet
1063 # Revision 1.4  2001/07/28 07:59:53  richard
1064 # Replaced errno integers with their module values.
1065 # De-tabbed templatebuilder.py
1067 # Revision 1.3  2001/07/25 03:39:47  richard
1068 # Hrm - displaying links to classes that don't specify a key property. I've
1069 # got it defaulting to 'name', then 'title' and then a "random" property (first
1070 # one returned by getprops().keys().
1071 # Needs to be moved onto the Class I think...
1073 # Revision 1.2  2001/07/22 12:09:32  richard
1074 # Final commit of Grande Splite
1076 # Revision 1.1  2001/07/22 11:58:35  richard
1077 # More Grande Splite
1080 # vim: set filetype=python ts=4 sw=4 et si