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