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