Code

85ddc719498a393b0f1eb537b441a257915d0fca
[roundup.git] / template.py
1 # $Id: template.py,v 1.5 2001-07-20 07:34:43 richard Exp $
3 import os, re, StringIO, urllib, cgi
5 import hyperdb, date
7 class Base:
8     def __init__(self, db, classname, nodeid=None, form=None):
9         self.db, self.classname, self.nodeid = db, classname, nodeid
10         self.form = form
11         self.cl = self.db.classes[self.classname]
12         self.properties = self.cl.getprops()
14 class Plain(Base):
15     ''' display a String property directly;
17         display a Date property in a specified time zone with an option to
18         omit the time from the date stamp;
20         for a Link or Multilink property, display the key strings of the
21         linked nodes (or the ids if the linked class has no key property)
22     '''
23     def __call__(self, property):
24         if not self.nodeid and self.form is None:
25             return '[Field: not called from item]'
26         propclass = self.properties[property]
27         if self.nodeid:
28             value = self.cl.get(self.nodeid, property)
29         else:
30             # TODO: pull the value from the form
31             if propclass.isMultilinkType: value = []
32             else: value = ''
33         if propclass.isStringType:
34             if value is None: value = ''
35             else: value = str(value)
36         elif propclass.isDateType:
37             value = str(value)
38         elif propclass.isIntervalType:
39             value = str(value)
40         elif propclass.isLinkType:
41             linkcl = self.db.classes[propclass.classname]
42             if value: value = str(linkcl.get(value, linkcl.getkey()))
43             else: value = '[unselected]'
44         elif propclass.isMultilinkType:
45             linkcl = self.db.classes[propclass.classname]
46             k = linkcl.getkey()
47             value = ', '.join([linkcl.get(i, k) for i in value])
48         else:
49             s = 'Plain: bad propclass "%s"'%propclass
50         return value
52 class Field(Base):
53     ''' display a property like the plain displayer, but in a text field
54         to be edited
55     '''
56     def __call__(self, property, size=None, height=None, showid=0):
57         if not self.nodeid and self.form is None:
58             return '[Field: not called from item]'
59         propclass = self.properties[property]
60         if self.nodeid:
61             value = self.cl.get(self.nodeid, property)
62         else:
63             # TODO: pull the value from the form
64             if propclass.isMultilinkType: value = []
65             else: value = ''
66         if (propclass.isStringType or propclass.isDateType or
67                 propclass.isIntervalType):
68             size = size or 30
69             if value is None:
70                 value = ''
71             else:
72                 value = cgi.escape(value)
73                 value = '"'.join(value.split('"'))
74             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
75         elif propclass.isLinkType:
76             linkcl = self.db.classes[propclass.classname]
77             l = ['<select name="%s">'%property]
78             k = linkcl.getkey()
79             for optionid in linkcl.list():
80                 option = linkcl.get(optionid, k)
81                 s = ''
82                 if optionid == value:
83                     s = 'selected '
84                 if showid:
85                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
86                 else:
87                     lab = option
88                 if size is not None and len(lab) > size:
89                     lab = lab[:size-3] + '...'
90                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
91             l.append('</select>')
92             s = '\n'.join(l)
93         elif propclass.isMultilinkType:
94             linkcl = self.db.classes[propclass.classname]
95             list = linkcl.list()
96             height = height or min(len(list), 7)
97             l = ['<select multiple name="%s" size="%s">'%(property, height)]
98             k = linkcl.getkey()
99             for optionid in list:
100                 option = linkcl.get(optionid, k)
101                 s = ''
102                 if optionid in value:
103                     s = 'selected '
104                 if showid:
105                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
106                 else:
107                     lab = option
108                 if size is not None and len(lab) > size:
109                     lab = lab[:size-3] + '...'
110                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
111             l.append('</select>')
112             s = '\n'.join(l)
113         else:
114             s = 'Plain: bad propclass "%s"'%propclass
115         return s
117 class Menu(Base):
118     ''' for a Link property, display a menu of the available choices
119     '''
120     def __call__(self, property, size=None, height=None, showid=0):
121         propclass = self.properties[property]
122         if self.nodeid:
123             value = self.cl.get(self.nodeid, property)
124         else:
125             # TODO: pull the value from the form
126             if propclass.isMultilinkType: value = []
127             else: value = None
128         if propclass.isLinkType:
129             linkcl = self.db.classes[propclass.classname]
130             l = ['<select name="%s">'%property]
131             k = linkcl.getkey()
132             for optionid in linkcl.list():
133                 option = linkcl.get(optionid, k)
134                 s = ''
135                 if optionid == value:
136                     s = 'selected '
137                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
138             l.append('</select>')
139             return '\n'.join(l)
140         if propclass.isMultilinkType:
141             linkcl = self.db.classes[propclass.classname]
142             list = linkcl.list()
143             height = height or min(len(list), 7)
144             l = ['<select multiple name="%s" size="%s">'%(property, height)]
145             k = linkcl.getkey()
146             for optionid in list:
147                 option = linkcl.get(optionid, k)
148                 s = ''
149                 if optionid in value:
150                     s = 'selected '
151                 if showid:
152                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
153                 else:
154                     lab = option
155                 if size is not None and len(lab) > size:
156                     lab = lab[:size-3] + '...'
157                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
158             l.append('</select>')
159             return '\n'.join(l)
160         return '[Menu: not a link]'
162 #XXX deviates from spec
163 class Link(Base):
164     ''' for a Link or Multilink property, display the names of the linked
165         nodes, hyperlinked to the item views on those nodes
166         for other properties, link to this node with the property as the text
167     '''
168     def __call__(self, property=None, **args):
169         if not self.nodeid and self.form is None:
170             return '[Link: not called from item]'
171         propclass = self.properties[property]
172         if self.nodeid:
173             value = self.cl.get(self.nodeid, property)
174         else:
175             if propclass.isMultilinkType: value = []
176             else: value = ''
177         if propclass.isLinkType:
178             linkcl = self.db.classes[propclass.classname]
179             linkvalue = linkcl.get(value, k)
180             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
181         if propclass.isMultilinkType:
182             linkcl = self.db.classes[propclass.classname]
183             l = []
184             for value in value:
185                 linkvalue = linkcl.get(value, k)
186                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
187             return ', '.join(l)
188         return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
190 class Count(Base):
191     ''' for a Multilink property, display a count of the number of links in
192         the list
193     '''
194     def __call__(self, property, **args):
195         if not self.nodeid:
196             return '[Count: not called from item]'
197         propclass = self.properties[property]
198         value = self.cl.get(self.nodeid, property)
199         if propclass.isMultilinkType:
200             return str(len(value))
201         return '[Count: not a Multilink]'
203 # XXX pretty is definitely new ;)
204 class Reldate(Base):
205     ''' display a Date property in terms of an interval relative to the
206         current date (e.g. "+ 3w", "- 2d").
208         with the 'pretty' flag, make it pretty
209     '''
210     def __call__(self, property, pretty=0):
211         if not self.nodeid and self.form is None:
212             return '[Reldate: not called from item]'
213         propclass = self.properties[property]
214         if not propclass.isDateType:
215             return '[Reldate: not a Date]'
216         if self.nodeid:
217             value = self.cl.get(self.nodeid, property)
218         else:
219             value = date.Date('.')
220         interval = value - date.Date('.')
221         if pretty:
222             if not self.nodeid:
223                 return 'now'
224             pretty = interval.pretty()
225             if pretty is None:
226                 pretty = value.pretty()
227             return pretty
228         return str(interval)
230 class Download(Base):
231     ''' show a Link("file") or Multilink("file") property using links that
232         allow you to download files
233     '''
234     def __call__(self, property, **args):
235         if not self.nodeid:
236             return '[Download: not called from item]'
237         propclass = self.properties[property]
238         value = self.cl.get(self.nodeid, property)
239         if propclass.isLinkType:
240             linkcl = self.db.classes[propclass.classname]
241             linkvalue = linkcl.get(value, k)
242             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
243         if propclass.isMultilinkType:
244             linkcl = self.db.classes[propclass.classname]
245             l = []
246             for value in value:
247                 linkvalue = linkcl.get(value, k)
248                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
249             return ', '.join(l)
250         return '[Download: not a link]'
253 class Checklist(Base):
254     ''' for a Link or Multilink property, display checkboxes for the available
255         choices to permit filtering
256     '''
257     def __call__(self, property, **args):
258         propclass = self.properties[property]
259         if self.nodeid:
260             value = self.cl.get(self.nodeid, property)
261         else:
262             value = []
263         if propclass.isLinkType or propclass.isMultilinkType:
264             linkcl = self.db.classes[propclass.classname]
265             l = []
266             k = linkcl.getkey()
267             for optionid in linkcl.list():
268                 option = linkcl.get(optionid, k)
269                 if optionid in value:
270                     checked = 'checked'
271                 else:
272                     checked = ''
273                 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
274                     option, checked, propclass.classname, option))
275             return '\n'.join(l)
276         return '[Checklist: not a link]'
278 class Note(Base):
279     ''' display a "note" field, which is a text area for entering a note to
280         go along with a change. 
281     '''
282     def __call__(self, rows=5, cols=80):
283        # TODO: pull the value from the form
284         return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
285             cols)
287 # XXX new function
288 class List(Base):
289     ''' list the items specified by property using the standard index for
290         the class
291     '''
292     def __call__(self, property, **args):
293         propclass = self.properties[property]
294         if not propclass.isMultilinkType:
295             return '[List: not a Multilink]'
296         fp = StringIO.StringIO()
297         args['show_display_form'] = 0
298         value = self.cl.get(self.nodeid, property)
299         index(fp, self.db, propclass.classname, nodeids=value,
300             show_display_form=0)
301         return fp.getvalue()
303 # XXX new function
304 class History(Base):
305     ''' list the history of the item
306     '''
307     def __call__(self, **args):
308         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
309             '<tr class="list-header">',
310             '<td><span class="list-item"><strong>Date</strong></span></td>',
311             '<td><span class="list-item"><strong>User</strong></span></td>',
312             '<td><span class="list-item"><strong>Action</strong></span></td>',
313             '<td><span class="list-item"><strong>Args</strong></span></td>']
315         for id, date, user, action, args in self.cl.history(self.nodeid):
316             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
317                date, user, action, args))
318         l.append('</table>')
319         return '\n'.join(l)
321 # XXX new function
322 class Submit(Base):
323     ''' add a submit button for the item
324     '''
325     def __call__(self):
326         if self.nodeid:
327             return '<input type="submit" value="Submit Changes">'
328         elif self.form is not None:
329             return '<input type="submit" value="Submit New Entry">'
330         else:
331             return '[Submit: not called from item]'
335 #   INDEX TEMPLATES
337 class IndexTemplateReplace:
338     def __init__(self, globals, locals, props):
339         self.globals = globals
340         self.locals = locals
341         self.props = props
343     def go(self, text, replace=re.compile(
344             r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
345             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
346         return replace.sub(self, text)
347         
348     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
349         if m.group('name'):
350             if m.group('name') in self.props:
351                 text = m.group('text')
352                 replace = IndexTemplateReplace(self.globals, {}, self.props)
353                 return replace.go(m.group('text'))
354             else:
355                 return ''
356         if m.group('display'):
357             command = m.group('command')
358             return eval(command, self.globals, self.locals)
359         print '*** unhandled match', m.groupdict()
361 def sortby(sort_name, columns, filter, sort, group, filterspec):
362     l = []
363     w = l.append
364     for k, v in filterspec.items():
365         k = urllib.quote(k)
366         if type(v) == type([]):
367             w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
368         else:
369             w('%s=%s'%(k, urllib.quote(v)))
370     if columns:
371         w(':columns=%s'%','.join(map(urllib.quote, columns)))
372     if filter:
373         w(':filter=%s'%','.join(map(urllib.quote, filter)))
374     if group:
375         w(':group=%s'%','.join(map(urllib.quote, group)))
376     m = []
377     s_dir = ''
378     for name in sort:
379         dir = name[0]
380         if dir == '-':
381             dir = ''
382         else:
383             name = name[1:]
384         if sort_name == name:
385             if dir == '':
386                 s_dir = '-'
387             elif dir == '-':
388                 s_dir = ''
389         else:
390             m.append(dir+urllib.quote(name))
391     m.insert(0, s_dir+urllib.quote(sort_name))
392     # so things don't get completely out of hand, limit the sort to two columns
393     w(':sort=%s'%','.join(m[:2]))
394     return '&'.join(l)
396 def index(fp, db, classname, filterspec={}, filter=[], columns=[], sort=[],
397             group=[], show_display_form=1, nodeids=None,
398             col_re=re.compile(r'<property\s+name="([^>]+)">')):
400     globals = {
401         'plain': Plain(db, classname, form={}),
402         'field': Field(db, classname, form={}),
403         'menu': Menu(db, classname, form={}),
404         'link': Link(db, classname, form={}),
405         'count': Count(db, classname, form={}),
406         'reldate': Reldate(db, classname, form={}),
407         'download': Download(db, classname, form={}),
408         'checklist': Checklist(db, classname, form={}),
409         'list': List(db, classname, form={}),
410         'history': History(db, classname, form={}),
411         'submit': Submit(db, classname, form={}),
412         'note': Note(db, classname, form={})
413     }
414     cl = db.classes[classname]
415     properties = cl.getprops()
416     w = fp.write
418     try:
419         template = open(os.path.join('templates', classname+'.filter')).read()
420         all_filters = col_re.findall(template)
421     except IOError, error:
422         if error.errno != 2: raise
423         template = None
424         all_filters = []
425     if template and filter:
426         # display the filter section
427         w('<form>')
428         w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
429         w('<tr class="location-bar">')
430         w(' <th align="left" colspan="2">Filter specification...</th>')
431         w('</tr>')
432         replace = IndexTemplateReplace(globals, locals(), filter)
433         w(replace.go(template))
434         if columns:
435             w('<input type="hidden" name=":columns" value="%s">'%','.join(columns))
436         if filter:
437             w('<input type="hidden" name=":filter" value="%s">'%','.join(filter))
438         if sort:
439             w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
440         if group:
441             w('<input type="hidden" name=":group" value="%s">'%','.join(group))
442         for k, v in filterspec.items():
443             if type(v) == type([]): v = ','.join(v)
444             w('<input type="hidden" name="%s" value="%s">'%(k, v))
445         w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
446         w('<td><input type="submit" value="Redisplay"></td></tr>')
447         w('</table>')
448         w('</form>')
450     # XXX deviate from spec here ...
451     # load the index section template and figure the default columns from it
452     template = open(os.path.join('templates', classname+'.index')).read()
453     all_columns = col_re.findall(template)
454     if not columns:
455         columns = []
456         for name in all_columns:
457             columns.append(name)
458     else:
459         # re-sort columns to be the same order as all_columns
460         l = []
461         for name in all_columns:
462             if name in columns:
463                 l.append(name)
464         columns = l
466     # now display the index section
467     w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
468     w('<tr class="list-header">')
469     for name in columns:
470         cname = name.capitalize()
471         if show_display_form:
472             anchor = "%s?%s"%(classname, sortby(name, columns, filter,
473                 sort, group, filterspec))
474             w('<td><span class="list-item"><a href="%s">%s</a></span></td>'%(
475                 anchor, cname))
476         else:
477             w('<td><span class="list-item">%s</span></td>'%cname)
478     w('</tr>')
480     # this stuff is used for group headings - optimise the group names
481     old_group = None
482     group_names = []
483     if group:
484         for name in group:
485             if name[0] == '-': group_names.append(name[1:])
486             else: group_names.append(name)
488     # now actually loop through all the nodes we get from the filter and
489     # apply the template
490     if nodeids is None:
491         nodeids = cl.filter(filterspec, sort, group)
492     for nodeid in nodeids:
493         # check for a group heading
494         if group_names:
495             this_group = [cl.get(nodeid, name) for name in group_names]
496             if this_group != old_group:
497                 l = []
498                 for name in group_names:
499                     prop = properties[name]
500                     if prop.isLinkType:
501                         group_cl = db.classes[prop.classname]
502                         key = group_cl.getkey()
503                         value = cl.get(nodeid, name)
504                         if value is None:
505                             l.append('[unselected %s]'%prop.classname)
506                         else:
507                             l.append(group_cl.get(cl.get(nodeid, name), key))
508                     elif prop.isMultilinkType:
509                         group_cl = db.classes[prop.classname]
510                         key = group_cl.getkey()
511                         for value in cl.get(nodeid, name):
512                             l.append(group_cl.get(value, key))
513                     else:
514                         value = cl.get(nodeid, name)
515                         if value is None:
516                             value = '[empty %s]'%name
517                         l.append(value)
518                 w('<tr class="list-header">'
519                   '<td align=left colspan=%s><strong>%s</strong></td></tr>'%(
520                     len(columns), ', '.join(l)))
521                 old_group = this_group
523         # display this node's row
524         for value in globals.values():
525             if hasattr(value, 'nodeid'):
526                 value.nodeid = nodeid
527         replace = IndexTemplateReplace(globals, locals(), columns)
528         w(replace.go(template))
530     w('</table>')
532     if not show_display_form:
533         return
535     # now add in the filter/columns/group/etc config table form
536     w('<p><form>')
537     w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
538     for k,v in filterspec.items():
539         if type(v) == type([]): v = ','.join(v)
540         w('<input type="hidden" name="%s" value="%s">'%(k, v))
541     if sort:
542         w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
543     names = []
544     for name in cl.getprops().keys():
545         if name in all_filters or name in all_columns:
546             names.append(name)
547     w('<tr class="location-bar">')
548     w('<th align="left" colspan=%s>View customisation...</th></tr>'%
549         (len(names)+1))
550     w('<tr class="location-bar"><th>&nbsp;</th>')
551     for name in names:
552         w('<th>%s</th>'%name.capitalize())
553     w('</tr>')
555     # filter
556     if all_filters:
557         w('<tr><th width="1%" align=right class="location-bar">Filters</th>')
558         for name in names:
559             if name not in all_filters:
560                 w('<td>&nbsp;</td>')
561                 continue
562             if name in filter: checked=' checked'
563             else: checked=''
564             w('<td align=middle>')
565             w('<input type="checkbox" name=":filter" value="%s" %s></td>'%(name,
566                 checked))
567         w('</tr>')
569     # columns
570     if all_columns:
571         w('<tr><th width="1%" align=right class="location-bar">Columns</th>')
572         for name in names:
573             if name not in all_columns:
574                 w('<td>&nbsp;</td>')
575                 continue
576             if name in columns: checked=' checked'
577             else: checked=''
578             w('<td align=middle>')
579             w('<input type="checkbox" name=":columns" value="%s" %s></td>'%(
580                 name, checked))
581         w('</tr>')
583         # group
584         w('<tr><th width="1%" align=right class="location-bar">Grouping</th>')
585         for name in names:
586             prop = properties[name]
587             if name not in all_columns:
588                 w('<td>&nbsp;</td>')
589                 continue
590             if name in group: checked=' checked'
591             else: checked=''
592             w('<td align=middle>')
593             w('<input type="checkbox" name=":group" value="%s" %s></td>'%(
594                 name, checked))
595         w('</tr>')
597     w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
598     w('<td colspan="%s">'%len(names))
599     w('<input type="submit" value="Redisplay"></td></tr>')
600     w('</table>')
601     w('</form>')
605 #   ITEM TEMPLATES
607 class ItemTemplateReplace:
608     def __init__(self, globals, locals, cl, nodeid):
609         self.globals = globals
610         self.locals = locals
611         self.cl = cl
612         self.nodeid = nodeid
614     def go(self, text, replace=re.compile(
615             r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
616             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
617         return replace.sub(self, text)
619     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
620         if m.group('name'):
621             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
622                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
623                     self.nodeid)
624                 return replace.go(m.group('text'))
625             else:
626                 return ''
627         if m.group('display'):
628             command = m.group('command')
629             return eval(command, self.globals, self.locals)
630         print '*** unhandled match', m.groupdict()
632 def item(fp, db, classname, nodeid, replace=re.compile(
633             r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
634             r'(?P<endprop></property>)|'
635             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
637     globals = {
638         'plain': Plain(db, classname, nodeid),
639         'field': Field(db, classname, nodeid),
640         'menu': Menu(db, classname, nodeid),
641         'link': Link(db, classname, nodeid),
642         'count': Count(db, classname, nodeid),
643         'reldate': Reldate(db, classname, nodeid),
644         'download': Download(db, classname, nodeid),
645         'checklist': Checklist(db, classname, nodeid),
646         'list': List(db, classname, nodeid),
647         'history': History(db, classname, nodeid),
648         'submit': Submit(db, classname, nodeid),
649         'note': Note(db, classname, nodeid)
650     }
652     cl = db.classes[classname]
653     properties = cl.getprops()
655     if properties.has_key('type') and properties.has_key('content'):
656         pass
657         # XXX we really want to return this as a downloadable...
658         #  currently I handle this at a higher level by detecting 'file'
659         #  designators...
661     w = fp.write
662     w('<form action="%s%s">'%(classname, nodeid))
663     s = open(os.path.join('templates', classname+'.item')).read()
664     replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
665     w(replace.go(s))
666     w('</form>')
669 def newitem(fp, db, classname, form, replace=re.compile(
670             r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
671             r'(?P<endprop></property>)|'
672             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
673     globals = {
674         'plain': Plain(db, classname, form=form),
675         'field': Field(db, classname, form=form),
676         'menu': Menu(db, classname, form=form),
677         'link': Link(db, classname, form=form),
678         'count': Count(db, classname, form=form),
679         'reldate': Reldate(db, classname, form=form),
680         'download': Download(db, classname, form=form),
681         'checklist': Checklist(db, classname, form=form),
682         'list': List(db, classname, form=form),
683         'history': History(db, classname, form=form),
684         'submit': Submit(db, classname, form=form),
685         'note': Note(db, classname, form=form)
686     }
688     cl = db.classes[classname]
689     properties = cl.getprops()
691     w = fp.write
692     try:
693         s = open(os.path.join('templates', classname+'.newitem')).read()
694     except:
695         s = open(os.path.join('templates', classname+'.item')).read()
696     w('<form action="new%s">'%classname)
697     replace = ItemTemplateReplace(globals, locals(), None, None)
698     w(replace.go(s))
699     w('</form>')
702 # $Log: not supported by cvs2svn $
703 # Revision 1.4  2001/07/19 06:27:07  anthonybaxter
704 # fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
705 # my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
706 # strings in a commit message. I'm a twonk.
708 # Also broke the help string in two.
710 # Revision 1.3  2001/07/19 05:52:22  anthonybaxter
711 # Added CVS keywords Id and Log to all python files.