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