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