d85684f1142502272ed2d79fbaa55822a047b25c
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.84 2002-03-29 19:41:48 rochecompaan Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno, types
26 import hyperdb, date
27 from i18n import _
29 # This imports the StructureText functionality for the do_stext function
30 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
31 try:
32 from StructuredText.StructuredText import HTML as StructuredText
33 except ImportError:
34 StructuredText = None
36 class MissingTemplateError(ValueError):
37 '''Error raised when a template file is missing
38 '''
39 pass
41 class TemplateFunctions:
42 '''Defines the templating functions that are used in the HTML templates
43 of the roundup web interface.
44 '''
45 def __init__(self):
46 self.form = None
47 self.nodeid = None
48 self.filterspec = None
49 self.globals = {}
50 for key in TemplateFunctions.__dict__.keys():
51 if key[:3] == 'do_':
52 self.globals[key[3:]] = getattr(self, key)
54 # These are added by the subclass where appropriate
55 self.client = None
56 self.instance = None
57 self.templates = None
58 self.classname = None
59 self.db = None
60 self.cl = None
61 self.properties = None
63 def do_plain(self, property, escape=0):
64 ''' display a String property directly;
66 display a Date property in a specified time zone with an option to
67 omit the time from the date stamp;
69 for a Link or Multilink property, display the key strings of the
70 linked nodes (or the ids if the linked class has no key property)
71 '''
72 if not self.nodeid and self.form is None:
73 return _('[Field: not called from item]')
74 propclass = self.properties[property]
75 if self.nodeid:
76 # make sure the property is a valid one
77 # TODO: this tests, but we should handle the exception
78 dummy = self.cl.getprops()[property]
80 # get the value for this property
81 try:
82 value = self.cl.get(self.nodeid, property)
83 except KeyError:
84 # a KeyError here means that the node doesn't have a value
85 # for the specified property
86 if isinstance(propclass, hyperdb.Multilink): value = []
87 else: value = ''
88 else:
89 # TODO: pull the value from the form
90 if isinstance(propclass, hyperdb.Multilink): value = []
91 else: value = ''
92 if isinstance(propclass, hyperdb.String):
93 if value is None: value = ''
94 else: value = str(value)
95 elif isinstance(propclass, hyperdb.Password):
96 if value is None: value = ''
97 else: value = _('*encrypted*')
98 elif isinstance(propclass, hyperdb.Date):
99 # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
100 value = str(value)
101 elif isinstance(propclass, hyperdb.Interval):
102 value = str(value)
103 elif isinstance(propclass, hyperdb.Link):
104 linkcl = self.db.classes[propclass.classname]
105 k = linkcl.labelprop()
106 if value:
107 value = linkcl.get(value, k)
108 else:
109 value = _('[unselected]')
110 elif isinstance(propclass, hyperdb.Multilink):
111 linkcl = self.db.classes[propclass.classname]
112 k = linkcl.labelprop()
113 labels = []
114 for v in value:
115 labels.append(linkcl.get(v, k))
116 value = ', '.join(labels)
117 else:
118 value = _('Plain: bad propclass "%(propclass)s"')%locals()
119 if escape:
120 value = cgi.escape(value)
121 return value
123 def do_stext(self, property, escape=0):
124 '''Render as structured text using the StructuredText module
125 (see above for details)
126 '''
127 s = self.do_plain(property, escape=escape)
128 if not StructuredText:
129 return s
130 return StructuredText(s,level=1,header=0)
132 def determine_value(self, property):
133 '''determine the value of a property using the node, form or
134 filterspec
135 '''
136 propclass = self.properties[property]
137 if self.nodeid:
138 value = self.cl.get(self.nodeid, property, None)
139 if isinstance(propclass, hyperdb.Multilink) and value is None:
140 return []
141 return value
142 elif self.filterspec is not None:
143 if isinstance(propclass, hyperdb.Multilink):
144 return self.filterspec.get(property, [])
145 else:
146 return self.filterspec.get(property, '')
147 # TODO: pull the value from the form
148 if isinstance(propclass, hyperdb.Multilink):
149 return []
150 else:
151 return ''
153 def make_sort_function(self, classname):
154 '''Make a sort function for a given class
155 '''
156 linkcl = self.db.classes[classname]
157 if linkcl.getprops().has_key('order'):
158 sort_on = 'order'
159 else:
160 sort_on = linkcl.labelprop()
161 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
162 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
163 return sortfunc
165 def do_field(self, property, size=None, showid=0):
166 ''' display a property like the plain displayer, but in a text field
167 to be edited
169 Note: if you would prefer an option list style display for
170 link or multilink editing, use menu().
171 '''
172 if not self.nodeid and self.form is None and self.filterspec is None:
173 return _('[Field: not called from item]')
175 if size is None:
176 size = 30
178 propclass = self.properties[property]
180 # get the value
181 value = self.determine_value(property)
183 # now display
184 if (isinstance(propclass, hyperdb.String) or
185 isinstance(propclass, hyperdb.Date) or
186 isinstance(propclass, hyperdb.Interval)):
187 if value is None:
188 value = ''
189 else:
190 value = cgi.escape(str(value))
191 value = '"'.join(value.split('"'))
192 s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
193 elif isinstance(propclass, hyperdb.Password):
194 s = '<input type="password" name="%s" size="%s">'%(property, size)
195 elif isinstance(propclass, hyperdb.Link):
196 sortfunc = self.make_sort_function(propclass.classname)
197 linkcl = self.db.classes[propclass.classname]
198 options = linkcl.list()
199 options.sort(sortfunc)
200 # TODO: make this a field display, not a menu one!
201 l = ['<select name="%s">'%property]
202 k = linkcl.labelprop()
203 if value is None:
204 s = 'selected '
205 else:
206 s = ''
207 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
208 for optionid in options:
209 option = linkcl.get(optionid, k)
210 s = ''
211 if optionid == value:
212 s = 'selected '
213 if showid:
214 lab = '%s%s: %s'%(propclass.classname, optionid, option)
215 else:
216 lab = option
217 if size is not None and len(lab) > size:
218 lab = lab[:size-3] + '...'
219 lab = cgi.escape(lab)
220 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
221 l.append('</select>')
222 s = '\n'.join(l)
223 elif isinstance(propclass, hyperdb.Multilink):
224 sortfunc = self.make_sort_function(propclass.classname)
225 linkcl = self.db.classes[propclass.classname]
226 list = linkcl.list()
227 list.sort(sortfunc)
228 l = []
229 # map the id to the label property
230 if not showid:
231 k = linkcl.labelprop()
232 value = [linkcl.get(v, k) for v in value]
233 value = cgi.escape(','.join(value))
234 s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
235 else:
236 s = _('Plain: bad propclass "%(propclass)s"')%locals()
237 return s
239 def do_multiline(self, property, rows=5, cols=40):
240 ''' display a string property in a multiline text edit field
241 '''
242 if not self.nodeid and self.form is None and self.filterspec is None:
243 return _('[Multiline: not called from item]')
245 propclass = self.properties[property]
247 # make sure this is a link property
248 if not isinstance(propclass, hyperdb.String):
249 return _('[Multiline: not a string]')
251 # get the value
252 value = self.determine_value(property)
253 if value is None:
254 value = ''
256 # display
257 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
258 property, rows, cols, value)
260 def do_menu(self, property, size=None, height=None, showid=0):
261 ''' for a Link property, display a menu of the available choices
262 '''
263 if not self.nodeid and self.form is None and self.filterspec is None:
264 return _('[Field: not called from item]')
266 propclass = self.properties[property]
268 # make sure this is a link property
269 if not (isinstance(propclass, hyperdb.Link) or
270 isinstance(propclass, hyperdb.Multilink)):
271 return _('[Menu: not a link]')
273 # sort function
274 sortfunc = self.make_sort_function(propclass.classname)
276 # get the value
277 value = self.determine_value(property)
279 # display
280 if isinstance(propclass, hyperdb.Multilink):
281 linkcl = self.db.classes[propclass.classname]
282 options = linkcl.list()
283 options.sort(sortfunc)
284 height = height or min(len(options), 7)
285 l = ['<select multiple name="%s" size="%s">'%(property, height)]
286 k = linkcl.labelprop()
287 for optionid in options:
288 option = linkcl.get(optionid, k)
289 s = ''
290 if optionid in value or option in value:
291 s = 'selected '
292 if showid:
293 lab = '%s%s: %s'%(propclass.classname, optionid, option)
294 else:
295 lab = option
296 if size is not None and len(lab) > size:
297 lab = lab[:size-3] + '...'
298 lab = cgi.escape(lab)
299 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
300 lab))
301 l.append('</select>')
302 return '\n'.join(l)
303 if isinstance(propclass, hyperdb.Link):
304 # force the value to be a single choice
305 if type(value) is types.ListType:
306 value = value[0]
307 linkcl = self.db.classes[propclass.classname]
308 l = ['<select name="%s">'%property]
309 k = linkcl.labelprop()
310 s = ''
311 if value is None:
312 s = 'selected '
313 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
314 options = linkcl.list()
315 options.sort(sortfunc)
316 for optionid in options:
317 option = linkcl.get(optionid, k)
318 s = ''
319 if value in [optionid, option]:
320 s = 'selected '
321 if showid:
322 lab = '%s%s: %s'%(propclass.classname, optionid, option)
323 else:
324 lab = option
325 if size is not None and len(lab) > size:
326 lab = lab[:size-3] + '...'
327 lab = cgi.escape(lab)
328 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
329 l.append('</select>')
330 return '\n'.join(l)
331 return _('[Menu: not a link]')
333 #XXX deviates from spec
334 def do_link(self, property=None, is_download=0):
335 '''For a Link or Multilink property, display the names of the linked
336 nodes, hyperlinked to the item views on those nodes.
337 For other properties, link to this node with the property as the
338 text.
340 If is_download is true, append the property value to the generated
341 URL so that the link may be used as a download link and the
342 downloaded file name is correct.
343 '''
344 if not self.nodeid and self.form is None:
345 return _('[Link: not called from item]')
347 # get the value
348 value = self.determine_value(property)
349 if not value:
350 return _('[no %(propname)s]')%{'propname':property.capitalize()}
352 propclass = self.properties[property]
353 if isinstance(propclass, hyperdb.Link):
354 linkname = propclass.classname
355 linkcl = self.db.classes[linkname]
356 k = linkcl.labelprop()
357 linkvalue = cgi.escape(linkcl.get(value, k))
358 if is_download:
359 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
360 linkvalue, linkvalue)
361 else:
362 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
363 if isinstance(propclass, hyperdb.Multilink):
364 linkname = propclass.classname
365 linkcl = self.db.classes[linkname]
366 k = linkcl.labelprop()
367 l = []
368 for value in value:
369 linkvalue = cgi.escape(linkcl.get(value, k))
370 if is_download:
371 l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
372 linkvalue, linkvalue))
373 else:
374 l.append('<a href="%s%s">%s</a>'%(linkname, value,
375 linkvalue))
376 return ', '.join(l)
377 if is_download:
378 return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
379 value, value)
380 else:
381 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
383 def do_count(self, property, **args):
384 ''' for a Multilink property, display a count of the number of links in
385 the list
386 '''
387 if not self.nodeid:
388 return _('[Count: not called from item]')
390 propclass = self.properties[property]
391 if not isinstance(propclass, hyperdb.Multilink):
392 return _('[Count: not a Multilink]')
394 # figure the length then...
395 value = self.cl.get(self.nodeid, property)
396 return str(len(value))
398 # XXX pretty is definitely new ;)
399 def do_reldate(self, property, pretty=0):
400 ''' display a Date property in terms of an interval relative to the
401 current date (e.g. "+ 3w", "- 2d").
403 with the 'pretty' flag, make it pretty
404 '''
405 if not self.nodeid and self.form is None:
406 return _('[Reldate: not called from item]')
408 propclass = self.properties[property]
409 if not isinstance(propclass, hyperdb.Date):
410 return _('[Reldate: not a Date]')
412 if self.nodeid:
413 value = self.cl.get(self.nodeid, property)
414 else:
415 return ''
416 if not value:
417 return ''
419 # figure the interval
420 interval = date.Date('.') - value
421 if pretty:
422 if not self.nodeid:
423 return _('now')
424 pretty = interval.pretty()
425 if pretty is None:
426 pretty = value.pretty()
427 return pretty
428 return str(interval)
430 def do_download(self, property, **args):
431 ''' show a Link("file") or Multilink("file") property using links that
432 allow you to download files
433 '''
434 if not self.nodeid:
435 return _('[Download: not called from item]')
436 return self.do_link(property, is_download=1)
439 def do_checklist(self, property, **args):
440 ''' for a Link or Multilink property, display checkboxes for the
441 available choices to permit filtering
442 '''
443 propclass = self.properties[property]
444 if (not isinstance(propclass, hyperdb.Link) and not
445 isinstance(propclass, hyperdb.Multilink)):
446 return _('[Checklist: not a link]')
448 # get our current checkbox state
449 if self.nodeid:
450 # get the info from the node - make sure it's a list
451 if isinstance(propclass, hyperdb.Link):
452 value = [self.cl.get(self.nodeid, property)]
453 else:
454 value = self.cl.get(self.nodeid, property)
455 elif self.filterspec is not None:
456 # get the state from the filter specification (always a list)
457 value = self.filterspec.get(property, [])
458 else:
459 # it's a new node, so there's no state
460 value = []
462 # so we can map to the linked node's "lable" property
463 linkcl = self.db.classes[propclass.classname]
464 l = []
465 k = linkcl.labelprop()
466 for optionid in linkcl.list():
467 option = cgi.escape(linkcl.get(optionid, k))
468 if optionid in value or option in value:
469 checked = 'checked'
470 else:
471 checked = ''
472 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
473 option, checked, property, option))
475 # for Links, allow the "unselected" option too
476 if isinstance(propclass, hyperdb.Link):
477 if value is None or '-1' in value:
478 checked = 'checked'
479 else:
480 checked = ''
481 l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
482 'value="-1">')%(checked, property))
483 return '\n'.join(l)
485 def do_note(self, rows=5, cols=80):
486 ''' display a "note" field, which is a text area for entering a note to
487 go along with a change.
488 '''
489 # TODO: pull the value from the form
490 return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
491 '</textarea>'%(rows, cols)
493 # XXX new function
494 def do_list(self, property, reverse=0):
495 ''' list the items specified by property using the standard index for
496 the class
497 '''
498 propcl = self.properties[property]
499 if not isinstance(propcl, hyperdb.Multilink):
500 return _('[List: not a Multilink]')
502 value = self.determine_value(property)
503 if not value:
504 return ''
506 # sort, possibly revers and then re-stringify
507 value = map(int, value)
508 value.sort()
509 if reverse:
510 value.reverse()
511 value = map(str, value)
513 # render the sub-index into a string
514 fp = StringIO.StringIO()
515 try:
516 write_save = self.client.write
517 self.client.write = fp.write
518 index = IndexTemplate(self.client, self.templates, propcl.classname)
519 index.render(nodeids=value, show_display_form=0)
520 finally:
521 self.client.write = write_save
523 return fp.getvalue()
525 # XXX new function
526 def do_history(self, direction='descending'):
527 ''' list the history of the item
529 If "direction" is 'descending' then the most recent event will
530 be displayed first. If it is 'ascending' then the oldest event
531 will be displayed first.
532 '''
533 if self.nodeid is None:
534 return _("[History: node doesn't exist]")
536 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
537 '<tr class="list-header">',
538 _('<th align=left><span class="list-item">Date</span></th>'),
539 _('<th align=left><span class="list-item">User</span></th>'),
540 _('<th align=left><span class="list-item">Action</span></th>'),
541 _('<th align=left><span class="list-item">Args</span></th>'),
542 '</tr>']
544 comments = {}
545 history = self.cl.history(self.nodeid)
546 history.sort()
547 if direction == 'descending':
548 history.reverse()
549 for id, evt_date, user, action, args in history:
550 date_s = str(evt_date).replace("."," ")
551 arg_s = ''
552 if action == 'link' and type(args) == type(()):
553 if len(args) == 3:
554 linkcl, linkid, key = args
555 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
556 linkcl, linkid, key)
557 else:
558 arg_s = str(args)
560 elif action == 'unlink' and type(args) == type(()):
561 if len(args) == 3:
562 linkcl, linkid, key = args
563 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
564 linkcl, linkid, key)
565 else:
566 arg_s = str(args)
568 elif type(args) == type({}):
569 cell = []
570 for k in args.keys():
571 # try to get the relevant property and treat it
572 # specially
573 try:
574 prop = self.properties[k]
575 except:
576 prop = None
577 if prop is not None:
578 if args[k] and (isinstance(prop, hyperdb.Multilink) or
579 isinstance(prop, hyperdb.Link)):
580 # figure what the link class is
581 classname = prop.classname
582 try:
583 linkcl = self.db.classes[classname]
584 except KeyError:
585 labelprop = None
586 comments[classname] = _('''The linked class
587 %(classname)s no longer exists''')%locals()
588 labelprop = linkcl.labelprop()
590 if isinstance(prop, hyperdb.Multilink) and \
591 len(args[k]) > 0:
592 ml = []
593 for linkid in args[k]:
594 label = classname + linkid
595 # if we have a label property, try to use it
596 # TODO: test for node existence even when
597 # there's no labelprop!
598 try:
599 if labelprop is not None:
600 label = linkcl.get(linkid, labelprop)
601 except IndexError:
602 comments['no_link'] = _('''<strike>The
603 linked node no longer
604 exists</strike>''')
605 ml.append('<strike>%s</strike>'%label)
606 else:
607 ml.append('<a href="%s%s">%s</a>'%(
608 classname, linkid, label))
609 cell.append('%s:\n %s'%(k, ',\n '.join(ml)))
610 elif isinstance(prop, hyperdb.Link) and args[k]:
611 label = classname + args[k]
612 # if we have a label property, try to use it
613 # TODO: test for node existence even when
614 # there's no labelprop!
615 if labelprop is not None:
616 try:
617 label = linkcl.get(args[k], labelprop)
618 except IndexError:
619 comments['no_link'] = _('''<strike>The
620 linked node no longer
621 exists</strike>''')
622 cell.append(' <strike>%s</strike>,\n'%label)
623 # "flag" this is done .... euwww
624 label = None
625 if label is not None:
626 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
627 classname, args[k], label))
629 elif isinstance(prop, hyperdb.Date) and args[k]:
630 d = date.Date(args[k])
631 cell.append('%s: %s'%(k, str(d)))
633 elif isinstance(prop, hyperdb.Interval) and args[k]:
634 d = date.Interval(args[k])
635 cell.append('%s: %s'%(k, str(d)))
637 elif not args[k]:
638 cell.append('%s: (no value)\n'%k)
640 else:
641 cell.append('%s: %s\n'%(k, str(args[k])))
642 else:
643 # property no longer exists
644 comments['no_exist'] = _('''<em>The indicated property
645 no longer exists</em>''')
646 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
647 arg_s = '<br />'.join(cell)
648 else:
649 # unkown event!!
650 comments['unknown'] = _('''<strong><em>This event is not
651 handled by the history display!</em></strong>''')
652 arg_s = '<strong><em>' + str(args) + '</em></strong>'
653 date_s = date_s.replace(' ', ' ')
654 l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
655 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
656 user, action, arg_s))
657 if comments:
658 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
659 for entry in comments.values():
660 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
661 l.append('</table>')
662 return '\n'.join(l)
664 # XXX new function
665 def do_submit(self):
666 ''' add a submit button for the item
667 '''
668 if self.nodeid:
669 return _('<input type="submit" name="submit" value="Submit Changes">')
670 elif self.form is not None:
671 return _('<input type="submit" name="submit" value="Submit New Entry">')
672 else:
673 return _('[Submit: not called from item]')
675 def do_classhelp(self, classname, properties, label='?', width='400',
676 height='400'):
677 '''pop up a javascript window with class help
679 This generates a link to a popup window which displays the
680 properties indicated by "properties" of the class named by
681 "classname". The "properties" should be a comma-separated list
682 (eg. 'id,name,description').
684 You may optionally override the label displayed, the width and
685 height. The popup window will be resizable and scrollable.
686 '''
687 return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
688 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
689 properties, width, height, label)
690 #
691 # INDEX TEMPLATES
692 #
693 class IndexTemplateReplace:
694 '''Regular-expression based parser that turns the template into HTML.
695 '''
696 def __init__(self, globals, locals, props):
697 self.globals = globals
698 self.locals = locals
699 self.props = props
701 replace=re.compile(
702 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
703 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
704 def go(self, text):
705 return self.replace.sub(self, text)
707 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
708 if m.group('name'):
709 if m.group('name') in self.props:
710 text = m.group('text')
711 replace = IndexTemplateReplace(self.globals, {}, self.props)
712 return replace.go(text)
713 else:
714 return ''
715 if m.group('display'):
716 command = m.group('command')
717 return eval(command, self.globals, self.locals)
718 return '*** unhandled match: %s'%str(m.groupdict())
720 class IndexTemplate(TemplateFunctions):
721 '''Templating functionality specifically for index pages
722 '''
723 def __init__(self, client, templates, classname):
724 TemplateFunctions.__init__(self)
725 self.client = client
726 self.instance = client.instance
727 self.templates = templates
728 self.classname = classname
730 # derived
731 self.db = self.client.db
732 self.cl = self.db.classes[self.classname]
733 self.properties = self.cl.getprops()
735 col_re=re.compile(r'<property\s+name="([^>]+)">')
736 def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
737 show_display_form=1, nodeids=None, show_customization=1):
738 self.filterspec = filterspec
740 w = self.client.write
742 # get the filter template
743 try:
744 filter_template = open(os.path.join(self.templates,
745 self.classname+'.filter')).read()
746 all_filters = self.col_re.findall(filter_template)
747 except IOError, error:
748 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
749 filter_template = None
750 all_filters = []
752 # XXX deviate from spec here ...
753 # load the index section template and figure the default columns from it
754 try:
755 template = open(os.path.join(self.templates,
756 self.classname+'.index')).read()
757 except IOError, error:
758 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
759 raise MissingTemplateError, self.classname+'.index'
760 all_columns = self.col_re.findall(template)
761 if not columns:
762 columns = []
763 for name in all_columns:
764 columns.append(name)
765 else:
766 # re-sort columns to be the same order as all_columns
767 l = []
768 for name in all_columns:
769 if name in columns:
770 l.append(name)
771 columns = l
773 # display the filter section
774 if (show_display_form and
775 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
776 w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
777 self.filter_section(filter_template, filter, columns, group,
778 all_filters, all_columns, show_customization)
779 # make sure that the sorting doesn't get lost either
780 if sort:
781 w('<input type="hidden" name=":sort" value="%s">'%
782 ','.join(sort))
783 w('</form>\n')
786 # now display the index section
787 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
788 w('<tr class="list-header">\n')
789 for name in columns:
790 cname = name.capitalize()
791 if show_display_form:
792 sb = self.sortby(name, filterspec, columns, filter, group, sort)
793 anchor = "%s?%s"%(self.classname, sb)
794 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
795 anchor, cname))
796 else:
797 w('<td><span class="list-header">%s</span></td>\n'%cname)
798 w('</tr>\n')
800 # this stuff is used for group headings - optimise the group names
801 old_group = None
802 group_names = []
803 if group:
804 for name in group:
805 if name[0] == '-': group_names.append(name[1:])
806 else: group_names.append(name)
808 # now actually loop through all the nodes we get from the filter and
809 # apply the template
810 if nodeids is None:
811 nodeids = self.cl.filter(filterspec, sort, group)
812 for nodeid in nodeids:
813 # check for a group heading
814 if group_names:
815 this_group = [self.cl.get(nodeid, name, _('[no value]'))
816 for name in group_names]
817 if this_group != old_group:
818 l = []
819 for name in group_names:
820 prop = self.properties[name]
821 if isinstance(prop, hyperdb.Link):
822 group_cl = self.db.classes[prop.classname]
823 key = group_cl.getkey()
824 value = self.cl.get(nodeid, name)
825 if value is None:
826 l.append(_('[unselected %(classname)s]')%{
827 'classname': prop.classname})
828 else:
829 l.append(group_cl.get(self.cl.get(nodeid,
830 name), key))
831 elif isinstance(prop, hyperdb.Multilink):
832 group_cl = self.db.classes[prop.classname]
833 key = group_cl.getkey()
834 for value in self.cl.get(nodeid, name):
835 l.append(group_cl.get(value, key))
836 else:
837 value = self.cl.get(nodeid, name, _('[no value]'))
838 if value is None:
839 value = _('[empty %(name)s]')%locals()
840 else:
841 value = str(value)
842 l.append(value)
843 w('<tr class="section-bar">'
844 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
845 len(columns), ', '.join(l)))
846 old_group = this_group
848 # display this node's row
849 replace = IndexTemplateReplace(self.globals, locals(), columns)
850 self.nodeid = nodeid
851 w(replace.go(template))
852 self.nodeid = None
854 w('</table>')
856 # display the filter section
857 if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
858 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
859 w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
860 self.filter_section(filter_template, filter, columns, group,
861 all_filters, all_columns, show_customization)
862 # make sure that the sorting doesn't get lost either
863 if sort:
864 w('<input type="hidden" name=":sort" value="%s">'%
865 ','.join(sort))
866 w('</form>\n')
869 def filter_section(self, template, filter, columns, group, all_filters,
870 all_columns, show_customization):
872 w = self.client.write
874 # wrap the template in a single table to ensure the whole widget
875 # is displayed at once
876 w('<table><tr><td>')
878 if template and filter:
879 # display the filter section
880 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
881 w('<tr class="location-bar">')
882 w(_(' <th align="left" colspan="2">Filter specification...</th>'))
883 w('</tr>')
884 replace = IndexTemplateReplace(self.globals, locals(), filter)
885 w(replace.go(template))
886 w('<tr class="location-bar"><td width="1%%"> </td>')
887 w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
888 w('</table>')
890 # now add in the filter/columns/group/etc config table form
891 w('<input type="hidden" name="show_customization" value="%s">' %
892 show_customization )
893 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
894 names = []
895 seen = {}
896 for name in all_filters + all_columns:
897 if self.properties.has_key(name) and not seen.has_key(name):
898 names.append(name)
899 seen[name] = 1
900 if show_customization:
901 action = '-'
902 else:
903 action = '+'
904 # hide the values for filters, columns and grouping in the form
905 # if the customization widget is not visible
906 for name in names:
907 if all_filters and name in filter:
908 w('<input type="hidden" name=":filter" value="%s">' % name)
909 if all_columns and name in columns:
910 w('<input type="hidden" name=":columns" value="%s">' % name)
911 if all_columns and name in group:
912 w('<input type="hidden" name=":group" value="%s">' % name)
914 # TODO: The widget style can go into the stylesheet
915 w(_('<th align="left" colspan=%s>'
916 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
917 'customisation...</th></tr>\n')%(len(names)+1, action))
919 if not show_customization:
920 w('</table>\n')
921 return
923 w('<tr class="location-bar"><th> </th>')
924 for name in names:
925 w('<th>%s</th>'%name.capitalize())
926 w('</tr>\n')
928 # Filter
929 if all_filters:
930 w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
931 for name in names:
932 if name not in all_filters:
933 w('<td> </td>')
934 continue
935 if name in filter: checked=' checked'
936 else: checked=''
937 w('<td align=middle>\n')
938 w(' <input type="checkbox" name=":filter" value="%s" '
939 '%s></td>\n'%(name, checked))
940 w('</tr>\n')
942 # Columns
943 if all_columns:
944 w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
945 for name in names:
946 if name not in all_columns:
947 w('<td> </td>')
948 continue
949 if name in columns: checked=' checked'
950 else: checked=''
951 w('<td align=middle>\n')
952 w(' <input type="checkbox" name=":columns" value="%s"'
953 '%s></td>\n'%(name, checked))
954 w('</tr>\n')
956 # Grouping
957 w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
958 for name in names:
959 if name not in all_columns:
960 w('<td> </td>')
961 continue
962 if name in group: checked=' checked'
963 else: checked=''
964 w('<td align=middle>\n')
965 w(' <input type="checkbox" name=":group" value="%s"'
966 '%s></td>\n'%(name, checked))
967 w('</tr>\n')
969 w('<tr class="location-bar"><td width="1%"> </td>')
970 w('<td colspan="%s">'%len(names))
971 w(_('<input type="submit" name="action" value="Redisplay"></td>'))
972 w('</tr>\n')
973 w('</table>\n')
975 # and the outer table
976 w('</td></tr></table>')
979 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
980 l = []
981 w = l.append
982 for k, v in filterspec.items():
983 k = urllib.quote(k)
984 if type(v) == type([]):
985 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
986 else:
987 w('%s=%s'%(k, urllib.quote(v)))
988 if columns:
989 w(':columns=%s'%','.join(map(urllib.quote, columns)))
990 if filter:
991 w(':filter=%s'%','.join(map(urllib.quote, filter)))
992 if group:
993 w(':group=%s'%','.join(map(urllib.quote, group)))
994 m = []
995 s_dir = ''
996 for name in sort:
997 dir = name[0]
998 if dir == '-':
999 name = name[1:]
1000 else:
1001 dir = ''
1002 if sort_name == name:
1003 if dir == '-':
1004 s_dir = ''
1005 else:
1006 s_dir = '-'
1007 else:
1008 m.append(dir+urllib.quote(name))
1009 m.insert(0, s_dir+urllib.quote(sort_name))
1010 # so things don't get completely out of hand, limit the sort to
1011 # two columns
1012 w(':sort=%s'%','.join(m[:2]))
1013 return '&'.join(l)
1015 #
1016 # ITEM TEMPLATES
1017 #
1018 class ItemTemplateReplace:
1019 '''Regular-expression based parser that turns the template into HTML.
1020 '''
1021 def __init__(self, globals, locals, cl, nodeid):
1022 self.globals = globals
1023 self.locals = locals
1024 self.cl = cl
1025 self.nodeid = nodeid
1027 replace=re.compile(
1028 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1029 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1030 def go(self, text):
1031 return self.replace.sub(self, text)
1033 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1034 if m.group('name'):
1035 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1036 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1037 self.nodeid)
1038 return replace.go(m.group('text'))
1039 else:
1040 return ''
1041 if m.group('display'):
1042 command = m.group('command')
1043 return eval(command, self.globals, self.locals)
1044 return '*** unhandled match: %s'%str(m.groupdict())
1047 class ItemTemplate(TemplateFunctions):
1048 '''Templating functionality specifically for item (node) display
1049 '''
1050 def __init__(self, client, templates, classname):
1051 TemplateFunctions.__init__(self)
1052 self.client = client
1053 self.instance = client.instance
1054 self.templates = templates
1055 self.classname = classname
1057 # derived
1058 self.db = self.client.db
1059 self.cl = self.db.classes[self.classname]
1060 self.properties = self.cl.getprops()
1062 def render(self, nodeid):
1063 self.nodeid = nodeid
1065 if (self.properties.has_key('type') and
1066 self.properties.has_key('content')):
1067 pass
1068 # XXX we really want to return this as a downloadable...
1069 # currently I handle this at a higher level by detecting 'file'
1070 # designators...
1072 w = self.client.write
1073 w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1074 self.classname, nodeid))
1075 s = open(os.path.join(self.templates, self.classname+'.item')).read()
1076 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1077 w(replace.go(s))
1078 w('</form>')
1081 class NewItemTemplate(TemplateFunctions):
1082 '''Templating functionality specifically for NEW item (node) display
1083 '''
1084 def __init__(self, client, templates, classname):
1085 TemplateFunctions.__init__(self)
1086 self.client = client
1087 self.instance = client.instance
1088 self.templates = templates
1089 self.classname = classname
1091 # derived
1092 self.db = self.client.db
1093 self.cl = self.db.classes[self.classname]
1094 self.properties = self.cl.getprops()
1096 def render(self, form):
1097 self.form = form
1098 w = self.client.write
1099 c = self.classname
1100 try:
1101 s = open(os.path.join(self.templates, c+'.newitem')).read()
1102 except IOError:
1103 s = open(os.path.join(self.templates, c+'.item')).read()
1104 w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1105 for key in form.keys():
1106 if key[0] == ':':
1107 value = form[key].value
1108 if type(value) != type([]): value = [value]
1109 for value in value:
1110 w('<input type="hidden" name="%s" value="%s">'%(key, value))
1111 replace = ItemTemplateReplace(self.globals, locals(), None, None)
1112 w(replace.go(s))
1113 w('</form>')
1115 #
1116 # $Log: not supported by cvs2svn $
1117 # Revision 1.83 2002/02/27 04:14:31 richard
1118 # Ran it through pychecker, made fixes
1119 #
1120 # Revision 1.82 2002/02/21 23:11:45 richard
1121 # . fixed some problems in date calculations (calendar.py doesn't handle over-
1122 # and under-flow). Also, hour/minute/second intervals may now be more than
1123 # 99 each.
1124 #
1125 # Revision 1.81 2002/02/21 07:21:38 richard
1126 # docco
1127 #
1128 # Revision 1.80 2002/02/21 07:19:08 richard
1129 # ... and label, width and height control for extra flavour!
1130 #
1131 # Revision 1.79 2002/02/21 06:57:38 richard
1132 # . Added popup help for classes using the classhelp html template function.
1133 # - add <display call="classhelp('priority', 'id,name,description')">
1134 # to an item page, and it generates a link to a popup window which displays
1135 # the id, name and description for the priority class. The description
1136 # field won't exist in most installations, but it will be added to the
1137 # default templates.
1138 #
1139 # Revision 1.78 2002/02/21 06:23:00 richard
1140 # *** empty log message ***
1141 #
1142 # Revision 1.77 2002/02/20 05:05:29 richard
1143 # . Added simple editing for classes that don't define a templated interface.
1144 # - access using the admin "class list" interface
1145 # - limited to admin-only
1146 # - requires the csv module from object-craft (url given if it's missing)
1147 #
1148 # Revision 1.76 2002/02/16 09:10:52 richard
1149 # oops
1150 #
1151 # Revision 1.75 2002/02/16 08:43:23 richard
1152 # . #517906 ] Attribute order in "View customisation"
1153 #
1154 # Revision 1.74 2002/02/16 08:39:42 richard
1155 # . #516854 ] "My Issues" and redisplay
1156 #
1157 # Revision 1.73 2002/02/15 07:08:44 richard
1158 # . Alternate email addresses are now available for users. See the MIGRATION
1159 # file for info on how to activate the feature.
1160 #
1161 # Revision 1.72 2002/02/14 23:39:18 richard
1162 # . All forms now have "double-submit" protection when Javascript is enabled
1163 # on the client-side.
1164 #
1165 # Revision 1.71 2002/01/23 06:15:24 richard
1166 # real (non-string, duh) sorting of lists by node id
1167 #
1168 # Revision 1.70 2002/01/23 05:47:57 richard
1169 # more HTML template cleanup and unit tests
1170 #
1171 # Revision 1.69 2002/01/23 05:10:27 richard
1172 # More HTML template cleanup and unit tests.
1173 # - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1174 # templates, but link(is_download=1) will still work for existing templates]
1175 #
1176 # Revision 1.68 2002/01/22 22:55:28 richard
1177 # . htmltemplate list() wasn't sorting...
1178 #
1179 # Revision 1.67 2002/01/22 22:46:22 richard
1180 # more htmltemplate cleanups and unit tests
1181 #
1182 # Revision 1.66 2002/01/22 06:35:40 richard
1183 # more htmltemplate tests and cleanup
1184 #
1185 # Revision 1.65 2002/01/22 00:12:06 richard
1186 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1187 # off the implementation of some of the functions so they behave sanely.
1188 #
1189 # Revision 1.64 2002/01/21 03:25:59 richard
1190 # oops
1191 #
1192 # Revision 1.63 2002/01/21 02:59:10 richard
1193 # Fixed up the HTML display of history so valid links are actually displayed.
1194 # Oh for some unit tests! :(
1195 #
1196 # Revision 1.62 2002/01/18 08:36:12 grubert
1197 # . add nowrap to history table date cell i.e. <td nowrap ...
1198 #
1199 # Revision 1.61 2002/01/17 23:04:53 richard
1200 # . much nicer history display (actualy real handling of property types etc)
1201 #
1202 # Revision 1.60 2002/01/17 08:48:19 grubert
1203 # . display superseder as html link in history.
1204 #
1205 # Revision 1.59 2002/01/17 07:58:24 grubert
1206 # . display links a html link in history.
1207 #
1208 # Revision 1.58 2002/01/15 00:50:03 richard
1209 # #502949 ] index view for non-issues and redisplay
1210 #
1211 # Revision 1.57 2002/01/14 23:31:21 richard
1212 # reverted the change that had plain() hyperlinking the link displays -
1213 # that's what link() is for!
1214 #
1215 # Revision 1.56 2002/01/14 07:04:36 richard
1216 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1217 # the linked node's page.
1218 # ... this allows a display very similar to bugzilla's where you can actually
1219 # find out information about the linked node.
1220 #
1221 # Revision 1.55 2002/01/14 06:45:03 richard
1222 # . #502953 ] nosy-like treatment of other multilinks
1223 # ... had to revert most of the previous change to the multilink field
1224 # display... not good.
1225 #
1226 # Revision 1.54 2002/01/14 05:16:51 richard
1227 # The submit buttons need a name attribute or mozilla won't submit without a
1228 # file upload. Yeah, that's bloody obscure. Grr.
1229 #
1230 # Revision 1.53 2002/01/14 04:03:32 richard
1231 # How about that ... date fields have never worked ...
1232 #
1233 # Revision 1.52 2002/01/14 02:20:14 richard
1234 # . changed all config accesses so they access either the instance or the
1235 # config attriubute on the db. This means that all config is obtained from
1236 # instance_config instead of the mish-mash of classes. This will make
1237 # switching to a ConfigParser setup easier too, I hope.
1238 #
1239 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1240 # 0.5.0 switch, I hope!)
1241 #
1242 # Revision 1.51 2002/01/10 10:02:15 grubert
1243 # In do_history: replace "." in date by " " so html wraps more sensible.
1244 # Should this be done in date's string converter ?
1245 #
1246 # Revision 1.50 2002/01/05 02:35:10 richard
1247 # I18N'ification
1248 #
1249 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1250 # Features added:
1251 # . Multilink properties are now displayed as comma separated values in
1252 # a textbox
1253 # . The add user link is now only visible to the admin user
1254 # . Modified the mail gateway to reject submissions from unknown
1255 # addresses if ANONYMOUS_ACCESS is denied
1256 #
1257 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1258 # Bugs fixed:
1259 # . Exception handling in hyperdb for strings-that-look-like numbers got
1260 # lost somewhere
1261 # . Internet Explorer submits full path for filename - we now strip away
1262 # the path
1263 # Features added:
1264 # . Link and multilink properties are now displayed sorted in the cgi
1265 # interface
1266 #
1267 # Revision 1.47 2001/11/26 22:55:56 richard
1268 # Feature:
1269 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1270 # the instance.
1271 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1272 # signature info in e-mails.
1273 # . Some more flexibility in the mail gateway and more error handling.
1274 # . Login now takes you to the page you back to the were denied access to.
1275 #
1276 # Fixed:
1277 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1278 #
1279 # Revision 1.46 2001/11/24 00:53:12 jhermann
1280 # "except:" is bad, bad , bad!
1281 #
1282 # Revision 1.45 2001/11/22 15:46:42 jhermann
1283 # Added module docstrings to all modules.
1284 #
1285 # Revision 1.44 2001/11/21 23:35:45 jhermann
1286 # Added globbing for win32, and sample marking in a 2nd file to test it
1287 #
1288 # Revision 1.43 2001/11/21 04:04:43 richard
1289 # *sigh* more missing value handling
1290 #
1291 # Revision 1.42 2001/11/21 03:40:54 richard
1292 # more new property handling
1293 #
1294 # Revision 1.41 2001/11/15 10:26:01 richard
1295 # . missing "return" in filter_section (thanks Roch'e Compaan)
1296 #
1297 # Revision 1.40 2001/11/03 01:56:51 richard
1298 # More HTML compliance fixes. This will probably fix the Netscape problem
1299 # too.
1300 #
1301 # Revision 1.39 2001/11/03 01:43:47 richard
1302 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1303 #
1304 # Revision 1.38 2001/10/31 06:58:51 richard
1305 # Added the wrap="hard" attribute to the textarea of the note field so the
1306 # messages wrap sanely.
1307 #
1308 # Revision 1.37 2001/10/31 06:24:35 richard
1309 # Added do_stext to htmltemplate, thanks Brad Clements.
1310 #
1311 # Revision 1.36 2001/10/28 22:51:38 richard
1312 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1313 #
1314 # Revision 1.35 2001/10/24 00:04:41 richard
1315 # Removed the "infinite authentication loop", thanks Roch'e
1316 #
1317 # Revision 1.34 2001/10/23 22:56:36 richard
1318 # Bugfix in filter "widget" placement, thanks Roch'e
1319 #
1320 # Revision 1.33 2001/10/23 01:00:18 richard
1321 # Re-enabled login and registration access after lopping them off via
1322 # disabling access for anonymous users.
1323 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1324 # a couple of bugs while I was there. Probably introduced a couple, but
1325 # things seem to work OK at the moment.
1326 #
1327 # Revision 1.32 2001/10/22 03:25:01 richard
1328 # Added configuration for:
1329 # . anonymous user access and registration (deny/allow)
1330 # . filter "widget" location on index page (top, bottom, both)
1331 # Updated some documentation.
1332 #
1333 # Revision 1.31 2001/10/21 07:26:35 richard
1334 # feature #473127: Filenames. I modified the file.index and htmltemplate
1335 # source so that the filename is used in the link and the creation
1336 # information is displayed.
1337 #
1338 # Revision 1.30 2001/10/21 04:44:50 richard
1339 # bug #473124: UI inconsistency with Link fields.
1340 # This also prompted me to fix a fairly long-standing usability issue -
1341 # that of being able to turn off certain filters.
1342 #
1343 # Revision 1.29 2001/10/21 00:17:56 richard
1344 # CGI interface view customisation section may now be hidden (patch from
1345 # Roch'e Compaan.)
1346 #
1347 # Revision 1.28 2001/10/21 00:00:16 richard
1348 # Fixed Checklist function - wasn't always working on a list.
1349 #
1350 # Revision 1.27 2001/10/20 12:13:44 richard
1351 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1352 #
1353 # Revision 1.26 2001/10/14 10:55:00 richard
1354 # Handle empty strings in HTML template Link function
1355 #
1356 # Revision 1.25 2001/10/09 07:25:59 richard
1357 # Added the Password property type. See "pydoc roundup.password" for
1358 # implementation details. Have updated some of the documentation too.
1359 #
1360 # Revision 1.24 2001/09/27 06:45:58 richard
1361 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1362 # on the plain() template function to escape the text for HTML.
1363 #
1364 # Revision 1.23 2001/09/10 09:47:18 richard
1365 # Fixed bug in the generation of links to Link/Multilink in indexes.
1366 # (thanks Hubert Hoegl)
1367 # Added AssignedTo to the "classic" schema's item page.
1368 #
1369 # Revision 1.22 2001/08/30 06:01:17 richard
1370 # Fixed missing import in mailgw :(
1371 #
1372 # Revision 1.21 2001/08/16 07:34:59 richard
1373 # better CGI text searching - but hidden filter fields are disappearing...
1374 #
1375 # Revision 1.20 2001/08/15 23:43:18 richard
1376 # Fixed some isFooTypes that I missed.
1377 # Refactored some code in the CGI code.
1378 #
1379 # Revision 1.19 2001/08/12 06:32:36 richard
1380 # using isinstance(blah, Foo) now instead of isFooType
1381 #
1382 # Revision 1.18 2001/08/07 00:24:42 richard
1383 # stupid typo
1384 #
1385 # Revision 1.17 2001/08/07 00:15:51 richard
1386 # Added the copyright/license notice to (nearly) all files at request of
1387 # Bizar Software.
1388 #
1389 # Revision 1.16 2001/08/01 03:52:23 richard
1390 # Checklist was using wrong name.
1391 #
1392 # Revision 1.15 2001/07/30 08:12:17 richard
1393 # Added time logging and file uploading to the templates.
1394 #
1395 # Revision 1.14 2001/07/30 06:17:45 richard
1396 # Features:
1397 # . Added ability for cgi newblah forms to indicate that the new node
1398 # should be linked somewhere.
1399 # Fixed:
1400 # . Fixed the agument handling for the roundup-admin find command.
1401 # . Fixed handling of summary when no note supplied for newblah. Again.
1402 # . Fixed detection of no form in htmltemplate Field display.
1403 #
1404 # Revision 1.13 2001/07/30 02:37:53 richard
1405 # Temporary measure until we have decent schema migration.
1406 #
1407 # Revision 1.12 2001/07/30 01:24:33 richard
1408 # Handles new node display now.
1409 #
1410 # Revision 1.11 2001/07/29 09:31:35 richard
1411 # oops
1412 #
1413 # Revision 1.10 2001/07/29 09:28:23 richard
1414 # Fixed sorting by clicking on column headings.
1415 #
1416 # Revision 1.9 2001/07/29 08:27:40 richard
1417 # Fixed handling of passed-in values in form elements (ie. during a
1418 # drill-down)
1419 #
1420 # Revision 1.8 2001/07/29 07:01:39 richard
1421 # Added vim command to all source so that we don't get no steenkin' tabs :)
1422 #
1423 # Revision 1.7 2001/07/29 05:36:14 richard
1424 # Cleanup of the link label generation.
1425 #
1426 # Revision 1.6 2001/07/29 04:06:42 richard
1427 # Fixed problem in link display when Link value is None.
1428 #
1429 # Revision 1.5 2001/07/28 08:17:09 richard
1430 # fixed use of stylesheet
1431 #
1432 # Revision 1.4 2001/07/28 07:59:53 richard
1433 # Replaced errno integers with their module values.
1434 # De-tabbed templatebuilder.py
1435 #
1436 # Revision 1.3 2001/07/25 03:39:47 richard
1437 # Hrm - displaying links to classes that don't specify a key property. I've
1438 # got it defaulting to 'name', then 'title' and then a "random" property (first
1439 # one returned by getprops().keys().
1440 # Needs to be moved onto the Class I think...
1441 #
1442 # Revision 1.2 2001/07/22 12:09:32 richard
1443 # Final commit of Grande Splite
1444 #
1445 # Revision 1.1 2001/07/22 11:58:35 richard
1446 # More Grande Splite
1447 #
1448 #
1449 # vim: set filetype=python ts=4 sw=4 et si