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