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.93 2002-06-27 12:05:25 gmcm 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(1)
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(1)
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(1)
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(1)
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(1)
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(1)
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(1)
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(1)
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(1)
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(1)
620 hrefable = os.path.exists(
621 os.path.join(self.templates, classname+'.item'))
623 if isinstance(prop, hyperdb.Multilink) and \
624 len(args[k]) > 0:
625 ml = []
626 for linkid in args[k]:
627 label = classname + linkid
628 # if we have a label property, try to use it
629 # TODO: test for node existence even when
630 # there's no labelprop!
631 try:
632 if labelprop is not None:
633 label = linkcl.get(linkid, labelprop)
634 except IndexError:
635 comments['no_link'] = _('''<strike>The
636 linked node no longer
637 exists</strike>''')
638 ml.append('<strike>%s</strike>'%label)
639 else:
640 if hrefable:
641 ml.append('<a href="%s%s">%s</a>'%(
642 classname, linkid, label))
643 else:
644 ml.append(label)
645 cell.append('%s:\n %s'%(k, ',\n '.join(ml)))
646 elif isinstance(prop, hyperdb.Link) and args[k]:
647 label = classname + args[k]
648 # if we have a label property, try to use it
649 # TODO: test for node existence even when
650 # there's no labelprop!
651 if labelprop is not None:
652 try:
653 label = linkcl.get(args[k], labelprop)
654 except IndexError:
655 comments['no_link'] = _('''<strike>The
656 linked node no longer
657 exists</strike>''')
658 cell.append(' <strike>%s</strike>,\n'%label)
659 # "flag" this is done .... euwww
660 label = None
661 if label is not None:
662 if hrefable:
663 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
664 classname, args[k], label))
665 else:
666 cell.append('%s: %s' % (k,label))
668 elif isinstance(prop, hyperdb.Date) and args[k]:
669 d = date.Date(args[k])
670 cell.append('%s: %s'%(k, str(d)))
672 elif isinstance(prop, hyperdb.Interval) and args[k]:
673 d = date.Interval(args[k])
674 cell.append('%s: %s'%(k, str(d)))
676 elif isinstance(prop, hyperdb.String) and args[k]:
677 cell.append('%s: %s'%(k, cgi.escape(args[k])))
679 elif not args[k]:
680 cell.append('%s: (no value)\n'%k)
682 else:
683 cell.append('%s: %s\n'%(k, str(args[k])))
684 else:
685 # property no longer exists
686 comments['no_exist'] = _('''<em>The indicated property
687 no longer exists</em>''')
688 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
689 arg_s = '<br />'.join(cell)
690 else:
691 # unkown event!!
692 comments['unknown'] = _('''<strong><em>This event is not
693 handled by the history display!</em></strong>''')
694 arg_s = '<strong><em>' + str(args) + '</em></strong>'
695 date_s = date_s.replace(' ', ' ')
696 l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
697 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
698 user, action, arg_s))
699 if comments:
700 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
701 for entry in comments.values():
702 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
703 l.append('</table>')
704 return '\n'.join(l)
706 # XXX new function
707 def do_submit(self):
708 ''' add a submit button for the item
709 '''
710 if self.nodeid:
711 return _('<input type="submit" name="submit" value="Submit Changes">')
712 elif self.form is not None:
713 return _('<input type="submit" name="submit" value="Submit New Entry">')
714 else:
715 return _('[Submit: not called from item]')
717 def do_classhelp(self, classname, properties, label='?', width='400',
718 height='400'):
719 '''pop up a javascript window with class help
721 This generates a link to a popup window which displays the
722 properties indicated by "properties" of the class named by
723 "classname". The "properties" should be a comma-separated list
724 (eg. 'id,name,description').
726 You may optionally override the label displayed, the width and
727 height. The popup window will be resizable and scrollable.
728 '''
729 return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
730 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
731 properties, width, height, label)
732 #
733 # INDEX TEMPLATES
734 #
735 class IndexTemplateReplace:
736 '''Regular-expression based parser that turns the template into HTML.
737 '''
738 def __init__(self, globals, locals, props):
739 self.globals = globals
740 self.locals = locals
741 self.props = props
743 replace=re.compile(
744 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
745 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
746 def go(self, text):
747 newtext = self.replace.sub(self, text)
748 self.locals = self.globals = None
749 return newtext
751 def __call__(self, m, search_text=None, filter=None, columns=None,
752 sort=None, group=None):
753 if m.group('name'):
754 if m.group('name') in self.props:
755 text = m.group('text')
756 replace = self.__class__(self.globals, {}, self.props)
757 return replace.go(text)
758 else:
759 return ''
760 if m.group('display'):
761 command = m.group('command')
762 return eval(command, self.globals, self.locals)
763 return '*** unhandled match: %s'%str(m.groupdict())
765 class IndexTemplate(TemplateFunctions):
766 '''Templating functionality specifically for index pages
767 '''
768 def __init__(self, client, templates, classname):
769 TemplateFunctions.__init__(self)
770 self.client = client
771 self.instance = client.instance
772 self.templates = templates
773 self.classname = classname
775 # derived
776 self.db = self.client.db
777 self.cl = self.db.classes[self.classname]
778 self.properties = self.cl.getprops()
780 col_re=re.compile(r'<property\s+name="([^>]+)">')
781 def render(self, filterspec={}, search_text='', filter=[], columns=[],
782 sort=[], group=[], show_display_form=1, nodeids=None,
783 show_customization=1, show_nodes=1):
785 self.filterspec = filterspec
787 w = self.client.write
789 # get the filter template
790 try:
791 filter_template = open(os.path.join(self.templates,
792 self.classname+'.filter')).read()
793 all_filters = self.col_re.findall(filter_template)
794 except IOError, error:
795 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
796 filter_template = None
797 all_filters = []
799 # XXX deviate from spec here ...
800 # load the index section template and figure the default columns from it
801 try:
802 template = open(os.path.join(self.templates,
803 self.classname+'.index')).read()
804 except IOError, error:
805 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
806 raise MissingTemplateError, self.classname+'.index'
807 all_columns = self.col_re.findall(template)
808 if not columns:
809 columns = []
810 for name in all_columns:
811 columns.append(name)
812 else:
813 # re-sort columns to be the same order as all_columns
814 l = []
815 for name in all_columns:
816 if name in columns:
817 l.append(name)
818 columns = l
820 # display the filter section
821 if (show_display_form and
822 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
823 w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
824 self.filter_section(filter_template, search_text, filter,
825 columns, group, all_filters, all_columns, show_customization)
826 # make sure that the sorting doesn't get lost either
827 if sort:
828 w('<input type="hidden" name=":sort" value="%s">'%
829 ','.join(sort))
830 w('</form>\n')
833 # now display the index section
834 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
835 w('<tr class="list-header">\n')
836 for name in columns:
837 cname = name.capitalize()
838 if show_display_form:
839 sb = self.sortby(name, filterspec, columns, filter, group, sort)
840 anchor = "%s?%s"%(self.classname, sb)
841 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
842 anchor, cname))
843 else:
844 w('<td><span class="list-header">%s</span></td>\n'%cname)
845 w('</tr>\n')
847 # this stuff is used for group headings - optimise the group names
848 old_group = None
849 group_names = []
850 if group:
851 for name in group:
852 if name[0] == '-': group_names.append(name[1:])
853 else: group_names.append(name)
855 # now actually loop through all the nodes we get from the filter and
856 # apply the template
857 if show_nodes:
858 matches = None
859 if nodeids is None:
860 if search_text != '':
861 matches = self.client.indexer.search(
862 search_text.split(' '), self.cl)
863 nodeids = self.cl.filter(matches, filterspec, sort, group)
864 for nodeid in nodeids:
865 # check for a group heading
866 if group_names:
867 this_group = [self.cl.get(nodeid, name, _('[no value]'))
868 for name in group_names]
869 if this_group != old_group:
870 l = []
871 for name in group_names:
872 prop = self.properties[name]
873 if isinstance(prop, hyperdb.Link):
874 group_cl = self.db.classes[prop.classname]
875 key = group_cl.getkey()
876 value = self.cl.get(nodeid, name)
877 if value is None:
878 l.append(_('[unselected %(classname)s]')%{
879 'classname': prop.classname})
880 else:
881 l.append(group_cl.get(self.cl.get(nodeid,
882 name), key))
883 elif isinstance(prop, hyperdb.Multilink):
884 group_cl = self.db.classes[prop.classname]
885 key = group_cl.getkey()
886 for value in self.cl.get(nodeid, name):
887 l.append(group_cl.get(value, key))
888 else:
889 value = self.cl.get(nodeid, name,
890 _('[no value]'))
891 if value is None:
892 value = _('[empty %(name)s]')%locals()
893 else:
894 value = str(value)
895 l.append(value)
896 w('<tr class="section-bar">'
897 '<td align=middle colspan=%s>'
898 '<strong>%s</strong></td></tr>'%(
899 len(columns), ', '.join(l)))
900 old_group = this_group
902 # display this node's row
903 replace = IndexTemplateReplace(self.globals, locals(), columns)
904 self.nodeid = nodeid
905 w(replace.go(template))
906 if matches:
907 self.node_matches(matches[nodeid], len(columns))
908 self.nodeid = None
910 w('</table>')
912 # display the filter section
913 if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
914 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
915 w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
916 self.filter_section(filter_template, search_text, filter,
917 columns, group, all_filters, all_columns, show_customization)
918 # make sure that the sorting doesn't get lost either
919 if sort:
920 w('<input type="hidden" name=":sort" value="%s">'%
921 ','.join(sort))
922 w('</form>\n')
924 self.db = self.cl = self.properties = None
926 def node_matches(self, match, colspan):
927 ''' display the files and messages for a node that matched a
928 full text search
929 '''
930 w = self.client.write
932 message_links = []
933 file_links = []
934 if match.has_key('messages'):
935 for msgid in match['messages']:
936 k = self.db.msg.labelprop(1)
937 lab = self.db.msg.get(msgid, k)
938 msgpath = 'msg%s'%msgid
939 message_links.append('<a href="%(msgpath)s">%(lab)s</a>'
940 %locals())
941 w(_('<tr class="row-hilite"><td colspan="%s">'
942 ' Matched messages: %s</td></tr>')%(
943 colspan, ', '.join(message_links)))
945 if match.has_key('files'):
946 for fileid in match['files']:
947 filename = self.db.file.get(fileid, 'name')
948 filepath = 'file%s/%s'%(fileid, filename)
949 file_links.append('<a href="%(filepath)s">%(filename)s</a>'
950 %locals())
951 w(_('<tr class="row-hilite"><td colspan="%s">'
952 ' Matched files: %s</td></tr>')%(
953 colspan, ', '.join(file_links)))
956 def filter_section(self, template, search_text, filter, columns, group,
957 all_filters, all_columns, show_customization):
959 w = self.client.write
961 # wrap the template in a single table to ensure the whole widget
962 # is displayed at once
963 w('<table><tr><td>')
965 if template and filter:
966 # display the filter section
967 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
968 w('<tr class="location-bar">')
969 w(_(' <th align="left" colspan="2">Filter specification...</th>'))
970 w('</tr>')
971 w('<tr>')
972 w('<th class="location-bar">Search terms</th>')
973 w('<td><input name="search_text" value="%s" size="50"></td>'%(
974 search_text))
975 w('</tr>')
976 replace = IndexTemplateReplace(self.globals, locals(), filter)
977 w(replace.go(template))
978 w('<tr class="location-bar"><td width="1%%"> </td>')
979 w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
980 w('</table>')
982 # now add in the filter/columns/group/etc config table form
983 w('<input type="hidden" name="show_customization" value="%s">' %
984 show_customization )
985 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
986 names = []
987 seen = {}
988 for name in all_filters + all_columns:
989 if self.properties.has_key(name) and not seen.has_key(name):
990 names.append(name)
991 seen[name] = 1
992 if show_customization:
993 action = '-'
994 else:
995 action = '+'
996 # hide the values for filters, columns and grouping in the form
997 # if the customization widget is not visible
998 for name in names:
999 if all_filters and name in filter:
1000 w('<input type="hidden" name=":filter" value="%s">' % name)
1001 if all_columns and name in columns:
1002 w('<input type="hidden" name=":columns" value="%s">' % name)
1003 if all_columns and name in group:
1004 w('<input type="hidden" name=":group" value="%s">' % name)
1006 # TODO: The widget style can go into the stylesheet
1007 w(_('<th align="left" colspan=%s>'
1008 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
1009 'customisation...</th></tr>\n')%(len(names)+1, action))
1011 if not show_customization:
1012 w('</table>\n')
1013 return
1015 w('<tr class="location-bar"><th> </th>')
1016 for name in names:
1017 w('<th>%s</th>'%name.capitalize())
1018 w('</tr>\n')
1020 # Filter
1021 if all_filters:
1022 w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
1023 for name in names:
1024 if name not in all_filters:
1025 w('<td> </td>')
1026 continue
1027 if name in filter: checked=' checked'
1028 else: checked=''
1029 w('<td align=middle>\n')
1030 w(' <input type="checkbox" name=":filter" value="%s" '
1031 '%s></td>\n'%(name, checked))
1032 w('</tr>\n')
1034 # Columns
1035 if all_columns:
1036 w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
1037 for name in names:
1038 if name not in all_columns:
1039 w('<td> </td>')
1040 continue
1041 if name in columns: checked=' checked'
1042 else: checked=''
1043 w('<td align=middle>\n')
1044 w(' <input type="checkbox" name=":columns" value="%s"'
1045 '%s></td>\n'%(name, checked))
1046 w('</tr>\n')
1048 # Grouping
1049 w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
1050 for name in names:
1051 if name not in all_columns:
1052 w('<td> </td>')
1053 continue
1054 if name in group: checked=' checked'
1055 else: checked=''
1056 w('<td align=middle>\n')
1057 w(' <input type="checkbox" name=":group" value="%s"'
1058 '%s></td>\n'%(name, checked))
1059 w('</tr>\n')
1061 w('<tr class="location-bar"><td width="1%"> </td>')
1062 w('<td colspan="%s">'%len(names))
1063 w(_('<input type="submit" name="action" value="Redisplay"></td>'))
1064 w('</tr>\n')
1065 w('</table>\n')
1067 # and the outer table
1068 w('</td></tr></table>')
1071 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
1072 l = []
1073 w = l.append
1074 for k, v in filterspec.items():
1075 k = urllib.quote(k)
1076 if type(v) == type([]):
1077 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
1078 else:
1079 w('%s=%s'%(k, urllib.quote(v)))
1080 if columns:
1081 w(':columns=%s'%','.join(map(urllib.quote, columns)))
1082 if filter:
1083 w(':filter=%s'%','.join(map(urllib.quote, filter)))
1084 if group:
1085 w(':group=%s'%','.join(map(urllib.quote, group)))
1086 m = []
1087 s_dir = ''
1088 for name in sort:
1089 dir = name[0]
1090 if dir == '-':
1091 name = name[1:]
1092 else:
1093 dir = ''
1094 if sort_name == name:
1095 if dir == '-':
1096 s_dir = ''
1097 else:
1098 s_dir = '-'
1099 else:
1100 m.append(dir+urllib.quote(name))
1101 m.insert(0, s_dir+urllib.quote(sort_name))
1102 # so things don't get completely out of hand, limit the sort to
1103 # two columns
1104 w(':sort=%s'%','.join(m[:2]))
1105 return '&'.join(l)
1108 #
1109 # ITEM TEMPLATES
1110 #
1111 class ItemTemplateReplace:
1112 '''Regular-expression based parser that turns the template into HTML.
1113 '''
1114 def __init__(self, globals, locals, cl, nodeid):
1115 self.globals = globals
1116 self.locals = locals
1117 self.cl = cl
1118 self.nodeid = nodeid
1120 replace=re.compile(
1121 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1122 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1123 def go(self, text):
1124 newtext = self.replace.sub(self, text)
1125 self.globals = self.locals = self.cl = None
1126 return newtext
1128 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1129 if m.group('name'):
1130 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1131 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1132 self.nodeid)
1133 return replace.go(m.group('text'))
1134 else:
1135 return ''
1136 if m.group('display'):
1137 command = m.group('command')
1138 return eval(command, self.globals, self.locals)
1139 return '*** unhandled match: %s'%str(m.groupdict())
1142 class ItemTemplate(TemplateFunctions):
1143 '''Templating functionality specifically for item (node) display
1144 '''
1145 def __init__(self, client, templates, classname):
1146 TemplateFunctions.__init__(self)
1147 self.client = client
1148 self.instance = client.instance
1149 self.templates = templates
1150 self.classname = classname
1152 # derived
1153 self.db = self.client.db
1154 self.cl = self.db.classes[self.classname]
1155 self.properties = self.cl.getprops()
1157 def render(self, nodeid):
1158 self.nodeid = nodeid
1160 if (self.properties.has_key('type') and
1161 self.properties.has_key('content')):
1162 pass
1163 # XXX we really want to return this as a downloadable...
1164 # currently I handle this at a higher level by detecting 'file'
1165 # designators...
1167 w = self.client.write
1168 w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1169 self.classname, nodeid))
1170 s = open(os.path.join(self.templates, self.classname+'.item')).read()
1171 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1172 w(replace.go(s))
1173 w('</form>')
1174 self.cl = self.db = self.properties = None
1177 class NewItemTemplate(TemplateFunctions):
1178 '''Templating functionality specifically for NEW item (node) display
1179 '''
1180 def __init__(self, client, templates, classname):
1181 TemplateFunctions.__init__(self)
1182 self.client = client
1183 self.instance = client.instance
1184 self.templates = templates
1185 self.classname = classname
1187 # derived
1188 self.db = self.client.db
1189 self.cl = self.db.classes[self.classname]
1190 self.properties = self.cl.getprops()
1192 def render(self, form):
1193 self.form = form
1194 w = self.client.write
1195 c = self.classname
1196 try:
1197 s = open(os.path.join(self.templates, c+'.newitem')).read()
1198 except IOError:
1199 s = open(os.path.join(self.templates, c+'.item')).read()
1200 w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1201 for key in form.keys():
1202 if key[0] == ':':
1203 value = form[key].value
1204 if type(value) != type([]): value = [value]
1205 for value in value:
1206 w('<input type="hidden" name="%s" value="%s">'%(key, value))
1207 replace = ItemTemplateReplace(self.globals, locals(), None, None)
1208 w(replace.go(s))
1209 w('</form>')
1210 self.cl = self.db = None
1212 #
1213 # $Log: not supported by cvs2svn $
1214 # Revision 1.92 2002/06/11 04:57:04 richard
1215 # Added optional additional property to display in a Multilink form menu.
1216 #
1217 # Revision 1.91 2002/05/31 00:08:02 richard
1218 # can now just display a link/multilink id - useful for stylesheet stuff
1219 #
1220 # Revision 1.90 2002/05/25 07:16:24 rochecompaan
1221 # Merged search_indexing-branch with HEAD
1222 #
1223 # Revision 1.89 2002/05/15 06:34:47 richard
1224 # forgot to fix the templating for last change
1225 #
1226 # Revision 1.88 2002/04/24 08:34:35 rochecompaan
1227 # Sorting was applied to all nodes of the MultiLink class instead of
1228 # the nodes that are actually linked to in the "field" template
1229 # function. This adds about 20+ seconds in the display of an issue if
1230 # your database has a 1000 or more issue in it.
1231 #
1232 # Revision 1.87 2002/04/03 06:12:46 richard
1233 # Fix for date properties as labels.
1234 #
1235 # Revision 1.86 2002/04/03 05:54:31 richard
1236 # Fixed serialisation problem by moving the serialisation step out of the
1237 # hyperdb.Class (get, set) into the hyperdb.Database.
1238 #
1239 # Also fixed htmltemplate after the showid changes I made yesterday.
1240 #
1241 # Unit tests for all of the above written.
1242 #
1243 # Revision 1.85 2002/04/02 01:40:58 richard
1244 # . link() htmltemplate function now has a "showid" option for links and
1245 # multilinks. When true, it only displays the linked node id as the anchor
1246 # text. The link value is displayed as a tooltip using the title anchor
1247 # attribute.
1248 #
1249 # Revision 1.84.2.2 2002/04/20 13:23:32 rochecompaan
1250 # We now have a separate search page for nodes. Search links for
1251 # different classes can be customized in instance_config similar to
1252 # index links.
1253 #
1254 # Revision 1.84.2.1 2002/04/19 19:54:42 rochecompaan
1255 # cgi_client.py
1256 # removed search link for the time being
1257 # moved rendering of matches to htmltemplate
1258 # hyperdb.py
1259 # filtering of nodes on full text search incorporated in filter method
1260 # roundupdb.py
1261 # added paramater to call of filter method
1262 # roundup_indexer.py
1263 # added search method to RoundupIndexer class
1264 #
1265 # Revision 1.84 2002/03/29 19:41:48 rochecompaan
1266 # . Fixed display of mutlilink properties when using the template
1267 # functions, menu and plain.
1268 #
1269 # Revision 1.83 2002/02/27 04:14:31 richard
1270 # Ran it through pychecker, made fixes
1271 #
1272 # Revision 1.82 2002/02/21 23:11:45 richard
1273 # . fixed some problems in date calculations (calendar.py doesn't handle over-
1274 # and under-flow). Also, hour/minute/second intervals may now be more than
1275 # 99 each.
1276 #
1277 # Revision 1.81 2002/02/21 07:21:38 richard
1278 # docco
1279 #
1280 # Revision 1.80 2002/02/21 07:19:08 richard
1281 # ... and label, width and height control for extra flavour!
1282 #
1283 # Revision 1.79 2002/02/21 06:57:38 richard
1284 # . Added popup help for classes using the classhelp html template function.
1285 # - add <display call="classhelp('priority', 'id,name,description')">
1286 # to an item page, and it generates a link to a popup window which displays
1287 # the id, name and description for the priority class. The description
1288 # field won't exist in most installations, but it will be added to the
1289 # default templates.
1290 #
1291 # Revision 1.78 2002/02/21 06:23:00 richard
1292 # *** empty log message ***
1293 #
1294 # Revision 1.77 2002/02/20 05:05:29 richard
1295 # . Added simple editing for classes that don't define a templated interface.
1296 # - access using the admin "class list" interface
1297 # - limited to admin-only
1298 # - requires the csv module from object-craft (url given if it's missing)
1299 #
1300 # Revision 1.76 2002/02/16 09:10:52 richard
1301 # oops
1302 #
1303 # Revision 1.75 2002/02/16 08:43:23 richard
1304 # . #517906 ] Attribute order in "View customisation"
1305 #
1306 # Revision 1.74 2002/02/16 08:39:42 richard
1307 # . #516854 ] "My Issues" and redisplay
1308 #
1309 # Revision 1.73 2002/02/15 07:08:44 richard
1310 # . Alternate email addresses are now available for users. See the MIGRATION
1311 # file for info on how to activate the feature.
1312 #
1313 # Revision 1.72 2002/02/14 23:39:18 richard
1314 # . All forms now have "double-submit" protection when Javascript is enabled
1315 # on the client-side.
1316 #
1317 # Revision 1.71 2002/01/23 06:15:24 richard
1318 # real (non-string, duh) sorting of lists by node id
1319 #
1320 # Revision 1.70 2002/01/23 05:47:57 richard
1321 # more HTML template cleanup and unit tests
1322 #
1323 # Revision 1.69 2002/01/23 05:10:27 richard
1324 # More HTML template cleanup and unit tests.
1325 # - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1326 # templates, but link(is_download=1) will still work for existing templates]
1327 #
1328 # Revision 1.68 2002/01/22 22:55:28 richard
1329 # . htmltemplate list() wasn't sorting...
1330 #
1331 # Revision 1.67 2002/01/22 22:46:22 richard
1332 # more htmltemplate cleanups and unit tests
1333 #
1334 # Revision 1.66 2002/01/22 06:35:40 richard
1335 # more htmltemplate tests and cleanup
1336 #
1337 # Revision 1.65 2002/01/22 00:12:06 richard
1338 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1339 # off the implementation of some of the functions so they behave sanely.
1340 #
1341 # Revision 1.64 2002/01/21 03:25:59 richard
1342 # oops
1343 #
1344 # Revision 1.63 2002/01/21 02:59:10 richard
1345 # Fixed up the HTML display of history so valid links are actually displayed.
1346 # Oh for some unit tests! :(
1347 #
1348 # Revision 1.62 2002/01/18 08:36:12 grubert
1349 # . add nowrap to history table date cell i.e. <td nowrap ...
1350 #
1351 # Revision 1.61 2002/01/17 23:04:53 richard
1352 # . much nicer history display (actualy real handling of property types etc)
1353 #
1354 # Revision 1.60 2002/01/17 08:48:19 grubert
1355 # . display superseder as html link in history.
1356 #
1357 # Revision 1.59 2002/01/17 07:58:24 grubert
1358 # . display links a html link in history.
1359 #
1360 # Revision 1.58 2002/01/15 00:50:03 richard
1361 # #502949 ] index view for non-issues and redisplay
1362 #
1363 # Revision 1.57 2002/01/14 23:31:21 richard
1364 # reverted the change that had plain() hyperlinking the link displays -
1365 # that's what link() is for!
1366 #
1367 # Revision 1.56 2002/01/14 07:04:36 richard
1368 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1369 # the linked node's page.
1370 # ... this allows a display very similar to bugzilla's where you can actually
1371 # find out information about the linked node.
1372 #
1373 # Revision 1.55 2002/01/14 06:45:03 richard
1374 # . #502953 ] nosy-like treatment of other multilinks
1375 # ... had to revert most of the previous change to the multilink field
1376 # display... not good.
1377 #
1378 # Revision 1.54 2002/01/14 05:16:51 richard
1379 # The submit buttons need a name attribute or mozilla won't submit without a
1380 # file upload. Yeah, that's bloody obscure. Grr.
1381 #
1382 # Revision 1.53 2002/01/14 04:03:32 richard
1383 # How about that ... date fields have never worked ...
1384 #
1385 # Revision 1.52 2002/01/14 02:20:14 richard
1386 # . changed all config accesses so they access either the instance or the
1387 # config attriubute on the db. This means that all config is obtained from
1388 # instance_config instead of the mish-mash of classes. This will make
1389 # switching to a ConfigParser setup easier too, I hope.
1390 #
1391 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1392 # 0.5.0 switch, I hope!)
1393 #
1394 # Revision 1.51 2002/01/10 10:02:15 grubert
1395 # In do_history: replace "." in date by " " so html wraps more sensible.
1396 # Should this be done in date's string converter ?
1397 #
1398 # Revision 1.50 2002/01/05 02:35:10 richard
1399 # I18N'ification
1400 #
1401 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1402 # Features added:
1403 # . Multilink properties are now displayed as comma separated values in
1404 # a textbox
1405 # . The add user link is now only visible to the admin user
1406 # . Modified the mail gateway to reject submissions from unknown
1407 # addresses if ANONYMOUS_ACCESS is denied
1408 #
1409 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1410 # Bugs fixed:
1411 # . Exception handling in hyperdb for strings-that-look-like numbers got
1412 # lost somewhere
1413 # . Internet Explorer submits full path for filename - we now strip away
1414 # the path
1415 # Features added:
1416 # . Link and multilink properties are now displayed sorted in the cgi
1417 # interface
1418 #
1419 # Revision 1.47 2001/11/26 22:55:56 richard
1420 # Feature:
1421 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1422 # the instance.
1423 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1424 # signature info in e-mails.
1425 # . Some more flexibility in the mail gateway and more error handling.
1426 # . Login now takes you to the page you back to the were denied access to.
1427 #
1428 # Fixed:
1429 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1430 #
1431 # Revision 1.46 2001/11/24 00:53:12 jhermann
1432 # "except:" is bad, bad , bad!
1433 #
1434 # Revision 1.45 2001/11/22 15:46:42 jhermann
1435 # Added module docstrings to all modules.
1436 #
1437 # Revision 1.44 2001/11/21 23:35:45 jhermann
1438 # Added globbing for win32, and sample marking in a 2nd file to test it
1439 #
1440 # Revision 1.43 2001/11/21 04:04:43 richard
1441 # *sigh* more missing value handling
1442 #
1443 # Revision 1.42 2001/11/21 03:40:54 richard
1444 # more new property handling
1445 #
1446 # Revision 1.41 2001/11/15 10:26:01 richard
1447 # . missing "return" in filter_section (thanks Roch'e Compaan)
1448 #
1449 # Revision 1.40 2001/11/03 01:56:51 richard
1450 # More HTML compliance fixes. This will probably fix the Netscape problem
1451 # too.
1452 #
1453 # Revision 1.39 2001/11/03 01:43:47 richard
1454 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1455 #
1456 # Revision 1.38 2001/10/31 06:58:51 richard
1457 # Added the wrap="hard" attribute to the textarea of the note field so the
1458 # messages wrap sanely.
1459 #
1460 # Revision 1.37 2001/10/31 06:24:35 richard
1461 # Added do_stext to htmltemplate, thanks Brad Clements.
1462 #
1463 # Revision 1.36 2001/10/28 22:51:38 richard
1464 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1465 #
1466 # Revision 1.35 2001/10/24 00:04:41 richard
1467 # Removed the "infinite authentication loop", thanks Roch'e
1468 #
1469 # Revision 1.34 2001/10/23 22:56:36 richard
1470 # Bugfix in filter "widget" placement, thanks Roch'e
1471 #
1472 # Revision 1.33 2001/10/23 01:00:18 richard
1473 # Re-enabled login and registration access after lopping them off via
1474 # disabling access for anonymous users.
1475 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1476 # a couple of bugs while I was there. Probably introduced a couple, but
1477 # things seem to work OK at the moment.
1478 #
1479 # Revision 1.32 2001/10/22 03:25:01 richard
1480 # Added configuration for:
1481 # . anonymous user access and registration (deny/allow)
1482 # . filter "widget" location on index page (top, bottom, both)
1483 # Updated some documentation.
1484 #
1485 # Revision 1.31 2001/10/21 07:26:35 richard
1486 # feature #473127: Filenames. I modified the file.index and htmltemplate
1487 # source so that the filename is used in the link and the creation
1488 # information is displayed.
1489 #
1490 # Revision 1.30 2001/10/21 04:44:50 richard
1491 # bug #473124: UI inconsistency with Link fields.
1492 # This also prompted me to fix a fairly long-standing usability issue -
1493 # that of being able to turn off certain filters.
1494 #
1495 # Revision 1.29 2001/10/21 00:17:56 richard
1496 # CGI interface view customisation section may now be hidden (patch from
1497 # Roch'e Compaan.)
1498 #
1499 # Revision 1.28 2001/10/21 00:00:16 richard
1500 # Fixed Checklist function - wasn't always working on a list.
1501 #
1502 # Revision 1.27 2001/10/20 12:13:44 richard
1503 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1504 #
1505 # Revision 1.26 2001/10/14 10:55:00 richard
1506 # Handle empty strings in HTML template Link function
1507 #
1508 # Revision 1.25 2001/10/09 07:25:59 richard
1509 # Added the Password property type. See "pydoc roundup.password" for
1510 # implementation details. Have updated some of the documentation too.
1511 #
1512 # Revision 1.24 2001/09/27 06:45:58 richard
1513 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1514 # on the plain() template function to escape the text for HTML.
1515 #
1516 # Revision 1.23 2001/09/10 09:47:18 richard
1517 # Fixed bug in the generation of links to Link/Multilink in indexes.
1518 # (thanks Hubert Hoegl)
1519 # Added AssignedTo to the "classic" schema's item page.
1520 #
1521 # Revision 1.22 2001/08/30 06:01:17 richard
1522 # Fixed missing import in mailgw :(
1523 #
1524 # Revision 1.21 2001/08/16 07:34:59 richard
1525 # better CGI text searching - but hidden filter fields are disappearing...
1526 #
1527 # Revision 1.20 2001/08/15 23:43:18 richard
1528 # Fixed some isFooTypes that I missed.
1529 # Refactored some code in the CGI code.
1530 #
1531 # Revision 1.19 2001/08/12 06:32:36 richard
1532 # using isinstance(blah, Foo) now instead of isFooType
1533 #
1534 # Revision 1.18 2001/08/07 00:24:42 richard
1535 # stupid typo
1536 #
1537 # Revision 1.17 2001/08/07 00:15:51 richard
1538 # Added the copyright/license notice to (nearly) all files at request of
1539 # Bizar Software.
1540 #
1541 # Revision 1.16 2001/08/01 03:52:23 richard
1542 # Checklist was using wrong name.
1543 #
1544 # Revision 1.15 2001/07/30 08:12:17 richard
1545 # Added time logging and file uploading to the templates.
1546 #
1547 # Revision 1.14 2001/07/30 06:17:45 richard
1548 # Features:
1549 # . Added ability for cgi newblah forms to indicate that the new node
1550 # should be linked somewhere.
1551 # Fixed:
1552 # . Fixed the agument handling for the roundup-admin find command.
1553 # . Fixed handling of summary when no note supplied for newblah. Again.
1554 # . Fixed detection of no form in htmltemplate Field display.
1555 #
1556 # Revision 1.13 2001/07/30 02:37:53 richard
1557 # Temporary measure until we have decent schema migration.
1558 #
1559 # Revision 1.12 2001/07/30 01:24:33 richard
1560 # Handles new node display now.
1561 #
1562 # Revision 1.11 2001/07/29 09:31:35 richard
1563 # oops
1564 #
1565 # Revision 1.10 2001/07/29 09:28:23 richard
1566 # Fixed sorting by clicking on column headings.
1567 #
1568 # Revision 1.9 2001/07/29 08:27:40 richard
1569 # Fixed handling of passed-in values in form elements (ie. during a
1570 # drill-down)
1571 #
1572 # Revision 1.8 2001/07/29 07:01:39 richard
1573 # Added vim command to all source so that we don't get no steenkin' tabs :)
1574 #
1575 # Revision 1.7 2001/07/29 05:36:14 richard
1576 # Cleanup of the link label generation.
1577 #
1578 # Revision 1.6 2001/07/29 04:06:42 richard
1579 # Fixed problem in link display when Link value is None.
1580 #
1581 # Revision 1.5 2001/07/28 08:17:09 richard
1582 # fixed use of stylesheet
1583 #
1584 # Revision 1.4 2001/07/28 07:59:53 richard
1585 # Replaced errno integers with their module values.
1586 # De-tabbed templatebuilder.py
1587 #
1588 # Revision 1.3 2001/07/25 03:39:47 richard
1589 # Hrm - displaying links to classes that don't specify a key property. I've
1590 # got it defaulting to 'name', then 'title' and then a "random" property (first
1591 # one returned by getprops().keys().
1592 # Needs to be moved onto the Class I think...
1593 #
1594 # Revision 1.2 2001/07/22 12:09:32 richard
1595 # Final commit of Grande Splite
1596 #
1597 # Revision 1.1 2001/07/22 11:58:35 richard
1598 # More Grande Splite
1599 #
1600 #
1601 # vim: set filetype=python ts=4 sw=4 et si