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