Code

fc0baa454079c1a11e94759ef31d127ab02e9597
[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.33 2001-10-23 01:00:18 richard Exp $
20 import os, re, StringIO, urllib, cgi, errno
22 import hyperdb, date, password
24 class TemplateFunctions:
25     def __init__(self):
26         self.form = None
27         self.nodeid = None
28         self.filterspec = None
29         self.globals = {}
30         for key in TemplateFunctions.__dict__.keys():
31             if key[:3] == 'do_':
32                 self.globals[key[3:]] = getattr(self, key)
34     def do_plain(self, property, escape=0):
35         ''' display a String property directly;
37             display a Date property in a specified time zone with an option to
38             omit the time from the date stamp;
40             for a Link or Multilink property, display the key strings of the
41             linked nodes (or the ids if the linked class has no key property)
42         '''
43         if not self.nodeid and self.form is None:
44             return '[Field: not called from item]'
45         propclass = self.properties[property]
46         if self.nodeid:
47             value = self.cl.get(self.nodeid, property)
48         else:
49             # TODO: pull the value from the form
50             if isinstance(propclass, hyperdb.Multilink): value = []
51             else: value = ''
52         if isinstance(propclass, hyperdb.String):
53             if value is None: value = ''
54             else: value = str(value)
55         elif isinstance(propclass, hyperdb.Password):
56             if value is None: value = ''
57             else: value = '*encrypted*'
58         elif isinstance(propclass, hyperdb.Date):
59             value = str(value)
60         elif isinstance(propclass, hyperdb.Interval):
61             value = str(value)
62         elif isinstance(propclass, hyperdb.Link):
63             linkcl = self.db.classes[propclass.classname]
64             k = linkcl.labelprop()
65             if value: value = str(linkcl.get(value, k))
66             else: value = '[unselected]'
67         elif isinstance(propclass, hyperdb.Multilink):
68             linkcl = self.db.classes[propclass.classname]
69             k = linkcl.labelprop()
70             value = ', '.join([linkcl.get(i, k) for i in value])
71         else:
72             s = 'Plain: bad propclass "%s"'%propclass
73         if escape:
74             return cgi.escape(value)
75         return value
77     def do_field(self, property, size=None, height=None, showid=0):
78         ''' display a property like the plain displayer, but in a text field
79             to be edited
80         '''
81         if not self.nodeid and self.form is None and self.filterspec is None:
82             return '[Field: not called from item]'
83         propclass = self.properties[property]
84         if self.nodeid:
85             value = self.cl.get(self.nodeid, property, None)
86             # TODO: remove this from the code ... it's only here for
87             # handling schema changes, and they should be handled outside
88             # of this code...
89             if isinstance(propclass, hyperdb.Multilink) and value is None:
90                 value = []
91         elif self.filterspec is not None:
92             if isinstance(propclass, hyperdb.Multilink):
93                 value = self.filterspec.get(property, [])
94             else:
95                 value = self.filterspec.get(property, '')
96         else:
97             # TODO: pull the value from the form
98             if isinstance(propclass, hyperdb.Multilink): value = []
99             else: value = ''
100         if (isinstance(propclass, hyperdb.String) or
101                 isinstance(propclass, hyperdb.Date) or
102                 isinstance(propclass, hyperdb.Interval)):
103             size = size or 30
104             if value is None:
105                 value = ''
106             else:
107                 value = cgi.escape(value)
108                 value = '"'.join(value.split('"'))
109             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
110         elif isinstance(propclass, hyperdb.Password):
111             size = size or 30
112             s = '<input type="password" name="%s" size="%s">'%(property, size)
113         elif isinstance(propclass, hyperdb.Link):
114             linkcl = self.db.classes[propclass.classname]
115             l = ['<select name="%s">'%property]
116             k = linkcl.labelprop()
117             if value is None:
118                 s = 'selected '
119             else:
120                 s = ''
121             l.append('<option %svalue="-1">- no selection -</option>'%s)
122             for optionid in linkcl.list():
123                 option = linkcl.get(optionid, k)
124                 s = ''
125                 if optionid == value:
126                     s = 'selected '
127                 if showid:
128                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
129                 else:
130                     lab = option
131                 if size is not None and len(lab) > size:
132                     lab = lab[:size-3] + '...'
133                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
134             l.append('</select>')
135             s = '\n'.join(l)
136         elif isinstance(propclass, hyperdb.Multilink):
137             linkcl = self.db.classes[propclass.classname]
138             list = linkcl.list()
139             height = height or min(len(list), 7)
140             l = ['<select multiple name="%s" size="%s">'%(property, height)]
141             k = linkcl.labelprop()
142             for optionid in list:
143                 option = linkcl.get(optionid, k)
144                 s = ''
145                 if optionid in value:
146                     s = 'selected '
147                 if showid:
148                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
149                 else:
150                     lab = option
151                 if size is not None and len(lab) > size:
152                     lab = lab[:size-3] + '...'
153                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
154             l.append('</select>')
155             s = '\n'.join(l)
156         else:
157             s = 'Plain: bad propclass "%s"'%propclass
158         return s
160     def do_menu(self, property, size=None, height=None, showid=0):
161         ''' for a Link property, display a menu of the available choices
162         '''
163         propclass = self.properties[property]
164         if self.nodeid:
165             value = self.cl.get(self.nodeid, property)
166         else:
167             # TODO: pull the value from the form
168             if isinstance(propclass, hyperdb.Multilink): value = []
169             else: value = None
170         if isinstance(propclass, hyperdb.Link):
171             linkcl = self.db.classes[propclass.classname]
172             l = ['<select name="%s">'%property]
173             k = linkcl.labelprop()
174             s = ''
175             if value is None:
176                 s = 'selected '
177             l.append('<option %svalue="-1">- no selection -</option>'%s)
178             for optionid in linkcl.list():
179                 option = linkcl.get(optionid, k)
180                 s = ''
181                 if optionid == value:
182                     s = 'selected '
183                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
184             l.append('</select>')
185             return '\n'.join(l)
186         if isinstance(propclass, hyperdb.Multilink):
187             linkcl = self.db.classes[propclass.classname]
188             list = linkcl.list()
189             height = height or min(len(list), 7)
190             l = ['<select multiple name="%s" size="%s">'%(property, height)]
191             k = linkcl.labelprop()
192             for optionid in list:
193                 option = linkcl.get(optionid, k)
194                 s = ''
195                 if optionid in value:
196                     s = 'selected '
197                 if showid:
198                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
199                 else:
200                     lab = option
201                 if size is not None and len(lab) > size:
202                     lab = lab[:size-3] + '...'
203                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
204             l.append('</select>')
205             return '\n'.join(l)
206         return '[Menu: not a link]'
208     #XXX deviates from spec
209     def do_link(self, property=None, is_download=0):
210         '''For a Link or Multilink property, display the names of the linked
211            nodes, hyperlinked to the item views on those nodes.
212            For other properties, link to this node with the property as the
213            text.
215            If is_download is true, append the property value to the generated
216            URL so that the link may be used as a download link and the
217            downloaded file name is correct.
218         '''
219         if not self.nodeid and self.form is None:
220             return '[Link: not called from item]'
221         propclass = self.properties[property]
222         if self.nodeid:
223             value = self.cl.get(self.nodeid, property)
224         else:
225             if isinstance(propclass, hyperdb.Multilink): value = []
226             elif isinstance(propclass, hyperdb.Link): value = None
227             else: value = ''
228         if isinstance(propclass, hyperdb.Link):
229             linkname = propclass.classname
230             if value is None: return '[no %s]'%property.capitalize()
231             linkcl = self.db.classes[linkname]
232             k = linkcl.labelprop()
233             linkvalue = linkcl.get(value, k)
234             if is_download:
235                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
236                     linkvalue, linkvalue)
237             else:
238                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
239         if isinstance(propclass, hyperdb.Multilink):
240             linkname = propclass.classname
241             linkcl = self.db.classes[linkname]
242             k = linkcl.labelprop()
243             if not value : return '[no %s]'%property.capitalize()
244             l = []
245             for value in value:
246                 linkvalue = linkcl.get(value, k)
247                 if is_download:
248                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
249                         linkvalue, linkvalue))
250                 else:
251                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
252                         linkvalue))
253             return ', '.join(l)
254         if isinstance(propclass, hyperdb.String):
255             if value == '': value = '[no %s]'%property.capitalize()
256         if is_download:
257             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
258                 value, value)
259         else:
260             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
262     def do_count(self, property, **args):
263         ''' for a Multilink property, display a count of the number of links in
264             the list
265         '''
266         if not self.nodeid:
267             return '[Count: not called from item]'
268         propclass = self.properties[property]
269         value = self.cl.get(self.nodeid, property)
270         if isinstance(propclass, hyperdb.Multilink):
271             return str(len(value))
272         return '[Count: not a Multilink]'
274     # XXX pretty is definitely new ;)
275     def do_reldate(self, property, pretty=0):
276         ''' display a Date property in terms of an interval relative to the
277             current date (e.g. "+ 3w", "- 2d").
279             with the 'pretty' flag, make it pretty
280         '''
281         if not self.nodeid and self.form is None:
282             return '[Reldate: not called from item]'
283         propclass = self.properties[property]
284         if isinstance(not propclass, hyperdb.Date):
285             return '[Reldate: not a Date]'
286         if self.nodeid:
287             value = self.cl.get(self.nodeid, property)
288         else:
289             value = date.Date('.')
290         interval = value - date.Date('.')
291         if pretty:
292             if not self.nodeid:
293                 return 'now'
294             pretty = interval.pretty()
295             if pretty is None:
296                 pretty = value.pretty()
297             return pretty
298         return str(interval)
300     def do_download(self, property, **args):
301         ''' show a Link("file") or Multilink("file") property using links that
302             allow you to download files
303         '''
304         if not self.nodeid:
305             return '[Download: not called from item]'
306         propclass = self.properties[property]
307         value = self.cl.get(self.nodeid, property)
308         if isinstance(propclass, hyperdb.Link):
309             linkcl = self.db.classes[propclass.classname]
310             linkvalue = linkcl.get(value, k)
311             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
312         if isinstance(propclass, hyperdb.Multilink):
313             linkcl = self.db.classes[propclass.classname]
314             l = []
315             for value in value:
316                 linkvalue = linkcl.get(value, k)
317                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
318             return ', '.join(l)
319         return '[Download: not a link]'
322     def do_checklist(self, property, **args):
323         ''' for a Link or Multilink property, display checkboxes for the
324             available choices to permit filtering
325         '''
326         propclass = self.properties[property]
327         if (not isinstance(propclass, hyperdb.Link) and not
328                 isinstance(propclass, hyperdb.Multilink)):
329             return '[Checklist: not a link]'
331         # get our current checkbox state
332         if self.nodeid:
333             # get the info from the node - make sure it's a list
334             if isinstance(propclass, hyperdb.Link):
335                 value = [self.cl.get(self.nodeid, property)]
336             else:
337                 value = self.cl.get(self.nodeid, property)
338         elif self.filterspec is not None:
339             # get the state from the filter specification (always a list)
340             value = self.filterspec.get(property, [])
341         else:
342             # it's a new node, so there's no state
343             value = []
345         # so we can map to the linked node's "lable" property
346         linkcl = self.db.classes[propclass.classname]
347         l = []
348         k = linkcl.labelprop()
349         for optionid in linkcl.list():
350             option = linkcl.get(optionid, k)
351             if optionid in value or option in value:
352                 checked = 'checked'
353             else:
354                 checked = ''
355             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
356                 option, checked, property, option))
358         # for Links, allow the "unselected" option too
359         if isinstance(propclass, hyperdb.Link):
360             if value is None or '-1' in value:
361                 checked = 'checked'
362             else:
363                 checked = ''
364             l.append('[unselected]:<input type="checkbox" %s name="%s" '
365                 'value="-1">'%(checked, property))
366         return '\n'.join(l)
368     def do_note(self, rows=5, cols=80):
369         ''' display a "note" field, which is a text area for entering a note to
370             go along with a change. 
371         '''
372         # TODO: pull the value from the form
373         return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
374             cols)
376     # XXX new function
377     def do_list(self, property, reverse=0):
378         ''' list the items specified by property using the standard index for
379             the class
380         '''
381         propcl = self.properties[property]
382         if not isinstance(propcl, hyperdb.Multilink):
383             return '[List: not a Multilink]'
384         value = self.cl.get(self.nodeid, property)
385         if reverse:
386             value.reverse()
388         # render the sub-index into a string
389         fp = StringIO.StringIO()
390         try:
391             write_save = self.client.write
392             self.client.write = fp.write
393             index = IndexTemplate(self.client, self.templates, propcl.classname)
394             index.render(nodeids=value, show_display_form=0)
395         finally:
396             self.client.write = write_save
398         return fp.getvalue()
400     # XXX new function
401     def do_history(self, **args):
402         ''' list the history of the item
403         '''
404         if self.nodeid is None:
405             return "[History: node doesn't exist]"
407         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
408             '<tr class="list-header">',
409             '<td><span class="list-item"><strong>Date</strong></span></td>',
410             '<td><span class="list-item"><strong>User</strong></span></td>',
411             '<td><span class="list-item"><strong>Action</strong></span></td>',
412             '<td><span class="list-item"><strong>Args</strong></span></td>']
414         for id, date, user, action, args in self.cl.history(self.nodeid):
415             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
416                date, user, action, args))
417         l.append('</table>')
418         return '\n'.join(l)
420     # XXX new function
421     def do_submit(self):
422         ''' add a submit button for the item
423         '''
424         if self.nodeid:
425             return '<input type="submit" value="Submit Changes">'
426         elif self.form is not None:
427             return '<input type="submit" value="Submit New Entry">'
428         else:
429             return '[Submit: not called from item]'
433 #   INDEX TEMPLATES
435 class IndexTemplateReplace:
436     def __init__(self, globals, locals, props):
437         self.globals = globals
438         self.locals = locals
439         self.props = props
441     replace=re.compile(
442         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
443         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
444     def go(self, text):
445         return self.replace.sub(self, text)
447     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
448         if m.group('name'):
449             if m.group('name') in self.props:
450                 text = m.group('text')
451                 replace = IndexTemplateReplace(self.globals, {}, self.props)
452                 return replace.go(m.group('text'))
453             else:
454                 return ''
455         if m.group('display'):
456             command = m.group('command')
457             return eval(command, self.globals, self.locals)
458         print '*** unhandled match', m.groupdict()
460 class IndexTemplate(TemplateFunctions):
461     def __init__(self, client, templates, classname):
462         self.client = client
463         self.templates = templates
464         self.classname = classname
466         # derived
467         self.db = self.client.db
468         self.cl = self.db.classes[self.classname]
469         self.properties = self.cl.getprops()
471         TemplateFunctions.__init__(self)
473     col_re=re.compile(r'<property\s+name="([^>]+)">')
474     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
475             show_display_form=1, nodeids=None, show_customization=1):
476         self.filterspec = filterspec
478         w = self.client.write
480         # get the filter template
481         try:
482             filter_template = open(os.path.join(self.templates,
483                 self.classname+'.filter')).read()
484             all_filters = self.col_re.findall(filter_template)
485         except IOError, error:
486             if error.errno != errno.ENOENT: raise
487             filter_template = None
488             all_filters = []
490         # display the filter section
491         if (hasattr(self.client, 'FILTER_POSITION') and
492                 self.client.FILTER_POSITION in ('top and bottom', 'top')):
493             w('<form>\n')
494             self.filter_section(filter_template, filter, columns, group,
495                 all_filters, all_columns, show_display_form, show_customization)
496             w('</form>\n')
498         # make sure that the sorting doesn't get lost either
499         if sort:
500             w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
502         # XXX deviate from spec here ...
503         # load the index section template and figure the default columns from it
504         template = open(os.path.join(self.templates,
505             self.classname+'.index')).read()
506         all_columns = self.col_re.findall(template)
507         if not columns:
508             columns = []
509             for name in all_columns:
510                 columns.append(name)
511         else:
512             # re-sort columns to be the same order as all_columns
513             l = []
514             for name in all_columns:
515                 if name in columns:
516                     l.append(name)
517             columns = l
519         # now display the index section
520         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
521         w('<tr class="list-header">\n')
522         for name in columns:
523             cname = name.capitalize()
524             if show_display_form:
525                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
526                 anchor = "%s?%s"%(self.classname, sb)
527                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
528                     anchor, cname))
529             else:
530                 w('<td><span class="list-header">%s</span></td>\n'%cname)
531         w('</tr>\n')
533         # this stuff is used for group headings - optimise the group names
534         old_group = None
535         group_names = []
536         if group:
537             for name in group:
538                 if name[0] == '-': group_names.append(name[1:])
539                 else: group_names.append(name)
541         # now actually loop through all the nodes we get from the filter and
542         # apply the template
543         if nodeids is None:
544             nodeids = self.cl.filter(filterspec, sort, group)
545         for nodeid in nodeids:
546             # check for a group heading
547             if group_names:
548                 this_group = [self.cl.get(nodeid, name) for name in group_names]
549                 if this_group != old_group:
550                     l = []
551                     for name in group_names:
552                         prop = self.properties[name]
553                         if isinstance(prop, hyperdb.Link):
554                             group_cl = self.db.classes[prop.classname]
555                             key = group_cl.getkey()
556                             value = self.cl.get(nodeid, name)
557                             if value is None:
558                                 l.append('[unselected %s]'%prop.classname)
559                             else:
560                                 l.append(group_cl.get(self.cl.get(nodeid,
561                                     name), key))
562                         elif isinstance(prop, hyperdb.Multilink):
563                             group_cl = self.db.classes[prop.classname]
564                             key = group_cl.getkey()
565                             for value in self.cl.get(nodeid, name):
566                                 l.append(group_cl.get(value, key))
567                         else:
568                             value = self.cl.get(nodeid, name)
569                             if value is None:
570                                 value = '[empty %s]'%name
571                             else:
572                                 value = str(value)
573                             l.append(value)
574                     w('<tr class="section-bar">'
575                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
576                         len(columns), ', '.join(l)))
577                     old_group = this_group
579             # display this node's row
580             replace = IndexTemplateReplace(self.globals, locals(), columns)
581             self.nodeid = nodeid
582             w(replace.go(template))
583             self.nodeid = None
585         w('</table>')
587         # display the filter section
588         if (hasattr(self.client, 'FILTER_POSITION') and
589                 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
590             w('<form>\n')
591             self.filter_section(filter_template, filter, columns, group,
592                 all_filters, all_columns, show_display_form, show_customization)
593             w('</form>\n')
596     def filter_section(self, template, filter, columns, group, all_filters,
597             all_columns, show_display_form, show_customization):
599         w = self.client.write
601         if template and filter:
602             # display the filter section
603             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
604             w('<tr class="location-bar">')
605             w(' <th align="left" colspan="2">Filter specification...</th>')
606             w('</tr>')
607             replace = IndexTemplateReplace(self.globals, locals(), filter)
608             w(replace.go(template))
609             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
610             w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
611             w('</table>')
613         # now add in the filter/columns/group/etc config table form
614         w('<input type="hidden" name="show_customization" value="%s">' %
615             show_customization )
616         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
617         names = []
618         for name in self.properties.keys():
619             if name in all_filters or name in all_columns:
620                 names.append(name)
621         w('<tr class="location-bar">')
622         if show_customization:
623             action = '-'
624         else:
625             action = '+'
626             # hide the values for filters, columns and grouping in the form
627             # if the customization widget is not visible
628             for name in names:
629                 if all_filters and name in filter:
630                     w('<input type="hidden" name=":filter" value="%s">' % name)
631                 if all_columns and name in columns:
632                     w('<input type="hidden" name=":columns" value="%s">' % name)
633                 if all_columns and name in group:
634                     w('<input type="hidden" name=":group" value="%s">' % name)
636         if show_display_form:
637             # TODO: The widget style can go into the stylesheet
638             w('<th align="left" colspan=%s>'
639               '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
640               'customisation...</th></tr>\n'%(len(names)+1, action))
641             if show_customization:
642                 w('<tr class="location-bar"><th>&nbsp;</th>')
643                 for name in names:
644                     w('<th>%s</th>'%name.capitalize())
645                 w('</tr>\n')
647                 # Filter
648                 if all_filters:
649                     w('<tr><th width="1%" align=right class="location-bar">'
650                       'Filters</th>\n')
651                     for name in names:
652                         if name not in all_filters:
653                             w('<td>&nbsp;</td>')
654                             continue
655                         if name in filter: checked=' checked'
656                         else: checked=''
657                         w('<td align=middle>\n')
658                         w(' <input type="checkbox" name=":filter" value="%s" '
659                           '%s></td>\n'%(name, checked))
660                     w('</tr>\n')
662                 # Columns
663                 if all_columns:
664                     w('<tr><th width="1%" align=right class="location-bar">'
665                       'Columns</th>\n')
666                     for name in names:
667                         if name not in all_columns:
668                             w('<td>&nbsp;</td>')
669                             continue
670                         if name in columns: checked=' checked'
671                         else: checked=''
672                         w('<td align=middle>\n')
673                         w(' <input type="checkbox" name=":columns" value="%s"'
674                           '%s></td>\n'%(name, checked))
675                     w('</tr>\n')
677                     # Grouping
678                     w('<tr><th width="1%" align=right class="location-bar">'
679                       'Grouping</th>\n')
680                     for name in names:
681                         prop = self.properties[name]
682                         if name not in all_columns:
683                             w('<td>&nbsp;</td>')
684                             continue
685                         if name in group: checked=' checked'
686                         else: checked=''
687                         w('<td align=middle>\n')
688                         w(' <input type="checkbox" name=":group" value="%s"'
689                           '%s></td>\n'%(name, checked))
690                     w('</tr>\n')
692                 w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
693                 w('<td colspan="%s">'%len(names))
694                 w('<input type="submit" name="action" value="Redisplay"></td>')
695                 w('</tr>\n')
697             w('</table>\n')
699     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
700         l = []
701         w = l.append
702         for k, v in filterspec.items():
703             k = urllib.quote(k)
704             if type(v) == type([]):
705                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
706             else:
707                 w('%s=%s'%(k, urllib.quote(v)))
708         if columns:
709             w(':columns=%s'%','.join(map(urllib.quote, columns)))
710         if filter:
711             w(':filter=%s'%','.join(map(urllib.quote, filter)))
712         if group:
713             w(':group=%s'%','.join(map(urllib.quote, group)))
714         m = []
715         s_dir = ''
716         for name in sort:
717             dir = name[0]
718             if dir == '-':
719                 name = name[1:]
720             else:
721                 dir = ''
722             if sort_name == name:
723                 if dir == '-':
724                     s_dir = ''
725                 else:
726                     s_dir = '-'
727             else:
728                 m.append(dir+urllib.quote(name))
729         m.insert(0, s_dir+urllib.quote(sort_name))
730         # so things don't get completely out of hand, limit the sort to
731         # two columns
732         w(':sort=%s'%','.join(m[:2]))
733         return '&'.join(l)
736 #   ITEM TEMPLATES
738 class ItemTemplateReplace:
739     def __init__(self, globals, locals, cl, nodeid):
740         self.globals = globals
741         self.locals = locals
742         self.cl = cl
743         self.nodeid = nodeid
745     replace=re.compile(
746         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
747         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
748     def go(self, text):
749         return self.replace.sub(self, text)
751     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
752         if m.group('name'):
753             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
754                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
755                     self.nodeid)
756                 return replace.go(m.group('text'))
757             else:
758                 return ''
759         if m.group('display'):
760             command = m.group('command')
761             return eval(command, self.globals, self.locals)
762         print '*** unhandled match', m.groupdict()
765 class ItemTemplate(TemplateFunctions):
766     def __init__(self, client, templates, classname):
767         self.client = client
768         self.templates = templates
769         self.classname = classname
771         # derived
772         self.db = self.client.db
773         self.cl = self.db.classes[self.classname]
774         self.properties = self.cl.getprops()
776         TemplateFunctions.__init__(self)
778     def render(self, nodeid):
779         self.nodeid = nodeid
781         if (self.properties.has_key('type') and
782                 self.properties.has_key('content')):
783             pass
784             # XXX we really want to return this as a downloadable...
785             #  currently I handle this at a higher level by detecting 'file'
786             #  designators...
788         w = self.client.write
789         w('<form action="%s%s">'%(self.classname, nodeid))
790         s = open(os.path.join(self.templates, self.classname+'.item')).read()
791         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
792         w(replace.go(s))
793         w('</form>')
796 class NewItemTemplate(TemplateFunctions):
797     def __init__(self, client, templates, classname):
798         self.client = client
799         self.templates = templates
800         self.classname = classname
802         # derived
803         self.db = self.client.db
804         self.cl = self.db.classes[self.classname]
805         self.properties = self.cl.getprops()
807         TemplateFunctions.__init__(self)
809     def render(self, form):
810         self.form = form
811         w = self.client.write
812         c = self.classname
813         try:
814             s = open(os.path.join(self.templates, c+'.newitem')).read()
815         except:
816             s = open(os.path.join(self.templates, c+'.item')).read()
817         w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
818         for key in form.keys():
819             if key[0] == ':':
820                 value = form[key].value
821                 if type(value) != type([]): value = [value]
822                 for value in value:
823                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
824         replace = ItemTemplateReplace(self.globals, locals(), None, None)
825         w(replace.go(s))
826         w('</form>')
829 # $Log: not supported by cvs2svn $
830 # Revision 1.32  2001/10/22 03:25:01  richard
831 # Added configuration for:
832 #  . anonymous user access and registration (deny/allow)
833 #  . filter "widget" location on index page (top, bottom, both)
834 # Updated some documentation.
836 # Revision 1.31  2001/10/21 07:26:35  richard
837 # feature #473127: Filenames. I modified the file.index and htmltemplate
838 #  source so that the filename is used in the link and the creation
839 #  information is displayed.
841 # Revision 1.30  2001/10/21 04:44:50  richard
842 # bug #473124: UI inconsistency with Link fields.
843 #    This also prompted me to fix a fairly long-standing usability issue -
844 #    that of being able to turn off certain filters.
846 # Revision 1.29  2001/10/21 00:17:56  richard
847 # CGI interface view customisation section may now be hidden (patch from
848 #  Roch'e Compaan.)
850 # Revision 1.28  2001/10/21 00:00:16  richard
851 # Fixed Checklist function - wasn't always working on a list.
853 # Revision 1.27  2001/10/20 12:13:44  richard
854 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
856 # Revision 1.26  2001/10/14 10:55:00  richard
857 # Handle empty strings in HTML template Link function
859 # Revision 1.25  2001/10/09 07:25:59  richard
860 # Added the Password property type. See "pydoc roundup.password" for
861 # implementation details. Have updated some of the documentation too.
863 # Revision 1.24  2001/09/27 06:45:58  richard
864 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
865 # on the plain() template function to escape the text for HTML.
867 # Revision 1.23  2001/09/10 09:47:18  richard
868 # Fixed bug in the generation of links to Link/Multilink in indexes.
869 #   (thanks Hubert Hoegl)
870 # Added AssignedTo to the "classic" schema's item page.
872 # Revision 1.22  2001/08/30 06:01:17  richard
873 # Fixed missing import in mailgw :(
875 # Revision 1.21  2001/08/16 07:34:59  richard
876 # better CGI text searching - but hidden filter fields are disappearing...
878 # Revision 1.20  2001/08/15 23:43:18  richard
879 # Fixed some isFooTypes that I missed.
880 # Refactored some code in the CGI code.
882 # Revision 1.19  2001/08/12 06:32:36  richard
883 # using isinstance(blah, Foo) now instead of isFooType
885 # Revision 1.18  2001/08/07 00:24:42  richard
886 # stupid typo
888 # Revision 1.17  2001/08/07 00:15:51  richard
889 # Added the copyright/license notice to (nearly) all files at request of
890 # Bizar Software.
892 # Revision 1.16  2001/08/01 03:52:23  richard
893 # Checklist was using wrong name.
895 # Revision 1.15  2001/07/30 08:12:17  richard
896 # Added time logging and file uploading to the templates.
898 # Revision 1.14  2001/07/30 06:17:45  richard
899 # Features:
900 #  . Added ability for cgi newblah forms to indicate that the new node
901 #    should be linked somewhere.
902 # Fixed:
903 #  . Fixed the agument handling for the roundup-admin find command.
904 #  . Fixed handling of summary when no note supplied for newblah. Again.
905 #  . Fixed detection of no form in htmltemplate Field display.
907 # Revision 1.13  2001/07/30 02:37:53  richard
908 # Temporary measure until we have decent schema migration.
910 # Revision 1.12  2001/07/30 01:24:33  richard
911 # Handles new node display now.
913 # Revision 1.11  2001/07/29 09:31:35  richard
914 # oops
916 # Revision 1.10  2001/07/29 09:28:23  richard
917 # Fixed sorting by clicking on column headings.
919 # Revision 1.9  2001/07/29 08:27:40  richard
920 # Fixed handling of passed-in values in form elements (ie. during a
921 # drill-down)
923 # Revision 1.8  2001/07/29 07:01:39  richard
924 # Added vim command to all source so that we don't get no steenkin' tabs :)
926 # Revision 1.7  2001/07/29 05:36:14  richard
927 # Cleanup of the link label generation.
929 # Revision 1.6  2001/07/29 04:06:42  richard
930 # Fixed problem in link display when Link value is None.
932 # Revision 1.5  2001/07/28 08:17:09  richard
933 # fixed use of stylesheet
935 # Revision 1.4  2001/07/28 07:59:53  richard
936 # Replaced errno integers with their module values.
937 # De-tabbed templatebuilder.py
939 # Revision 1.3  2001/07/25 03:39:47  richard
940 # Hrm - displaying links to classes that don't specify a key property. I've
941 # got it defaulting to 'name', then 'title' and then a "random" property (first
942 # one returned by getprops().keys().
943 # Needs to be moved onto the Class I think...
945 # Revision 1.2  2001/07/22 12:09:32  richard
946 # Final commit of Grande Splite
948 # Revision 1.1  2001/07/22 11:58:35  richard
949 # More Grande Splite
952 # vim: set filetype=python ts=4 sw=4 et si