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