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.88 2002-04-24 08:34:35 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 pretty = interval.pretty()
438 if pretty is None:
439 pretty = value.pretty()
440 return pretty
441 return str(interval)
443 def do_download(self, property, **args):
444 ''' show a Link("file") or Multilink("file") property using links that
445 allow you to download files
446 '''
447 if not self.nodeid:
448 return _('[Download: not called from item]')
449 return self.do_link(property, is_download=1)
452 def do_checklist(self, property, **args):
453 ''' for a Link or Multilink property, display checkboxes for the
454 available choices to permit filtering
455 '''
456 propclass = self.properties[property]
457 if (not isinstance(propclass, hyperdb.Link) and not
458 isinstance(propclass, hyperdb.Multilink)):
459 return _('[Checklist: not a link]')
461 # get our current checkbox state
462 if self.nodeid:
463 # get the info from the node - make sure it's a list
464 if isinstance(propclass, hyperdb.Link):
465 value = [self.cl.get(self.nodeid, property)]
466 else:
467 value = self.cl.get(self.nodeid, property)
468 elif self.filterspec is not None:
469 # get the state from the filter specification (always a list)
470 value = self.filterspec.get(property, [])
471 else:
472 # it's a new node, so there's no state
473 value = []
475 # so we can map to the linked node's "lable" property
476 linkcl = self.db.classes[propclass.classname]
477 l = []
478 k = linkcl.labelprop()
479 for optionid in linkcl.list():
480 option = cgi.escape(str(linkcl.get(optionid, k)))
481 if optionid in value or option in value:
482 checked = 'checked'
483 else:
484 checked = ''
485 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
486 option, checked, property, option))
488 # for Links, allow the "unselected" option too
489 if isinstance(propclass, hyperdb.Link):
490 if value is None or '-1' in value:
491 checked = 'checked'
492 else:
493 checked = ''
494 l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
495 'value="-1">')%(checked, property))
496 return '\n'.join(l)
498 def do_note(self, rows=5, cols=80):
499 ''' display a "note" field, which is a text area for entering a note to
500 go along with a change.
501 '''
502 # TODO: pull the value from the form
503 return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
504 '</textarea>'%(rows, cols)
506 # XXX new function
507 def do_list(self, property, reverse=0):
508 ''' list the items specified by property using the standard index for
509 the class
510 '''
511 propcl = self.properties[property]
512 if not isinstance(propcl, hyperdb.Multilink):
513 return _('[List: not a Multilink]')
515 value = self.determine_value(property)
516 if not value:
517 return ''
519 # sort, possibly revers and then re-stringify
520 value = map(int, value)
521 value.sort()
522 if reverse:
523 value.reverse()
524 value = map(str, value)
526 # render the sub-index into a string
527 fp = StringIO.StringIO()
528 try:
529 write_save = self.client.write
530 self.client.write = fp.write
531 index = IndexTemplate(self.client, self.templates, propcl.classname)
532 index.render(nodeids=value, show_display_form=0)
533 finally:
534 self.client.write = write_save
536 return fp.getvalue()
538 # XXX new function
539 def do_history(self, direction='descending'):
540 ''' list the history of the item
542 If "direction" is 'descending' then the most recent event will
543 be displayed first. If it is 'ascending' then the oldest event
544 will be displayed first.
545 '''
546 if self.nodeid is None:
547 return _("[History: node doesn't exist]")
549 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
550 '<tr class="list-header">',
551 _('<th align=left><span class="list-item">Date</span></th>'),
552 _('<th align=left><span class="list-item">User</span></th>'),
553 _('<th align=left><span class="list-item">Action</span></th>'),
554 _('<th align=left><span class="list-item">Args</span></th>'),
555 '</tr>']
557 comments = {}
558 history = self.cl.history(self.nodeid)
559 history.sort()
560 if direction == 'descending':
561 history.reverse()
562 for id, evt_date, user, action, args in history:
563 date_s = str(evt_date).replace("."," ")
564 arg_s = ''
565 if action == 'link' and type(args) == type(()):
566 if len(args) == 3:
567 linkcl, linkid, key = args
568 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
569 linkcl, linkid, key)
570 else:
571 arg_s = str(args)
573 elif action == 'unlink' and type(args) == type(()):
574 if len(args) == 3:
575 linkcl, linkid, key = args
576 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
577 linkcl, linkid, key)
578 else:
579 arg_s = str(args)
581 elif type(args) == type({}):
582 cell = []
583 for k in args.keys():
584 # try to get the relevant property and treat it
585 # specially
586 try:
587 prop = self.properties[k]
588 except:
589 prop = None
590 if prop is not None:
591 if args[k] and (isinstance(prop, hyperdb.Multilink) or
592 isinstance(prop, hyperdb.Link)):
593 # figure what the link class is
594 classname = prop.classname
595 try:
596 linkcl = self.db.classes[classname]
597 except KeyError:
598 labelprop = None
599 comments[classname] = _('''The linked class
600 %(classname)s no longer exists''')%locals()
601 labelprop = linkcl.labelprop()
603 if isinstance(prop, hyperdb.Multilink) and \
604 len(args[k]) > 0:
605 ml = []
606 for linkid in args[k]:
607 label = classname + linkid
608 # if we have a label property, try to use it
609 # TODO: test for node existence even when
610 # there's no labelprop!
611 try:
612 if labelprop is not None:
613 label = linkcl.get(linkid, labelprop)
614 except IndexError:
615 comments['no_link'] = _('''<strike>The
616 linked node no longer
617 exists</strike>''')
618 ml.append('<strike>%s</strike>'%label)
619 else:
620 ml.append('<a href="%s%s">%s</a>'%(
621 classname, linkid, label))
622 cell.append('%s:\n %s'%(k, ',\n '.join(ml)))
623 elif isinstance(prop, hyperdb.Link) and args[k]:
624 label = classname + args[k]
625 # if we have a label property, try to use it
626 # TODO: test for node existence even when
627 # there's no labelprop!
628 if labelprop is not None:
629 try:
630 label = linkcl.get(args[k], labelprop)
631 except IndexError:
632 comments['no_link'] = _('''<strike>The
633 linked node no longer
634 exists</strike>''')
635 cell.append(' <strike>%s</strike>,\n'%label)
636 # "flag" this is done .... euwww
637 label = None
638 if label is not None:
639 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
640 classname, args[k], label))
642 elif isinstance(prop, hyperdb.Date) and args[k]:
643 d = date.Date(args[k])
644 cell.append('%s: %s'%(k, str(d)))
646 elif isinstance(prop, hyperdb.Interval) and args[k]:
647 d = date.Interval(args[k])
648 cell.append('%s: %s'%(k, str(d)))
650 elif not args[k]:
651 cell.append('%s: (no value)\n'%k)
653 else:
654 cell.append('%s: %s\n'%(k, str(args[k])))
655 else:
656 # property no longer exists
657 comments['no_exist'] = _('''<em>The indicated property
658 no longer exists</em>''')
659 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
660 arg_s = '<br />'.join(cell)
661 else:
662 # unkown event!!
663 comments['unknown'] = _('''<strong><em>This event is not
664 handled by the history display!</em></strong>''')
665 arg_s = '<strong><em>' + str(args) + '</em></strong>'
666 date_s = date_s.replace(' ', ' ')
667 l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
668 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
669 user, action, arg_s))
670 if comments:
671 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
672 for entry in comments.values():
673 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
674 l.append('</table>')
675 return '\n'.join(l)
677 # XXX new function
678 def do_submit(self):
679 ''' add a submit button for the item
680 '''
681 if self.nodeid:
682 return _('<input type="submit" name="submit" value="Submit Changes">')
683 elif self.form is not None:
684 return _('<input type="submit" name="submit" value="Submit New Entry">')
685 else:
686 return _('[Submit: not called from item]')
688 def do_classhelp(self, classname, properties, label='?', width='400',
689 height='400'):
690 '''pop up a javascript window with class help
692 This generates a link to a popup window which displays the
693 properties indicated by "properties" of the class named by
694 "classname". The "properties" should be a comma-separated list
695 (eg. 'id,name,description').
697 You may optionally override the label displayed, the width and
698 height. The popup window will be resizable and scrollable.
699 '''
700 return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
701 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
702 properties, width, height, label)
703 #
704 # INDEX TEMPLATES
705 #
706 class IndexTemplateReplace:
707 '''Regular-expression based parser that turns the template into HTML.
708 '''
709 def __init__(self, globals, locals, props):
710 self.globals = globals
711 self.locals = locals
712 self.props = props
714 replace=re.compile(
715 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
716 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
717 def go(self, text):
718 return self.replace.sub(self, text)
720 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
721 if m.group('name'):
722 if m.group('name') in self.props:
723 text = m.group('text')
724 replace = IndexTemplateReplace(self.globals, {}, self.props)
725 return replace.go(text)
726 else:
727 return ''
728 if m.group('display'):
729 command = m.group('command')
730 return eval(command, self.globals, self.locals)
731 return '*** unhandled match: %s'%str(m.groupdict())
733 class IndexTemplate(TemplateFunctions):
734 '''Templating functionality specifically for index pages
735 '''
736 def __init__(self, client, templates, classname):
737 TemplateFunctions.__init__(self)
738 self.client = client
739 self.instance = client.instance
740 self.templates = templates
741 self.classname = classname
743 # derived
744 self.db = self.client.db
745 self.cl = self.db.classes[self.classname]
746 self.properties = self.cl.getprops()
748 col_re=re.compile(r'<property\s+name="([^>]+)">')
749 def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
750 show_display_form=1, nodeids=None, show_customization=1):
751 self.filterspec = filterspec
753 w = self.client.write
755 # get the filter template
756 try:
757 filter_template = open(os.path.join(self.templates,
758 self.classname+'.filter')).read()
759 all_filters = self.col_re.findall(filter_template)
760 except IOError, error:
761 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
762 filter_template = None
763 all_filters = []
765 # XXX deviate from spec here ...
766 # load the index section template and figure the default columns from it
767 try:
768 template = open(os.path.join(self.templates,
769 self.classname+'.index')).read()
770 except IOError, error:
771 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
772 raise MissingTemplateError, self.classname+'.index'
773 all_columns = self.col_re.findall(template)
774 if not columns:
775 columns = []
776 for name in all_columns:
777 columns.append(name)
778 else:
779 # re-sort columns to be the same order as all_columns
780 l = []
781 for name in all_columns:
782 if name in columns:
783 l.append(name)
784 columns = l
786 # display the filter section
787 if (show_display_form and
788 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
789 w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
790 self.filter_section(filter_template, filter, columns, group,
791 all_filters, all_columns, show_customization)
792 # make sure that the sorting doesn't get lost either
793 if sort:
794 w('<input type="hidden" name=":sort" value="%s">'%
795 ','.join(sort))
796 w('</form>\n')
799 # now display the index section
800 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
801 w('<tr class="list-header">\n')
802 for name in columns:
803 cname = name.capitalize()
804 if show_display_form:
805 sb = self.sortby(name, filterspec, columns, filter, group, sort)
806 anchor = "%s?%s"%(self.classname, sb)
807 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
808 anchor, cname))
809 else:
810 w('<td><span class="list-header">%s</span></td>\n'%cname)
811 w('</tr>\n')
813 # this stuff is used for group headings - optimise the group names
814 old_group = None
815 group_names = []
816 if group:
817 for name in group:
818 if name[0] == '-': group_names.append(name[1:])
819 else: group_names.append(name)
821 # now actually loop through all the nodes we get from the filter and
822 # apply the template
823 if nodeids is None:
824 nodeids = self.cl.filter(filterspec, sort, group)
825 for nodeid in nodeids:
826 # check for a group heading
827 if group_names:
828 this_group = [self.cl.get(nodeid, name, _('[no value]'))
829 for name in group_names]
830 if this_group != old_group:
831 l = []
832 for name in group_names:
833 prop = self.properties[name]
834 if isinstance(prop, hyperdb.Link):
835 group_cl = self.db.classes[prop.classname]
836 key = group_cl.getkey()
837 value = self.cl.get(nodeid, name)
838 if value is None:
839 l.append(_('[unselected %(classname)s]')%{
840 'classname': prop.classname})
841 else:
842 l.append(group_cl.get(self.cl.get(nodeid,
843 name), key))
844 elif isinstance(prop, hyperdb.Multilink):
845 group_cl = self.db.classes[prop.classname]
846 key = group_cl.getkey()
847 for value in self.cl.get(nodeid, name):
848 l.append(group_cl.get(value, key))
849 else:
850 value = self.cl.get(nodeid, name, _('[no value]'))
851 if value is None:
852 value = _('[empty %(name)s]')%locals()
853 else:
854 value = str(value)
855 l.append(value)
856 w('<tr class="section-bar">'
857 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
858 len(columns), ', '.join(l)))
859 old_group = this_group
861 # display this node's row
862 replace = IndexTemplateReplace(self.globals, locals(), columns)
863 self.nodeid = nodeid
864 w(replace.go(template))
865 self.nodeid = None
867 w('</table>')
869 # display the filter section
870 if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
871 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
872 w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
873 self.filter_section(filter_template, filter, columns, group,
874 all_filters, all_columns, show_customization)
875 # make sure that the sorting doesn't get lost either
876 if sort:
877 w('<input type="hidden" name=":sort" value="%s">'%
878 ','.join(sort))
879 w('</form>\n')
882 def filter_section(self, template, filter, columns, group, all_filters,
883 all_columns, show_customization):
885 w = self.client.write
887 # wrap the template in a single table to ensure the whole widget
888 # is displayed at once
889 w('<table><tr><td>')
891 if template and filter:
892 # display the filter section
893 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
894 w('<tr class="location-bar">')
895 w(_(' <th align="left" colspan="2">Filter specification...</th>'))
896 w('</tr>')
897 replace = IndexTemplateReplace(self.globals, locals(), filter)
898 w(replace.go(template))
899 w('<tr class="location-bar"><td width="1%%"> </td>')
900 w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
901 w('</table>')
903 # now add in the filter/columns/group/etc config table form
904 w('<input type="hidden" name="show_customization" value="%s">' %
905 show_customization )
906 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
907 names = []
908 seen = {}
909 for name in all_filters + all_columns:
910 if self.properties.has_key(name) and not seen.has_key(name):
911 names.append(name)
912 seen[name] = 1
913 if show_customization:
914 action = '-'
915 else:
916 action = '+'
917 # hide the values for filters, columns and grouping in the form
918 # if the customization widget is not visible
919 for name in names:
920 if all_filters and name in filter:
921 w('<input type="hidden" name=":filter" value="%s">' % name)
922 if all_columns and name in columns:
923 w('<input type="hidden" name=":columns" value="%s">' % name)
924 if all_columns and name in group:
925 w('<input type="hidden" name=":group" value="%s">' % name)
927 # TODO: The widget style can go into the stylesheet
928 w(_('<th align="left" colspan=%s>'
929 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
930 'customisation...</th></tr>\n')%(len(names)+1, action))
932 if not show_customization:
933 w('</table>\n')
934 return
936 w('<tr class="location-bar"><th> </th>')
937 for name in names:
938 w('<th>%s</th>'%name.capitalize())
939 w('</tr>\n')
941 # Filter
942 if all_filters:
943 w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
944 for name in names:
945 if name not in all_filters:
946 w('<td> </td>')
947 continue
948 if name in filter: checked=' checked'
949 else: checked=''
950 w('<td align=middle>\n')
951 w(' <input type="checkbox" name=":filter" value="%s" '
952 '%s></td>\n'%(name, checked))
953 w('</tr>\n')
955 # Columns
956 if all_columns:
957 w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
958 for name in names:
959 if name not in all_columns:
960 w('<td> </td>')
961 continue
962 if name in columns: checked=' checked'
963 else: checked=''
964 w('<td align=middle>\n')
965 w(' <input type="checkbox" name=":columns" value="%s"'
966 '%s></td>\n'%(name, checked))
967 w('</tr>\n')
969 # Grouping
970 w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
971 for name in names:
972 if name not in all_columns:
973 w('<td> </td>')
974 continue
975 if name in group: checked=' checked'
976 else: checked=''
977 w('<td align=middle>\n')
978 w(' <input type="checkbox" name=":group" value="%s"'
979 '%s></td>\n'%(name, checked))
980 w('</tr>\n')
982 w('<tr class="location-bar"><td width="1%"> </td>')
983 w('<td colspan="%s">'%len(names))
984 w(_('<input type="submit" name="action" value="Redisplay"></td>'))
985 w('</tr>\n')
986 w('</table>\n')
988 # and the outer table
989 w('</td></tr></table>')
992 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
993 l = []
994 w = l.append
995 for k, v in filterspec.items():
996 k = urllib.quote(k)
997 if type(v) == type([]):
998 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
999 else:
1000 w('%s=%s'%(k, urllib.quote(v)))
1001 if columns:
1002 w(':columns=%s'%','.join(map(urllib.quote, columns)))
1003 if filter:
1004 w(':filter=%s'%','.join(map(urllib.quote, filter)))
1005 if group:
1006 w(':group=%s'%','.join(map(urllib.quote, group)))
1007 m = []
1008 s_dir = ''
1009 for name in sort:
1010 dir = name[0]
1011 if dir == '-':
1012 name = name[1:]
1013 else:
1014 dir = ''
1015 if sort_name == name:
1016 if dir == '-':
1017 s_dir = ''
1018 else:
1019 s_dir = '-'
1020 else:
1021 m.append(dir+urllib.quote(name))
1022 m.insert(0, s_dir+urllib.quote(sort_name))
1023 # so things don't get completely out of hand, limit the sort to
1024 # two columns
1025 w(':sort=%s'%','.join(m[:2]))
1026 return '&'.join(l)
1028 #
1029 # ITEM TEMPLATES
1030 #
1031 class ItemTemplateReplace:
1032 '''Regular-expression based parser that turns the template into HTML.
1033 '''
1034 def __init__(self, globals, locals, cl, nodeid):
1035 self.globals = globals
1036 self.locals = locals
1037 self.cl = cl
1038 self.nodeid = nodeid
1040 replace=re.compile(
1041 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1042 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1043 def go(self, text):
1044 return self.replace.sub(self, text)
1046 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1047 if m.group('name'):
1048 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1049 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1050 self.nodeid)
1051 return replace.go(m.group('text'))
1052 else:
1053 return ''
1054 if m.group('display'):
1055 command = m.group('command')
1056 return eval(command, self.globals, self.locals)
1057 return '*** unhandled match: %s'%str(m.groupdict())
1060 class ItemTemplate(TemplateFunctions):
1061 '''Templating functionality specifically for item (node) display
1062 '''
1063 def __init__(self, client, templates, classname):
1064 TemplateFunctions.__init__(self)
1065 self.client = client
1066 self.instance = client.instance
1067 self.templates = templates
1068 self.classname = classname
1070 # derived
1071 self.db = self.client.db
1072 self.cl = self.db.classes[self.classname]
1073 self.properties = self.cl.getprops()
1075 def render(self, nodeid):
1076 self.nodeid = nodeid
1078 if (self.properties.has_key('type') and
1079 self.properties.has_key('content')):
1080 pass
1081 # XXX we really want to return this as a downloadable...
1082 # currently I handle this at a higher level by detecting 'file'
1083 # designators...
1085 w = self.client.write
1086 w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1087 self.classname, nodeid))
1088 s = open(os.path.join(self.templates, self.classname+'.item')).read()
1089 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1090 w(replace.go(s))
1091 w('</form>')
1094 class NewItemTemplate(TemplateFunctions):
1095 '''Templating functionality specifically for NEW item (node) display
1096 '''
1097 def __init__(self, client, templates, classname):
1098 TemplateFunctions.__init__(self)
1099 self.client = client
1100 self.instance = client.instance
1101 self.templates = templates
1102 self.classname = classname
1104 # derived
1105 self.db = self.client.db
1106 self.cl = self.db.classes[self.classname]
1107 self.properties = self.cl.getprops()
1109 def render(self, form):
1110 self.form = form
1111 w = self.client.write
1112 c = self.classname
1113 try:
1114 s = open(os.path.join(self.templates, c+'.newitem')).read()
1115 except IOError:
1116 s = open(os.path.join(self.templates, c+'.item')).read()
1117 w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1118 for key in form.keys():
1119 if key[0] == ':':
1120 value = form[key].value
1121 if type(value) != type([]): value = [value]
1122 for value in value:
1123 w('<input type="hidden" name="%s" value="%s">'%(key, value))
1124 replace = ItemTemplateReplace(self.globals, locals(), None, None)
1125 w(replace.go(s))
1126 w('</form>')
1128 #
1129 # $Log: not supported by cvs2svn $
1130 # Revision 1.87 2002/04/03 06:12:46 richard
1131 # Fix for date properties as labels.
1132 #
1133 # Revision 1.86 2002/04/03 05:54:31 richard
1134 # Fixed serialisation problem by moving the serialisation step out of the
1135 # hyperdb.Class (get, set) into the hyperdb.Database.
1136 #
1137 # Also fixed htmltemplate after the showid changes I made yesterday.
1138 #
1139 # Unit tests for all of the above written.
1140 #
1141 # Revision 1.85 2002/04/02 01:40:58 richard
1142 # . link() htmltemplate function now has a "showid" option for links and
1143 # multilinks. When true, it only displays the linked node id as the anchor
1144 # text. The link value is displayed as a tooltip using the title anchor
1145 # attribute.
1146 #
1147 # Revision 1.84 2002/03/29 19:41:48 rochecompaan
1148 # . Fixed display of mutlilink properties when using the template
1149 # functions, menu and plain.
1150 #
1151 # Revision 1.83 2002/02/27 04:14:31 richard
1152 # Ran it through pychecker, made fixes
1153 #
1154 # Revision 1.82 2002/02/21 23:11:45 richard
1155 # . fixed some problems in date calculations (calendar.py doesn't handle over-
1156 # and under-flow). Also, hour/minute/second intervals may now be more than
1157 # 99 each.
1158 #
1159 # Revision 1.81 2002/02/21 07:21:38 richard
1160 # docco
1161 #
1162 # Revision 1.80 2002/02/21 07:19:08 richard
1163 # ... and label, width and height control for extra flavour!
1164 #
1165 # Revision 1.79 2002/02/21 06:57:38 richard
1166 # . Added popup help for classes using the classhelp html template function.
1167 # - add <display call="classhelp('priority', 'id,name,description')">
1168 # to an item page, and it generates a link to a popup window which displays
1169 # the id, name and description for the priority class. The description
1170 # field won't exist in most installations, but it will be added to the
1171 # default templates.
1172 #
1173 # Revision 1.78 2002/02/21 06:23:00 richard
1174 # *** empty log message ***
1175 #
1176 # Revision 1.77 2002/02/20 05:05:29 richard
1177 # . Added simple editing for classes that don't define a templated interface.
1178 # - access using the admin "class list" interface
1179 # - limited to admin-only
1180 # - requires the csv module from object-craft (url given if it's missing)
1181 #
1182 # Revision 1.76 2002/02/16 09:10:52 richard
1183 # oops
1184 #
1185 # Revision 1.75 2002/02/16 08:43:23 richard
1186 # . #517906 ] Attribute order in "View customisation"
1187 #
1188 # Revision 1.74 2002/02/16 08:39:42 richard
1189 # . #516854 ] "My Issues" and redisplay
1190 #
1191 # Revision 1.73 2002/02/15 07:08:44 richard
1192 # . Alternate email addresses are now available for users. See the MIGRATION
1193 # file for info on how to activate the feature.
1194 #
1195 # Revision 1.72 2002/02/14 23:39:18 richard
1196 # . All forms now have "double-submit" protection when Javascript is enabled
1197 # on the client-side.
1198 #
1199 # Revision 1.71 2002/01/23 06:15:24 richard
1200 # real (non-string, duh) sorting of lists by node id
1201 #
1202 # Revision 1.70 2002/01/23 05:47:57 richard
1203 # more HTML template cleanup and unit tests
1204 #
1205 # Revision 1.69 2002/01/23 05:10:27 richard
1206 # More HTML template cleanup and unit tests.
1207 # - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1208 # templates, but link(is_download=1) will still work for existing templates]
1209 #
1210 # Revision 1.68 2002/01/22 22:55:28 richard
1211 # . htmltemplate list() wasn't sorting...
1212 #
1213 # Revision 1.67 2002/01/22 22:46:22 richard
1214 # more htmltemplate cleanups and unit tests
1215 #
1216 # Revision 1.66 2002/01/22 06:35:40 richard
1217 # more htmltemplate tests and cleanup
1218 #
1219 # Revision 1.65 2002/01/22 00:12:06 richard
1220 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1221 # off the implementation of some of the functions so they behave sanely.
1222 #
1223 # Revision 1.64 2002/01/21 03:25:59 richard
1224 # oops
1225 #
1226 # Revision 1.63 2002/01/21 02:59:10 richard
1227 # Fixed up the HTML display of history so valid links are actually displayed.
1228 # Oh for some unit tests! :(
1229 #
1230 # Revision 1.62 2002/01/18 08:36:12 grubert
1231 # . add nowrap to history table date cell i.e. <td nowrap ...
1232 #
1233 # Revision 1.61 2002/01/17 23:04:53 richard
1234 # . much nicer history display (actualy real handling of property types etc)
1235 #
1236 # Revision 1.60 2002/01/17 08:48:19 grubert
1237 # . display superseder as html link in history.
1238 #
1239 # Revision 1.59 2002/01/17 07:58:24 grubert
1240 # . display links a html link in history.
1241 #
1242 # Revision 1.58 2002/01/15 00:50:03 richard
1243 # #502949 ] index view for non-issues and redisplay
1244 #
1245 # Revision 1.57 2002/01/14 23:31:21 richard
1246 # reverted the change that had plain() hyperlinking the link displays -
1247 # that's what link() is for!
1248 #
1249 # Revision 1.56 2002/01/14 07:04:36 richard
1250 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1251 # the linked node's page.
1252 # ... this allows a display very similar to bugzilla's where you can actually
1253 # find out information about the linked node.
1254 #
1255 # Revision 1.55 2002/01/14 06:45:03 richard
1256 # . #502953 ] nosy-like treatment of other multilinks
1257 # ... had to revert most of the previous change to the multilink field
1258 # display... not good.
1259 #
1260 # Revision 1.54 2002/01/14 05:16:51 richard
1261 # The submit buttons need a name attribute or mozilla won't submit without a
1262 # file upload. Yeah, that's bloody obscure. Grr.
1263 #
1264 # Revision 1.53 2002/01/14 04:03:32 richard
1265 # How about that ... date fields have never worked ...
1266 #
1267 # Revision 1.52 2002/01/14 02:20:14 richard
1268 # . changed all config accesses so they access either the instance or the
1269 # config attriubute on the db. This means that all config is obtained from
1270 # instance_config instead of the mish-mash of classes. This will make
1271 # switching to a ConfigParser setup easier too, I hope.
1272 #
1273 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1274 # 0.5.0 switch, I hope!)
1275 #
1276 # Revision 1.51 2002/01/10 10:02:15 grubert
1277 # In do_history: replace "." in date by " " so html wraps more sensible.
1278 # Should this be done in date's string converter ?
1279 #
1280 # Revision 1.50 2002/01/05 02:35:10 richard
1281 # I18N'ification
1282 #
1283 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1284 # Features added:
1285 # . Multilink properties are now displayed as comma separated values in
1286 # a textbox
1287 # . The add user link is now only visible to the admin user
1288 # . Modified the mail gateway to reject submissions from unknown
1289 # addresses if ANONYMOUS_ACCESS is denied
1290 #
1291 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1292 # Bugs fixed:
1293 # . Exception handling in hyperdb for strings-that-look-like numbers got
1294 # lost somewhere
1295 # . Internet Explorer submits full path for filename - we now strip away
1296 # the path
1297 # Features added:
1298 # . Link and multilink properties are now displayed sorted in the cgi
1299 # interface
1300 #
1301 # Revision 1.47 2001/11/26 22:55:56 richard
1302 # Feature:
1303 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1304 # the instance.
1305 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1306 # signature info in e-mails.
1307 # . Some more flexibility in the mail gateway and more error handling.
1308 # . Login now takes you to the page you back to the were denied access to.
1309 #
1310 # Fixed:
1311 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1312 #
1313 # Revision 1.46 2001/11/24 00:53:12 jhermann
1314 # "except:" is bad, bad , bad!
1315 #
1316 # Revision 1.45 2001/11/22 15:46:42 jhermann
1317 # Added module docstrings to all modules.
1318 #
1319 # Revision 1.44 2001/11/21 23:35:45 jhermann
1320 # Added globbing for win32, and sample marking in a 2nd file to test it
1321 #
1322 # Revision 1.43 2001/11/21 04:04:43 richard
1323 # *sigh* more missing value handling
1324 #
1325 # Revision 1.42 2001/11/21 03:40:54 richard
1326 # more new property handling
1327 #
1328 # Revision 1.41 2001/11/15 10:26:01 richard
1329 # . missing "return" in filter_section (thanks Roch'e Compaan)
1330 #
1331 # Revision 1.40 2001/11/03 01:56:51 richard
1332 # More HTML compliance fixes. This will probably fix the Netscape problem
1333 # too.
1334 #
1335 # Revision 1.39 2001/11/03 01:43:47 richard
1336 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1337 #
1338 # Revision 1.38 2001/10/31 06:58:51 richard
1339 # Added the wrap="hard" attribute to the textarea of the note field so the
1340 # messages wrap sanely.
1341 #
1342 # Revision 1.37 2001/10/31 06:24:35 richard
1343 # Added do_stext to htmltemplate, thanks Brad Clements.
1344 #
1345 # Revision 1.36 2001/10/28 22:51:38 richard
1346 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1347 #
1348 # Revision 1.35 2001/10/24 00:04:41 richard
1349 # Removed the "infinite authentication loop", thanks Roch'e
1350 #
1351 # Revision 1.34 2001/10/23 22:56:36 richard
1352 # Bugfix in filter "widget" placement, thanks Roch'e
1353 #
1354 # Revision 1.33 2001/10/23 01:00:18 richard
1355 # Re-enabled login and registration access after lopping them off via
1356 # disabling access for anonymous users.
1357 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1358 # a couple of bugs while I was there. Probably introduced a couple, but
1359 # things seem to work OK at the moment.
1360 #
1361 # Revision 1.32 2001/10/22 03:25:01 richard
1362 # Added configuration for:
1363 # . anonymous user access and registration (deny/allow)
1364 # . filter "widget" location on index page (top, bottom, both)
1365 # Updated some documentation.
1366 #
1367 # Revision 1.31 2001/10/21 07:26:35 richard
1368 # feature #473127: Filenames. I modified the file.index and htmltemplate
1369 # source so that the filename is used in the link and the creation
1370 # information is displayed.
1371 #
1372 # Revision 1.30 2001/10/21 04:44:50 richard
1373 # bug #473124: UI inconsistency with Link fields.
1374 # This also prompted me to fix a fairly long-standing usability issue -
1375 # that of being able to turn off certain filters.
1376 #
1377 # Revision 1.29 2001/10/21 00:17:56 richard
1378 # CGI interface view customisation section may now be hidden (patch from
1379 # Roch'e Compaan.)
1380 #
1381 # Revision 1.28 2001/10/21 00:00:16 richard
1382 # Fixed Checklist function - wasn't always working on a list.
1383 #
1384 # Revision 1.27 2001/10/20 12:13:44 richard
1385 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1386 #
1387 # Revision 1.26 2001/10/14 10:55:00 richard
1388 # Handle empty strings in HTML template Link function
1389 #
1390 # Revision 1.25 2001/10/09 07:25:59 richard
1391 # Added the Password property type. See "pydoc roundup.password" for
1392 # implementation details. Have updated some of the documentation too.
1393 #
1394 # Revision 1.24 2001/09/27 06:45:58 richard
1395 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1396 # on the plain() template function to escape the text for HTML.
1397 #
1398 # Revision 1.23 2001/09/10 09:47:18 richard
1399 # Fixed bug in the generation of links to Link/Multilink in indexes.
1400 # (thanks Hubert Hoegl)
1401 # Added AssignedTo to the "classic" schema's item page.
1402 #
1403 # Revision 1.22 2001/08/30 06:01:17 richard
1404 # Fixed missing import in mailgw :(
1405 #
1406 # Revision 1.21 2001/08/16 07:34:59 richard
1407 # better CGI text searching - but hidden filter fields are disappearing...
1408 #
1409 # Revision 1.20 2001/08/15 23:43:18 richard
1410 # Fixed some isFooTypes that I missed.
1411 # Refactored some code in the CGI code.
1412 #
1413 # Revision 1.19 2001/08/12 06:32:36 richard
1414 # using isinstance(blah, Foo) now instead of isFooType
1415 #
1416 # Revision 1.18 2001/08/07 00:24:42 richard
1417 # stupid typo
1418 #
1419 # Revision 1.17 2001/08/07 00:15:51 richard
1420 # Added the copyright/license notice to (nearly) all files at request of
1421 # Bizar Software.
1422 #
1423 # Revision 1.16 2001/08/01 03:52:23 richard
1424 # Checklist was using wrong name.
1425 #
1426 # Revision 1.15 2001/07/30 08:12:17 richard
1427 # Added time logging and file uploading to the templates.
1428 #
1429 # Revision 1.14 2001/07/30 06:17:45 richard
1430 # Features:
1431 # . Added ability for cgi newblah forms to indicate that the new node
1432 # should be linked somewhere.
1433 # Fixed:
1434 # . Fixed the agument handling for the roundup-admin find command.
1435 # . Fixed handling of summary when no note supplied for newblah. Again.
1436 # . Fixed detection of no form in htmltemplate Field display.
1437 #
1438 # Revision 1.13 2001/07/30 02:37:53 richard
1439 # Temporary measure until we have decent schema migration.
1440 #
1441 # Revision 1.12 2001/07/30 01:24:33 richard
1442 # Handles new node display now.
1443 #
1444 # Revision 1.11 2001/07/29 09:31:35 richard
1445 # oops
1446 #
1447 # Revision 1.10 2001/07/29 09:28:23 richard
1448 # Fixed sorting by clicking on column headings.
1449 #
1450 # Revision 1.9 2001/07/29 08:27:40 richard
1451 # Fixed handling of passed-in values in form elements (ie. during a
1452 # drill-down)
1453 #
1454 # Revision 1.8 2001/07/29 07:01:39 richard
1455 # Added vim command to all source so that we don't get no steenkin' tabs :)
1456 #
1457 # Revision 1.7 2001/07/29 05:36:14 richard
1458 # Cleanup of the link label generation.
1459 #
1460 # Revision 1.6 2001/07/29 04:06:42 richard
1461 # Fixed problem in link display when Link value is None.
1462 #
1463 # Revision 1.5 2001/07/28 08:17:09 richard
1464 # fixed use of stylesheet
1465 #
1466 # Revision 1.4 2001/07/28 07:59:53 richard
1467 # Replaced errno integers with their module values.
1468 # De-tabbed templatebuilder.py
1469 #
1470 # Revision 1.3 2001/07/25 03:39:47 richard
1471 # Hrm - displaying links to classes that don't specify a key property. I've
1472 # got it defaulting to 'name', then 'title' and then a "random" property (first
1473 # one returned by getprops().keys().
1474 # Needs to be moved onto the Class I think...
1475 #
1476 # Revision 1.2 2001/07/22 12:09:32 richard
1477 # Final commit of Grande Splite
1478 #
1479 # Revision 1.1 2001/07/22 11:58:35 richard
1480 # More Grande Splite
1481 #
1482 #
1483 # vim: set filetype=python ts=4 sw=4 et si