Code

Added the Password property type. See "pydoc roundup.password" for
[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.25 2001-10-09 07:25:59 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             else: value = ''
217         if isinstance(propclass, hyperdb.Link):
218             linkname = propclass.classname
219             if value is None:
220                 return '[not assigned]'
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             l = []
230             for value in value:
231                 linkvalue = linkcl.get(value, k)
232                 l.append('<a href="%s%s">%s</a>'%(linkname, value, linkvalue))
233             return ', '.join(l)
234         return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
236 class Count(Base):
237     ''' for a Multilink property, display a count of the number of links in
238         the list
239     '''
240     def __call__(self, property, **args):
241         if not self.nodeid:
242             return '[Count: not called from item]'
243         propclass = self.properties[property]
244         value = self.cl.get(self.nodeid, property)
245         if isinstance(propclass, hyperdb.Multilink):
246             return str(len(value))
247         return '[Count: not a Multilink]'
249 # XXX pretty is definitely new ;)
250 class Reldate(Base):
251     ''' display a Date property in terms of an interval relative to the
252         current date (e.g. "+ 3w", "- 2d").
254         with the 'pretty' flag, make it pretty
255     '''
256     def __call__(self, property, pretty=0):
257         if not self.nodeid and self.form is None:
258             return '[Reldate: not called from item]'
259         propclass = self.properties[property]
260         if isinstance(not propclass, hyperdb.Date):
261             return '[Reldate: not a Date]'
262         if self.nodeid:
263             value = self.cl.get(self.nodeid, property)
264         else:
265             value = date.Date('.')
266         interval = value - date.Date('.')
267         if pretty:
268             if not self.nodeid:
269                 return 'now'
270             pretty = interval.pretty()
271             if pretty is None:
272                 pretty = value.pretty()
273             return pretty
274         return str(interval)
276 class Download(Base):
277     ''' show a Link("file") or Multilink("file") property using links that
278         allow you to download files
279     '''
280     def __call__(self, property, **args):
281         if not self.nodeid:
282             return '[Download: not called from item]'
283         propclass = self.properties[property]
284         value = self.cl.get(self.nodeid, property)
285         if isinstance(propclass, hyperdb.Link):
286             linkcl = self.db.classes[propclass.classname]
287             linkvalue = linkcl.get(value, k)
288             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
289         if isinstance(propclass, hyperdb.Multilink):
290             linkcl = self.db.classes[propclass.classname]
291             l = []
292             for value in value:
293                 linkvalue = linkcl.get(value, k)
294                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
295             return ', '.join(l)
296         return '[Download: not a link]'
299 class Checklist(Base):
300     ''' for a Link or Multilink property, display checkboxes for the available
301         choices to permit filtering
302     '''
303     def __call__(self, property, **args):
304         propclass = self.properties[property]
305         if self.nodeid:
306             value = self.cl.get(self.nodeid, property)
307         elif self.filterspec is not None:
308             value = self.filterspec.get(property, [])
309         else:
310             value = []
311         if (isinstance(propclass, hyperdb.Link) or
312                 isinstance(propclass, hyperdb.Multilink)):
313             linkcl = self.db.classes[propclass.classname]
314             l = []
315             k = linkcl.labelprop()
316             for optionid in linkcl.list():
317                 option = linkcl.get(optionid, k)
318                 if optionid in value or option in value:
319                     checked = 'checked'
320                 else:
321                     checked = ''
322                 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
323                     option, checked, property, option))
324             return '\n'.join(l)
325         return '[Checklist: not a link]'
327 class Note(Base):
328     ''' display a "note" field, which is a text area for entering a note to
329         go along with a change. 
330     '''
331     def __call__(self, rows=5, cols=80):
332        # TODO: pull the value from the form
333         return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
334             cols)
336 # XXX new function
337 class List(Base):
338     ''' list the items specified by property using the standard index for
339         the class
340     '''
341     def __call__(self, property, reverse=0):
342         propclass = self.properties[property]
343         if isinstance(not propclass, hyperdb.Multilink):
344             return '[List: not a Multilink]'
345         fp = StringIO.StringIO()
346         value = self.cl.get(self.nodeid, property)
347         if reverse:
348             value.reverse()
349         # TODO: really not happy with the way templates is passed on here
350         index(fp, self.templates, self.db, propclass.classname, nodeids=value,
351             show_display_form=0)
352         return fp.getvalue()
354 # XXX new function
355 class History(Base):
356     ''' list the history of the item
357     '''
358     def __call__(self, **args):
359         if self.nodeid is None:
360             return "[History: node doesn't exist]"
362         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
363             '<tr class="list-header">',
364             '<td><span class="list-item"><strong>Date</strong></span></td>',
365             '<td><span class="list-item"><strong>User</strong></span></td>',
366             '<td><span class="list-item"><strong>Action</strong></span></td>',
367             '<td><span class="list-item"><strong>Args</strong></span></td>']
369         for id, date, user, action, args in self.cl.history(self.nodeid):
370             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
371                date, user, action, args))
372         l.append('</table>')
373         return '\n'.join(l)
375 # XXX new function
376 class Submit(Base):
377     ''' add a submit button for the item
378     '''
379     def __call__(self):
380         if self.nodeid:
381             return '<input type="submit" value="Submit Changes">'
382         elif self.form is not None:
383             return '<input type="submit" value="Submit New Entry">'
384         else:
385             return '[Submit: not called from item]'
389 #   INDEX TEMPLATES
391 class IndexTemplateReplace:
392     def __init__(self, globals, locals, props):
393         self.globals = globals
394         self.locals = locals
395         self.props = props
397     def go(self, text, replace=re.compile(
398             r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
399             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
400         return replace.sub(self, text)
402     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
403         if m.group('name'):
404             if m.group('name') in self.props:
405                 text = m.group('text')
406                 replace = IndexTemplateReplace(self.globals, {}, self.props)
407                 return replace.go(m.group('text'))
408             else:
409                 return ''
410         if m.group('display'):
411             command = m.group('command')
412             return eval(command, self.globals, self.locals)
413         print '*** unhandled match', m.groupdict()
415 def sortby(sort_name, columns, filter, sort, group, filterspec):
416     l = []
417     w = l.append
418     for k, v in filterspec.items():
419         k = urllib.quote(k)
420         if type(v) == type([]):
421             w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
422         else:
423             w('%s=%s'%(k, urllib.quote(v)))
424     if columns:
425         w(':columns=%s'%','.join(map(urllib.quote, columns)))
426     if filter:
427         w(':filter=%s'%','.join(map(urllib.quote, filter)))
428     if group:
429         w(':group=%s'%','.join(map(urllib.quote, group)))
430     m = []
431     s_dir = ''
432     for name in sort:
433         dir = name[0]
434         if dir == '-':
435             name = name[1:]
436         else:
437             dir = ''
438         if sort_name == name:
439             if dir == '-':
440                 s_dir = ''
441             else:
442                 s_dir = '-'
443         else:
444             m.append(dir+urllib.quote(name))
445     m.insert(0, s_dir+urllib.quote(sort_name))
446     # so things don't get completely out of hand, limit the sort to two columns
447     w(':sort=%s'%','.join(m[:2]))
448     return '&'.join(l)
450 def index(client, templates, db, classname, filterspec={}, filter=[],
451         columns=[], sort=[], group=[], show_display_form=1, nodeids=None,
452         col_re=re.compile(r'<property\s+name="([^>]+)">')):
453     globals = {
454         'plain': Plain(db, templates, classname, filterspec=filterspec),
455         'field': Field(db, templates, classname, filterspec=filterspec),
456         'menu': Menu(db, templates, classname, filterspec=filterspec),
457         'link': Link(db, templates, classname, filterspec=filterspec),
458         'count': Count(db, templates, classname, filterspec=filterspec),
459         'reldate': Reldate(db, templates, classname, filterspec=filterspec),
460         'download': Download(db, templates, classname, filterspec=filterspec),
461         'checklist': Checklist(db, templates, classname, filterspec=filterspec),
462         'list': List(db, templates, classname, filterspec=filterspec),
463         'history': History(db, templates, classname, filterspec=filterspec),
464         'submit': Submit(db, templates, classname, filterspec=filterspec),
465         'note': Note(db, templates, classname, filterspec=filterspec)
466     }
467     cl = db.classes[classname]
468     properties = cl.getprops()
469     w = client.write
470     w('<form>')
472     try:
473         template = open(os.path.join(templates, classname+'.filter')).read()
474         all_filters = col_re.findall(template)
475     except IOError, error:
476         if error.errno != errno.ENOENT: raise
477         template = None
478         all_filters = []
479     if template and filter:
480         # display the filter section
481         w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
482         w('<tr class="location-bar">')
483         w(' <th align="left" colspan="2">Filter specification...</th>')
484         w('</tr>')
485         replace = IndexTemplateReplace(globals, locals(), filter)
486         w(replace.go(template))
487         w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
488         w('<td><input type="submit" value="Redisplay"></td></tr>')
489         w('</table>')
491     # If the filters aren't being displayed, then hide their current
492     # value in the form
493     if not filter:
494         for k, v in filterspec.items():
495             if type(v) == type([]): v = ','.join(v)
496             w('<input type="hidden" name="%s" value="%s">'%(k, v))
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(templates, classname+'.index')).read()
505     all_columns = col_re.findall(template)
506     if not columns:
507         columns = []
508         for name in all_columns:
509             columns.append(name)
510     else:
511         # re-sort columns to be the same order as all_columns
512         l = []
513         for name in all_columns:
514             if name in columns:
515                 l.append(name)
516         columns = l
518     # now display the index section
519     w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
520     w('<tr class="list-header">\n')
521     for name in columns:
522         cname = name.capitalize()
523         if show_display_form:
524             anchor = "%s?%s"%(classname, sortby(name, columns, filter,
525                 sort, group, filterspec))
526             w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
527                 anchor, cname))
528         else:
529             w('<td><span class="list-header">%s</span></td>\n'%cname)
530     w('</tr>\n')
532     # this stuff is used for group headings - optimise the group names
533     old_group = None
534     group_names = []
535     if group:
536         for name in group:
537             if name[0] == '-': group_names.append(name[1:])
538             else: group_names.append(name)
540     # now actually loop through all the nodes we get from the filter and
541     # apply the template
542     if nodeids is None:
543         nodeids = cl.filter(filterspec, sort, group)
544     for nodeid in nodeids:
545         # check for a group heading
546         if group_names:
547             this_group = [cl.get(nodeid, name) for name in group_names]
548             if this_group != old_group:
549                 l = []
550                 for name in group_names:
551                     prop = properties[name]
552                     if isinstance(prop, hyperdb.Link):
553                         group_cl = db.classes[prop.classname]
554                         key = group_cl.getkey()
555                         value = cl.get(nodeid, name)
556                         if value is None:
557                             l.append('[unselected %s]'%prop.classname)
558                         else:
559                             l.append(group_cl.get(cl.get(nodeid, name), key))
560                     elif isinstance(prop, hyperdb.Multilink):
561                         group_cl = db.classes[prop.classname]
562                         key = group_cl.getkey()
563                         for value in cl.get(nodeid, name):
564                             l.append(group_cl.get(value, key))
565                     else:
566                         value = cl.get(nodeid, name)
567                         if value is None:
568                             value = '[empty %s]'%name
569                         l.append(value)
570                 w('<tr class="section-bar">'
571                   '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
572                     len(columns), ', '.join(l)))
573                 old_group = this_group
575         # display this node's row
576         for value in globals.values():
577             if hasattr(value, 'nodeid'):
578                 value.nodeid = nodeid
579         replace = IndexTemplateReplace(globals, locals(), columns)
580         w(replace.go(template))
582     w('</table>')
584     if not show_display_form:
585         return
587     # now add in the filter/columns/group/etc config table form
588     w('<p>')
589     w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
590     names = []
591     for name in cl.getprops().keys():
592         if name in all_filters or name in all_columns:
593             names.append(name)
594     w('<tr class="location-bar">')
595     w('<th align="left" colspan=%s>View customisation...</th></tr>\n'%
596         (len(names)+1))
597     w('<tr class="location-bar"><th>&nbsp;</th>')
598     for name in names:
599         w('<th>%s</th>'%name.capitalize())
600     w('</tr>\n')
602     # filter
603     if all_filters:
604         w('<tr><th width="1%" align=right class="location-bar">Filters</th>\n')
605         for name in names:
606             if name not in all_filters:
607                 w('<td>&nbsp;</td>')
608                 continue
609             if name in filter: checked=' checked'
610             else: checked=''
611             w('<td align=middle>\n')
612             w(' <input type="checkbox" name=":filter" value="%s" %s></td>\n'%(
613                 name, checked))
614         w('</tr>\n')
616     # columns
617     if all_columns:
618         w('<tr><th width="1%" align=right class="location-bar">Columns</th>\n')
619         for name in names:
620             if name not in all_columns:
621                 w('<td>&nbsp;</td>')
622                 continue
623             if name in columns: checked=' checked'
624             else: checked=''
625             w('<td align=middle>\n')
626             w(' <input type="checkbox" name=":columns" value="%s" %s></td>\n'%(
627                 name, checked))
628         w('</tr>\n')
630         # group
631         w('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n')
632         for name in names:
633             prop = properties[name]
634             if name not in all_columns:
635                 w('<td>&nbsp;</td>')
636                 continue
637             if name in group: checked=' checked'
638             else: checked=''
639             w('<td align=middle>\n')
640             w(' <input type="checkbox" name=":group" value="%s" %s></td>\n'%(
641                 name, checked))
642         w('</tr>\n')
644     w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
645     w('<td colspan="%s">'%len(names))
646     w('<input type="submit" value="Redisplay"></td></tr>\n')
647     w('</table>\n')
648     w('</form>\n')
652 #   ITEM TEMPLATES
654 class ItemTemplateReplace:
655     def __init__(self, globals, locals, cl, nodeid):
656         self.globals = globals
657         self.locals = locals
658         self.cl = cl
659         self.nodeid = nodeid
661     def go(self, text, replace=re.compile(
662             r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
663             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
664         return replace.sub(self, text)
666     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
667         if m.group('name'):
668             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
669                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
670                     self.nodeid)
671                 return replace.go(m.group('text'))
672             else:
673                 return ''
674         if m.group('display'):
675             command = m.group('command')
676             return eval(command, self.globals, self.locals)
677         print '*** unhandled match', m.groupdict()
679 def item(client, templates, db, classname, nodeid, replace=re.compile(
680             r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
681             r'(?P<endprop></property>)|'
682             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
684     globals = {
685         'plain': Plain(db, templates, classname, nodeid),
686         'field': Field(db, templates, classname, nodeid),
687         'menu': Menu(db, templates, classname, nodeid),
688         'link': Link(db, templates, classname, nodeid),
689         'count': Count(db, templates, classname, nodeid),
690         'reldate': Reldate(db, templates, classname, nodeid),
691         'download': Download(db, templates, classname, nodeid),
692         'checklist': Checklist(db, templates, classname, nodeid),
693         'list': List(db, templates, classname, nodeid),
694         'history': History(db, templates, classname, nodeid),
695         'submit': Submit(db, templates, classname, nodeid),
696         'note': Note(db, templates, classname, nodeid)
697     }
699     cl = db.classes[classname]
700     properties = cl.getprops()
702     if properties.has_key('type') and properties.has_key('content'):
703         pass
704         # XXX we really want to return this as a downloadable...
705         #  currently I handle this at a higher level by detecting 'file'
706         #  designators...
708     w = client.write
709     w('<form action="%s%s">'%(classname, nodeid))
710     s = open(os.path.join(templates, classname+'.item')).read()
711     replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
712     w(replace.go(s))
713     w('</form>')
716 def newitem(client, templates, db, classname, form, 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)):
720     globals = {
721         'plain': Plain(db, templates, classname, form=form),
722         'field': Field(db, templates, classname, form=form),
723         'menu': Menu(db, templates, classname, form=form),
724         'link': Link(db, templates, classname, form=form),
725         'count': Count(db, templates, classname, form=form),
726         'reldate': Reldate(db, templates, classname, form=form),
727         'download': Download(db, templates, classname, form=form),
728         'checklist': Checklist(db, templates, classname, form=form),
729         'list': List(db, templates, classname, form=form),
730         'history': History(db, templates, classname, form=form),
731         'submit': Submit(db, templates, classname, form=form),
732         'note': Note(db, templates, classname, form=form)
733     }
735     cl = db.classes[classname]
736     properties = cl.getprops()
738     w = client.write
739     try:
740         s = open(os.path.join(templates, classname+'.newitem')).read()
741     except:
742         s = open(os.path.join(templates, classname+'.item')).read()
743     w('<form action="new%s" method="POST" enctype="multipart/form-data">'%classname)
744     for key in form.keys():
745         if key[0] == ':':
746             value = form[key].value
747             if type(value) != type([]): value = [value]
748             for value in value:
749                 w('<input type="hidden" name="%s" value="%s">'%(key, value))
750     replace = ItemTemplateReplace(globals, locals(), None, None)
751     w(replace.go(s))
752     w('</form>')
755 # $Log: not supported by cvs2svn $
756 # Revision 1.24  2001/09/27 06:45:58  richard
757 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
758 # on the plain() template function to escape the text for HTML.
760 # Revision 1.23  2001/09/10 09:47:18  richard
761 # Fixed bug in the generation of links to Link/Multilink in indexes.
762 #   (thanks Hubert Hoegl)
763 # Added AssignedTo to the "classic" schema's item page.
765 # Revision 1.22  2001/08/30 06:01:17  richard
766 # Fixed missing import in mailgw :(
768 # Revision 1.21  2001/08/16 07:34:59  richard
769 # better CGI text searching - but hidden filter fields are disappearing...
771 # Revision 1.20  2001/08/15 23:43:18  richard
772 # Fixed some isFooTypes that I missed.
773 # Refactored some code in the CGI code.
775 # Revision 1.19  2001/08/12 06:32:36  richard
776 # using isinstance(blah, Foo) now instead of isFooType
778 # Revision 1.18  2001/08/07 00:24:42  richard
779 # stupid typo
781 # Revision 1.17  2001/08/07 00:15:51  richard
782 # Added the copyright/license notice to (nearly) all files at request of
783 # Bizar Software.
785 # Revision 1.16  2001/08/01 03:52:23  richard
786 # Checklist was using wrong name.
788 # Revision 1.15  2001/07/30 08:12:17  richard
789 # Added time logging and file uploading to the templates.
791 # Revision 1.14  2001/07/30 06:17:45  richard
792 # Features:
793 #  . Added ability for cgi newblah forms to indicate that the new node
794 #    should be linked somewhere.
795 # Fixed:
796 #  . Fixed the agument handling for the roundup-admin find command.
797 #  . Fixed handling of summary when no note supplied for newblah. Again.
798 #  . Fixed detection of no form in htmltemplate Field display.
800 # Revision 1.13  2001/07/30 02:37:53  richard
801 # Temporary measure until we have decent schema migration.
803 # Revision 1.12  2001/07/30 01:24:33  richard
804 # Handles new node display now.
806 # Revision 1.11  2001/07/29 09:31:35  richard
807 # oops
809 # Revision 1.10  2001/07/29 09:28:23  richard
810 # Fixed sorting by clicking on column headings.
812 # Revision 1.9  2001/07/29 08:27:40  richard
813 # Fixed handling of passed-in values in form elements (ie. during a
814 # drill-down)
816 # Revision 1.8  2001/07/29 07:01:39  richard
817 # Added vim command to all source so that we don't get no steenkin' tabs :)
819 # Revision 1.7  2001/07/29 05:36:14  richard
820 # Cleanup of the link label generation.
822 # Revision 1.6  2001/07/29 04:06:42  richard
823 # Fixed problem in link display when Link value is None.
825 # Revision 1.5  2001/07/28 08:17:09  richard
826 # fixed use of stylesheet
828 # Revision 1.4  2001/07/28 07:59:53  richard
829 # Replaced errno integers with their module values.
830 # De-tabbed templatebuilder.py
832 # Revision 1.3  2001/07/25 03:39:47  richard
833 # Hrm - displaying links to classes that don't specify a key property. I've
834 # got it defaulting to 'name', then 'title' and then a "random" property (first
835 # one returned by getprops().keys().
836 # Needs to be moved onto the Class I think...
838 # Revision 1.2  2001/07/22 12:09:32  richard
839 # Final commit of Grande Splite
841 # Revision 1.1  2001/07/22 11:58:35  richard
842 # More Grande Splite
845 # vim: set filetype=python ts=4 sw=4 et si