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