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