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