2dc9af7ea6d11aa3e146ba92d8811216d80d2bb1
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.103 2002-07-20 19:29:10 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 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 and not self.client.user in (None, "anonymous")):
1201 w(' <tr class="location-bar">\n')
1202 w(' <td colspan=7><hr></td>\n')
1203 w(' </tr>\n')
1204 w(' <tr class="location-bar">\n')
1205 w(' <td align=right class="form-label">Name</td>\n')
1206 w(' <td colspan=2 class="form-text"><input type="text" name=":name" value=""></td>\n')
1207 w(' <td colspan=4 rowspan=2 class="form-help">If you give the query a name '
1208 'and click <b>Save</b>, it will appear on your menu. Saved queries may be '
1209 'edited by going to <b>My Details</b> and clicking on the query name.</td>')
1210 w(' </tr>\n')
1211 w(' <tr class="location-bar">\n')
1212 w(' <td> </td><input type="hidden" name=":classname" value="%s">\n' % self.classname)
1213 w(' <td colspan=2><input type="submit" name="Query" value="Save"></td>\n')
1214 w(' </tr>\n')
1215 w('</table>\n')
1217 def sortby(self, sort_name, filterspec, columns, filter, group, sort, pagesize, startwith):
1218 l = []
1219 w = l.append
1220 for k, v in filterspec.items():
1221 k = urllib.quote(k)
1222 if type(v) == type([]):
1223 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
1224 else:
1225 w('%s=%s'%(k, urllib.quote(v)))
1226 if columns:
1227 w(':columns=%s'%','.join(map(urllib.quote, columns)))
1228 if filter:
1229 w(':filter=%s'%','.join(map(urllib.quote, filter)))
1230 if group:
1231 w(':group=%s'%','.join(map(urllib.quote, group)))
1232 w(':pagesize=%s' % pagesize)
1233 w(':startwith=%s' % startwith)
1234 m = []
1235 s_dir = ''
1236 for name in sort:
1237 dir = name[0]
1238 if dir == '-':
1239 name = name[1:]
1240 else:
1241 dir = ''
1242 if sort_name == name:
1243 if dir == '-':
1244 s_dir = ''
1245 else:
1246 s_dir = '-'
1247 else:
1248 m.append(dir+urllib.quote(name))
1249 m.insert(0, s_dir+urllib.quote(sort_name))
1250 # so things don't get completely out of hand, limit the sort to
1251 # two columns
1252 w(':sort=%s'%','.join(m[:2]))
1253 return '&'.join(l)
1255 #
1256 # ITEM TEMPLATES
1257 #
1258 class ItemTemplateReplace:
1259 '''Regular-expression based parser that turns the template into HTML.
1260 '''
1261 def __init__(self, globals, locals, cl, nodeid):
1262 self.globals = globals
1263 self.locals = locals
1264 self.cl = cl
1265 self.nodeid = nodeid
1267 replace=re.compile(
1268 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1269 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1270 def go(self, text):
1271 newtext = self.replace.sub(self, text)
1272 self.globals = self.locals = self.cl = None
1273 return newtext
1275 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1276 if m.group('name'):
1277 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1278 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1279 self.nodeid)
1280 return replace.go(m.group('text'))
1281 else:
1282 return ''
1283 if m.group('display'):
1284 command = m.group('command')
1285 return eval(command, self.globals, self.locals)
1286 return '*** unhandled match: %s'%str(m.groupdict())
1289 class ItemTemplate(TemplateFunctions):
1290 '''Templating functionality specifically for item (node) display
1291 '''
1292 def __init__(self, client, templates, classname):
1293 TemplateFunctions.__init__(self)
1294 self.client = client
1295 self.instance = client.instance
1296 self.templates = templates
1297 self.classname = classname
1299 # derived
1300 self.db = self.client.db
1301 self.cl = self.db.classes[self.classname]
1302 self.properties = self.cl.getprops()
1304 def clear(self):
1305 self.db = self.cl = self.properties = None
1306 TemplateFunctions.clear(self)
1308 def render(self, nodeid):
1309 self.nodeid = nodeid
1311 if (self.properties.has_key('type') and
1312 self.properties.has_key('content')):
1313 pass
1314 # XXX we really want to return this as a downloadable...
1315 # currently I handle this at a higher level by detecting 'file'
1316 # designators...
1318 w = self.client.write
1319 w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1320 self.classname, nodeid))
1321 s = open(os.path.join(self.templates, self.classname+'.item')).read()
1322 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1323 w(replace.go(s))
1324 w('</form>')
1326 self.clear()
1329 class NewItemTemplate(TemplateFunctions):
1330 '''Templating functionality specifically for NEW item (node) display
1331 '''
1332 def __init__(self, client, templates, classname):
1333 TemplateFunctions.__init__(self)
1334 self.client = client
1335 self.instance = client.instance
1336 self.templates = templates
1337 self.classname = classname
1339 # derived
1340 self.db = self.client.db
1341 self.cl = self.db.classes[self.classname]
1342 self.properties = self.cl.getprops()
1344 def clear(self):
1345 self.db = self.cl = None
1346 TemplateFunctions.clear(self)
1348 def render(self, form):
1349 self.form = form
1350 w = self.client.write
1351 c = self.classname
1352 try:
1353 s = open(os.path.join(self.templates, c+'.newitem')).read()
1354 except IOError:
1355 s = open(os.path.join(self.templates, c+'.item')).read()
1356 w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1357 for key in form.keys():
1358 if key[0] == ':':
1359 value = form[key].value
1360 if type(value) != type([]): value = [value]
1361 for value in value:
1362 w('<input type="hidden" name="%s" value="%s">'%(key, value))
1363 replace = ItemTemplateReplace(self.globals, locals(), None, None)
1364 w(replace.go(s))
1365 w('</form>')
1367 self.clear()
1369 #
1370 # $Log: not supported by cvs2svn $
1371 # Revision 1.102 2002/07/18 23:07:08 richard
1372 # Unit tests and a few fixes.
1373 #
1374 # Revision 1.101 2002/07/18 11:17:30 gmcm
1375 # Add Number and Boolean types to hyperdb.
1376 # Add conversion cases to web, mail & admin interfaces.
1377 # Add storage/serialization cases to back_anydbm & back_metakit.
1378 #
1379 # Revision 1.100 2002/07/18 07:01:54 richard
1380 # minor bugfix
1381 #
1382 # Revision 1.99 2002/07/17 12:39:10 gmcm
1383 # Saving, running & editing queries.
1384 #
1385 # Revision 1.98 2002/07/10 00:17:46 richard
1386 # . added sorting of checklist HTML display
1387 #
1388 # Revision 1.97 2002/07/09 05:20:09 richard
1389 # . added email display function - mangles email addrs so they're not so easily
1390 # scraped from the web
1391 #
1392 # Revision 1.96 2002/07/09 04:19:09 richard
1393 # Added reindex command to roundup-admin.
1394 # Fixed reindex on first access.
1395 # Also fixed reindexing of entries that change.
1396 #
1397 # Revision 1.95 2002/07/08 15:32:06 gmcm
1398 # Pagination of index pages.
1399 # New search form.
1400 #
1401 # Revision 1.94 2002/06/27 15:38:53 gmcm
1402 # Fix the cycles (a clear method, called after render, that removes
1403 # the bound methods from the globals dict).
1404 # Use cl.filter instead of cl.list followed by sortfunc. For some
1405 # backends (Metakit), filter can sort at C speeds, cutting >10 secs
1406 # off of filling in the <select...> box for assigned_to when you
1407 # have 600+ users.
1408 #
1409 # Revision 1.93 2002/06/27 12:05:25 gmcm
1410 # Default labelprops to id.
1411 # In history, make sure there's a .item before making a link / multilink into an href.
1412 # Also in history, cgi.escape String properties.
1413 # Clean up some of the reference cycles.
1414 #
1415 # Revision 1.92 2002/06/11 04:57:04 richard
1416 # Added optional additional property to display in a Multilink form menu.
1417 #
1418 # Revision 1.91 2002/05/31 00:08:02 richard
1419 # can now just display a link/multilink id - useful for stylesheet stuff
1420 #
1421 # Revision 1.90 2002/05/25 07:16:24 rochecompaan
1422 # Merged search_indexing-branch with HEAD
1423 #
1424 # Revision 1.89 2002/05/15 06:34:47 richard
1425 # forgot to fix the templating for last change
1426 #
1427 # Revision 1.88 2002/04/24 08:34:35 rochecompaan
1428 # Sorting was applied to all nodes of the MultiLink class instead of
1429 # the nodes that are actually linked to in the "field" template
1430 # function. This adds about 20+ seconds in the display of an issue if
1431 # your database has a 1000 or more issue in it.
1432 #
1433 # Revision 1.87 2002/04/03 06:12:46 richard
1434 # Fix for date properties as labels.
1435 #
1436 # Revision 1.86 2002/04/03 05:54:31 richard
1437 # Fixed serialisation problem by moving the serialisation step out of the
1438 # hyperdb.Class (get, set) into the hyperdb.Database.
1439 #
1440 # Also fixed htmltemplate after the showid changes I made yesterday.
1441 #
1442 # Unit tests for all of the above written.
1443 #
1444 # Revision 1.85 2002/04/02 01:40:58 richard
1445 # . link() htmltemplate function now has a "showid" option for links and
1446 # multilinks. When true, it only displays the linked node id as the anchor
1447 # text. The link value is displayed as a tooltip using the title anchor
1448 # attribute.
1449 #
1450 # Revision 1.84.2.2 2002/04/20 13:23:32 rochecompaan
1451 # We now have a separate search page for nodes. Search links for
1452 # different classes can be customized in instance_config similar to
1453 # index links.
1454 #
1455 # Revision 1.84.2.1 2002/04/19 19:54:42 rochecompaan
1456 # cgi_client.py
1457 # removed search link for the time being
1458 # moved rendering of matches to htmltemplate
1459 # hyperdb.py
1460 # filtering of nodes on full text search incorporated in filter method
1461 # roundupdb.py
1462 # added paramater to call of filter method
1463 # roundup_indexer.py
1464 # added search method to RoundupIndexer class
1465 #
1466 # Revision 1.84 2002/03/29 19:41:48 rochecompaan
1467 # . Fixed display of mutlilink properties when using the template
1468 # functions, menu and plain.
1469 #
1470 # Revision 1.83 2002/02/27 04:14:31 richard
1471 # Ran it through pychecker, made fixes
1472 #
1473 # Revision 1.82 2002/02/21 23:11:45 richard
1474 # . fixed some problems in date calculations (calendar.py doesn't handle over-
1475 # and under-flow). Also, hour/minute/second intervals may now be more than
1476 # 99 each.
1477 #
1478 # Revision 1.81 2002/02/21 07:21:38 richard
1479 # docco
1480 #
1481 # Revision 1.80 2002/02/21 07:19:08 richard
1482 # ... and label, width and height control for extra flavour!
1483 #
1484 # Revision 1.79 2002/02/21 06:57:38 richard
1485 # . Added popup help for classes using the classhelp html template function.
1486 # - add <display call="classhelp('priority', 'id,name,description')">
1487 # to an item page, and it generates a link to a popup window which displays
1488 # the id, name and description for the priority class. The description
1489 # field won't exist in most installations, but it will be added to the
1490 # default templates.
1491 #
1492 # Revision 1.78 2002/02/21 06:23:00 richard
1493 # *** empty log message ***
1494 #
1495 # Revision 1.77 2002/02/20 05:05:29 richard
1496 # . Added simple editing for classes that don't define a templated interface.
1497 # - access using the admin "class list" interface
1498 # - limited to admin-only
1499 # - requires the csv module from object-craft (url given if it's missing)
1500 #
1501 # Revision 1.76 2002/02/16 09:10:52 richard
1502 # oops
1503 #
1504 # Revision 1.75 2002/02/16 08:43:23 richard
1505 # . #517906 ] Attribute order in "View customisation"
1506 #
1507 # Revision 1.74 2002/02/16 08:39:42 richard
1508 # . #516854 ] "My Issues" and redisplay
1509 #
1510 # Revision 1.73 2002/02/15 07:08:44 richard
1511 # . Alternate email addresses are now available for users. See the MIGRATION
1512 # file for info on how to activate the feature.
1513 #
1514 # Revision 1.72 2002/02/14 23:39:18 richard
1515 # . All forms now have "double-submit" protection when Javascript is enabled
1516 # on the client-side.
1517 #
1518 # Revision 1.71 2002/01/23 06:15:24 richard
1519 # real (non-string, duh) sorting of lists by node id
1520 #
1521 # Revision 1.70 2002/01/23 05:47:57 richard
1522 # more HTML template cleanup and unit tests
1523 #
1524 # Revision 1.69 2002/01/23 05:10:27 richard
1525 # More HTML template cleanup and unit tests.
1526 # - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1527 # templates, but link(is_download=1) will still work for existing templates]
1528 #
1529 # Revision 1.68 2002/01/22 22:55:28 richard
1530 # . htmltemplate list() wasn't sorting...
1531 #
1532 # Revision 1.67 2002/01/22 22:46:22 richard
1533 # more htmltemplate cleanups and unit tests
1534 #
1535 # Revision 1.66 2002/01/22 06:35:40 richard
1536 # more htmltemplate tests and cleanup
1537 #
1538 # Revision 1.65 2002/01/22 00:12:06 richard
1539 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1540 # off the implementation of some of the functions so they behave sanely.
1541 #
1542 # Revision 1.64 2002/01/21 03:25:59 richard
1543 # oops
1544 #
1545 # Revision 1.63 2002/01/21 02:59:10 richard
1546 # Fixed up the HTML display of history so valid links are actually displayed.
1547 # Oh for some unit tests! :(
1548 #
1549 # Revision 1.62 2002/01/18 08:36:12 grubert
1550 # . add nowrap to history table date cell i.e. <td nowrap ...
1551 #
1552 # Revision 1.61 2002/01/17 23:04:53 richard
1553 # . much nicer history display (actualy real handling of property types etc)
1554 #
1555 # Revision 1.60 2002/01/17 08:48:19 grubert
1556 # . display superseder as html link in history.
1557 #
1558 # Revision 1.59 2002/01/17 07:58:24 grubert
1559 # . display links a html link in history.
1560 #
1561 # Revision 1.58 2002/01/15 00:50:03 richard
1562 # #502949 ] index view for non-issues and redisplay
1563 #
1564 # Revision 1.57 2002/01/14 23:31:21 richard
1565 # reverted the change that had plain() hyperlinking the link displays -
1566 # that's what link() is for!
1567 #
1568 # Revision 1.56 2002/01/14 07:04:36 richard
1569 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1570 # the linked node's page.
1571 # ... this allows a display very similar to bugzilla's where you can actually
1572 # find out information about the linked node.
1573 #
1574 # Revision 1.55 2002/01/14 06:45:03 richard
1575 # . #502953 ] nosy-like treatment of other multilinks
1576 # ... had to revert most of the previous change to the multilink field
1577 # display... not good.
1578 #
1579 # Revision 1.54 2002/01/14 05:16:51 richard
1580 # The submit buttons need a name attribute or mozilla won't submit without a
1581 # file upload. Yeah, that's bloody obscure. Grr.
1582 #
1583 # Revision 1.53 2002/01/14 04:03:32 richard
1584 # How about that ... date fields have never worked ...
1585 #
1586 # Revision 1.52 2002/01/14 02:20:14 richard
1587 # . changed all config accesses so they access either the instance or the
1588 # config attriubute on the db. This means that all config is obtained from
1589 # instance_config instead of the mish-mash of classes. This will make
1590 # switching to a ConfigParser setup easier too, I hope.
1591 #
1592 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1593 # 0.5.0 switch, I hope!)
1594 #
1595 # Revision 1.51 2002/01/10 10:02:15 grubert
1596 # In do_history: replace "." in date by " " so html wraps more sensible.
1597 # Should this be done in date's string converter ?
1598 #
1599 # Revision 1.50 2002/01/05 02:35:10 richard
1600 # I18N'ification
1601 #
1602 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1603 # Features added:
1604 # . Multilink properties are now displayed as comma separated values in
1605 # a textbox
1606 # . The add user link is now only visible to the admin user
1607 # . Modified the mail gateway to reject submissions from unknown
1608 # addresses if ANONYMOUS_ACCESS is denied
1609 #
1610 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1611 # Bugs fixed:
1612 # . Exception handling in hyperdb for strings-that-look-like numbers got
1613 # lost somewhere
1614 # . Internet Explorer submits full path for filename - we now strip away
1615 # the path
1616 # Features added:
1617 # . Link and multilink properties are now displayed sorted in the cgi
1618 # interface
1619 #
1620 # Revision 1.47 2001/11/26 22:55:56 richard
1621 # Feature:
1622 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1623 # the instance.
1624 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1625 # signature info in e-mails.
1626 # . Some more flexibility in the mail gateway and more error handling.
1627 # . Login now takes you to the page you back to the were denied access to.
1628 #
1629 # Fixed:
1630 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1631 #
1632 # Revision 1.46 2001/11/24 00:53:12 jhermann
1633 # "except:" is bad, bad , bad!
1634 #
1635 # Revision 1.45 2001/11/22 15:46:42 jhermann
1636 # Added module docstrings to all modules.
1637 #
1638 # Revision 1.44 2001/11/21 23:35:45 jhermann
1639 # Added globbing for win32, and sample marking in a 2nd file to test it
1640 #
1641 # Revision 1.43 2001/11/21 04:04:43 richard
1642 # *sigh* more missing value handling
1643 #
1644 # Revision 1.42 2001/11/21 03:40:54 richard
1645 # more new property handling
1646 #
1647 # Revision 1.41 2001/11/15 10:26:01 richard
1648 # . missing "return" in filter_section (thanks Roch'e Compaan)
1649 #
1650 # Revision 1.40 2001/11/03 01:56:51 richard
1651 # More HTML compliance fixes. This will probably fix the Netscape problem
1652 # too.
1653 #
1654 # Revision 1.39 2001/11/03 01:43:47 richard
1655 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1656 #
1657 # Revision 1.38 2001/10/31 06:58:51 richard
1658 # Added the wrap="hard" attribute to the textarea of the note field so the
1659 # messages wrap sanely.
1660 #
1661 # Revision 1.37 2001/10/31 06:24:35 richard
1662 # Added do_stext to htmltemplate, thanks Brad Clements.
1663 #
1664 # Revision 1.36 2001/10/28 22:51:38 richard
1665 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1666 #
1667 # Revision 1.35 2001/10/24 00:04:41 richard
1668 # Removed the "infinite authentication loop", thanks Roch'e
1669 #
1670 # Revision 1.34 2001/10/23 22:56:36 richard
1671 # Bugfix in filter "widget" placement, thanks Roch'e
1672 #
1673 # Revision 1.33 2001/10/23 01:00:18 richard
1674 # Re-enabled login and registration access after lopping them off via
1675 # disabling access for anonymous users.
1676 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1677 # a couple of bugs while I was there. Probably introduced a couple, but
1678 # things seem to work OK at the moment.
1679 #
1680 # Revision 1.32 2001/10/22 03:25:01 richard
1681 # Added configuration for:
1682 # . anonymous user access and registration (deny/allow)
1683 # . filter "widget" location on index page (top, bottom, both)
1684 # Updated some documentation.
1685 #
1686 # Revision 1.31 2001/10/21 07:26:35 richard
1687 # feature #473127: Filenames. I modified the file.index and htmltemplate
1688 # source so that the filename is used in the link and the creation
1689 # information is displayed.
1690 #
1691 # Revision 1.30 2001/10/21 04:44:50 richard
1692 # bug #473124: UI inconsistency with Link fields.
1693 # This also prompted me to fix a fairly long-standing usability issue -
1694 # that of being able to turn off certain filters.
1695 #
1696 # Revision 1.29 2001/10/21 00:17:56 richard
1697 # CGI interface view customisation section may now be hidden (patch from
1698 # Roch'e Compaan.)
1699 #
1700 # Revision 1.28 2001/10/21 00:00:16 richard
1701 # Fixed Checklist function - wasn't always working on a list.
1702 #
1703 # Revision 1.27 2001/10/20 12:13:44 richard
1704 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1705 #
1706 # Revision 1.26 2001/10/14 10:55:00 richard
1707 # Handle empty strings in HTML template Link function
1708 #
1709 # Revision 1.25 2001/10/09 07:25:59 richard
1710 # Added the Password property type. See "pydoc roundup.password" for
1711 # implementation details. Have updated some of the documentation too.
1712 #
1713 # Revision 1.24 2001/09/27 06:45:58 richard
1714 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1715 # on the plain() template function to escape the text for HTML.
1716 #
1717 # Revision 1.23 2001/09/10 09:47:18 richard
1718 # Fixed bug in the generation of links to Link/Multilink in indexes.
1719 # (thanks Hubert Hoegl)
1720 # Added AssignedTo to the "classic" schema's item page.
1721 #
1722 # Revision 1.22 2001/08/30 06:01:17 richard
1723 # Fixed missing import in mailgw :(
1724 #
1725 # Revision 1.21 2001/08/16 07:34:59 richard
1726 # better CGI text searching - but hidden filter fields are disappearing...
1727 #
1728 # Revision 1.20 2001/08/15 23:43:18 richard
1729 # Fixed some isFooTypes that I missed.
1730 # Refactored some code in the CGI code.
1731 #
1732 # Revision 1.19 2001/08/12 06:32:36 richard
1733 # using isinstance(blah, Foo) now instead of isFooType
1734 #
1735 # Revision 1.18 2001/08/07 00:24:42 richard
1736 # stupid typo
1737 #
1738 # Revision 1.17 2001/08/07 00:15:51 richard
1739 # Added the copyright/license notice to (nearly) all files at request of
1740 # Bizar Software.
1741 #
1742 # Revision 1.16 2001/08/01 03:52:23 richard
1743 # Checklist was using wrong name.
1744 #
1745 # Revision 1.15 2001/07/30 08:12:17 richard
1746 # Added time logging and file uploading to the templates.
1747 #
1748 # Revision 1.14 2001/07/30 06:17:45 richard
1749 # Features:
1750 # . Added ability for cgi newblah forms to indicate that the new node
1751 # should be linked somewhere.
1752 # Fixed:
1753 # . Fixed the agument handling for the roundup-admin find command.
1754 # . Fixed handling of summary when no note supplied for newblah. Again.
1755 # . Fixed detection of no form in htmltemplate Field display.
1756 #
1757 # Revision 1.13 2001/07/30 02:37:53 richard
1758 # Temporary measure until we have decent schema migration.
1759 #
1760 # Revision 1.12 2001/07/30 01:24:33 richard
1761 # Handles new node display now.
1762 #
1763 # Revision 1.11 2001/07/29 09:31:35 richard
1764 # oops
1765 #
1766 # Revision 1.10 2001/07/29 09:28:23 richard
1767 # Fixed sorting by clicking on column headings.
1768 #
1769 # Revision 1.9 2001/07/29 08:27:40 richard
1770 # Fixed handling of passed-in values in form elements (ie. during a
1771 # drill-down)
1772 #
1773 # Revision 1.8 2001/07/29 07:01:39 richard
1774 # Added vim command to all source so that we don't get no steenkin' tabs :)
1775 #
1776 # Revision 1.7 2001/07/29 05:36:14 richard
1777 # Cleanup of the link label generation.
1778 #
1779 # Revision 1.6 2001/07/29 04:06:42 richard
1780 # Fixed problem in link display when Link value is None.
1781 #
1782 # Revision 1.5 2001/07/28 08:17:09 richard
1783 # fixed use of stylesheet
1784 #
1785 # Revision 1.4 2001/07/28 07:59:53 richard
1786 # Replaced errno integers with their module values.
1787 # De-tabbed templatebuilder.py
1788 #
1789 # Revision 1.3 2001/07/25 03:39:47 richard
1790 # Hrm - displaying links to classes that don't specify a key property. I've
1791 # got it defaulting to 'name', then 'title' and then a "random" property (first
1792 # one returned by getprops().keys().
1793 # Needs to be moved onto the Class I think...
1794 #
1795 # Revision 1.2 2001/07/22 12:09:32 richard
1796 # Final commit of Grande Splite
1797 #
1798 # Revision 1.1 2001/07/22 11:58:35 richard
1799 # More Grande Splite
1800 #
1801 #
1802 # vim: set filetype=python ts=4 sw=4 et si