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