Code

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