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