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.71 2002-01-23 06:15:24 richard Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno
26 import hyperdb, date, password
27 from i18n import _
29 # This imports the StructureText functionality for the do_stext function
30 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
31 try:
32 from StructuredText.StructuredText import HTML as StructuredText
33 except ImportError:
34 StructuredText = None
36 class TemplateFunctions:
37 def __init__(self):
38 self.form = None
39 self.nodeid = None
40 self.filterspec = None
41 self.globals = {}
42 for key in TemplateFunctions.__dict__.keys():
43 if key[:3] == 'do_':
44 self.globals[key[3:]] = getattr(self, key)
46 def do_plain(self, property, escape=0):
47 ''' display a String property directly;
49 display a Date property in a specified time zone with an option to
50 omit the time from the date stamp;
52 for a Link or Multilink property, display the key strings of the
53 linked nodes (or the ids if the linked class has no key property)
54 '''
55 if not self.nodeid and self.form is None:
56 return _('[Field: not called from item]')
57 propclass = self.properties[property]
58 if self.nodeid:
59 # make sure the property is a valid one
60 # TODO: this tests, but we should handle the exception
61 prop_test = self.cl.getprops()[property]
63 # get the value for this property
64 try:
65 value = self.cl.get(self.nodeid, property)
66 except KeyError:
67 # a KeyError here means that the node doesn't have a value
68 # for the specified property
69 if isinstance(propclass, hyperdb.Multilink): value = []
70 else: value = ''
71 else:
72 # TODO: pull the value from the form
73 if isinstance(propclass, hyperdb.Multilink): value = []
74 else: value = ''
75 if isinstance(propclass, hyperdb.String):
76 if value is None: value = ''
77 else: value = str(value)
78 elif isinstance(propclass, hyperdb.Password):
79 if value is None: value = ''
80 else: value = _('*encrypted*')
81 elif isinstance(propclass, hyperdb.Date):
82 # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
83 value = str(value)
84 elif isinstance(propclass, hyperdb.Interval):
85 value = str(value)
86 elif isinstance(propclass, hyperdb.Link):
87 linkcl = self.db.classes[propclass.classname]
88 k = linkcl.labelprop()
89 if value:
90 value = linkcl.get(value, k)
91 else:
92 value = _('[unselected]')
93 elif isinstance(propclass, hyperdb.Multilink):
94 linkcl = self.db.classes[propclass.classname]
95 k = linkcl.labelprop()
96 value = ', '.join(value)
97 else:
98 s = _('Plain: bad propclass "%(propclass)s"')%locals()
99 if escape:
100 value = cgi.escape(value)
101 return value
103 def do_stext(self, property, escape=0):
104 '''Render as structured text using the StructuredText module
105 (see above for details)
106 '''
107 s = self.do_plain(property, escape=escape)
108 if not StructuredText:
109 return s
110 return StructuredText(s,level=1,header=0)
112 def determine_value(self, property):
113 '''determine the value of a property using the node, form or
114 filterspec
115 '''
116 propclass = self.properties[property]
117 if self.nodeid:
118 value = self.cl.get(self.nodeid, property, None)
119 if isinstance(propclass, hyperdb.Multilink) and value is None:
120 return []
121 return value
122 elif self.filterspec is not None:
123 if isinstance(propclass, hyperdb.Multilink):
124 return self.filterspec.get(property, [])
125 else:
126 return self.filterspec.get(property, '')
127 # TODO: pull the value from the form
128 if isinstance(propclass, hyperdb.Multilink):
129 return []
130 else:
131 return ''
133 def make_sort_function(self, classname):
134 '''Make a sort function for a given class
135 '''
136 linkcl = self.db.classes[classname]
137 if linkcl.getprops().has_key('order'):
138 sort_on = 'order'
139 else:
140 sort_on = linkcl.labelprop()
141 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
142 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
143 return sortfunc
145 def do_field(self, property, size=None, showid=0):
146 ''' display a property like the plain displayer, but in a text field
147 to be edited
149 Note: if you would prefer an option list style display for
150 link or multilink editing, use menu().
151 '''
152 if not self.nodeid and self.form is None and self.filterspec is None:
153 return _('[Field: not called from item]')
155 if size is None:
156 size = 30
158 propclass = self.properties[property]
160 # get the value
161 value = self.determine_value(property)
163 # now display
164 if (isinstance(propclass, hyperdb.String) or
165 isinstance(propclass, hyperdb.Date) or
166 isinstance(propclass, hyperdb.Interval)):
167 if value is None:
168 value = ''
169 else:
170 value = cgi.escape(str(value))
171 value = '"'.join(value.split('"'))
172 s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
173 elif isinstance(propclass, hyperdb.Password):
174 s = '<input type="password" name="%s" size="%s">'%(property, size)
175 elif isinstance(propclass, hyperdb.Link):
176 sortfunc = self.make_sort_function(propclass.classname)
177 linkcl = self.db.classes[propclass.classname]
178 options = linkcl.list()
179 options.sort(sortfunc)
180 # TODO: make this a field display, not a menu one!
181 l = ['<select name="%s">'%property]
182 k = linkcl.labelprop()
183 if value is None:
184 s = 'selected '
185 else:
186 s = ''
187 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
188 for optionid in options:
189 option = linkcl.get(optionid, k)
190 s = ''
191 if optionid == value:
192 s = 'selected '
193 if showid:
194 lab = '%s%s: %s'%(propclass.classname, optionid, option)
195 else:
196 lab = option
197 if size is not None and len(lab) > size:
198 lab = lab[:size-3] + '...'
199 lab = cgi.escape(lab)
200 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
201 l.append('</select>')
202 s = '\n'.join(l)
203 elif isinstance(propclass, hyperdb.Multilink):
204 sortfunc = self.make_sort_function(propclass.classname)
205 linkcl = self.db.classes[propclass.classname]
206 list = linkcl.list()
207 list.sort(sortfunc)
208 l = []
209 # map the id to the label property
210 if not showid:
211 k = linkcl.labelprop()
212 value = [linkcl.get(v, k) for v in value]
213 value = cgi.escape(','.join(value))
214 s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
215 else:
216 s = _('Plain: bad propclass "%(propclass)s"')%locals()
217 return s
219 def do_menu(self, property, size=None, height=None, showid=0):
220 ''' for a Link property, display a menu of the available choices
221 '''
222 if not self.nodeid and self.form is None and self.filterspec is None:
223 return _('[Field: not called from item]')
225 propclass = self.properties[property]
227 # make sure this is a link property
228 if not (isinstance(propclass, hyperdb.Link) or
229 isinstance(propclass, hyperdb.Multilink)):
230 return _('[Menu: not a link]')
232 # sort function
233 sortfunc = self.make_sort_function(propclass.classname)
235 # get the value
236 value = self.determine_value(property)
238 # display
239 if isinstance(propclass, hyperdb.Link):
240 linkcl = self.db.classes[propclass.classname]
241 l = ['<select name="%s">'%property]
242 k = linkcl.labelprop()
243 s = ''
244 if value is None:
245 s = 'selected '
246 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
247 options = linkcl.list()
248 options.sort(sortfunc)
249 for optionid in options:
250 option = linkcl.get(optionid, k)
251 s = ''
252 if optionid == value:
253 s = 'selected '
254 if showid:
255 lab = '%s%s: %s'%(propclass.classname, optionid, option)
256 else:
257 lab = option
258 if size is not None and len(lab) > size:
259 lab = lab[:size-3] + '...'
260 lab = cgi.escape(lab)
261 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
262 l.append('</select>')
263 return '\n'.join(l)
264 if isinstance(propclass, hyperdb.Multilink):
265 linkcl = self.db.classes[propclass.classname]
266 options = linkcl.list()
267 options.sort(sortfunc)
268 height = height or min(len(options), 7)
269 l = ['<select multiple name="%s" size="%s">'%(property, height)]
270 k = linkcl.labelprop()
271 for optionid in options:
272 option = linkcl.get(optionid, k)
273 s = ''
274 if optionid in value:
275 s = 'selected '
276 if showid:
277 lab = '%s%s: %s'%(propclass.classname, optionid, option)
278 else:
279 lab = option
280 if size is not None and len(lab) > size:
281 lab = lab[:size-3] + '...'
282 lab = cgi.escape(lab)
283 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
284 lab))
285 l.append('</select>')
286 return '\n'.join(l)
287 return _('[Menu: not a link]')
289 #XXX deviates from spec
290 def do_link(self, property=None, is_download=0):
291 '''For a Link or Multilink property, display the names of the linked
292 nodes, hyperlinked to the item views on those nodes.
293 For other properties, link to this node with the property as the
294 text.
296 If is_download is true, append the property value to the generated
297 URL so that the link may be used as a download link and the
298 downloaded file name is correct.
299 '''
300 if not self.nodeid and self.form is None:
301 return _('[Link: not called from item]')
303 # get the value
304 value = self.determine_value(property)
305 if not value:
306 return _('[no %(propname)s]')%{'propname':property.capitalize()}
308 propclass = self.properties[property]
309 if isinstance(propclass, hyperdb.Link):
310 linkname = propclass.classname
311 linkcl = self.db.classes[linkname]
312 k = linkcl.labelprop()
313 linkvalue = cgi.escape(linkcl.get(value, k))
314 if is_download:
315 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
316 linkvalue, linkvalue)
317 else:
318 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
319 if isinstance(propclass, hyperdb.Multilink):
320 linkname = propclass.classname
321 linkcl = self.db.classes[linkname]
322 k = linkcl.labelprop()
323 l = []
324 for value in value:
325 linkvalue = cgi.escape(linkcl.get(value, k))
326 if is_download:
327 l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
328 linkvalue, linkvalue))
329 else:
330 l.append('<a href="%s%s">%s</a>'%(linkname, value,
331 linkvalue))
332 return ', '.join(l)
333 if is_download:
334 return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
335 value, value)
336 else:
337 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
339 def do_count(self, property, **args):
340 ''' for a Multilink property, display a count of the number of links in
341 the list
342 '''
343 if not self.nodeid:
344 return _('[Count: not called from item]')
346 propclass = self.properties[property]
347 if not isinstance(propclass, hyperdb.Multilink):
348 return _('[Count: not a Multilink]')
350 # figure the length then...
351 value = self.cl.get(self.nodeid, property)
352 return str(len(value))
354 # XXX pretty is definitely new ;)
355 def do_reldate(self, property, pretty=0):
356 ''' display a Date property in terms of an interval relative to the
357 current date (e.g. "+ 3w", "- 2d").
359 with the 'pretty' flag, make it pretty
360 '''
361 if not self.nodeid and self.form is None:
362 return _('[Reldate: not called from item]')
364 propclass = self.properties[property]
365 if not isinstance(propclass, hyperdb.Date):
366 return _('[Reldate: not a Date]')
368 if self.nodeid:
369 value = self.cl.get(self.nodeid, property)
370 else:
371 return ''
372 if not value:
373 return ''
375 # figure the interval
376 interval = value - date.Date('.')
377 if pretty:
378 if not self.nodeid:
379 return _('now')
380 pretty = interval.pretty()
381 if pretty is None:
382 pretty = value.pretty()
383 return pretty
384 return str(interval)
386 def do_download(self, property, **args):
387 ''' show a Link("file") or Multilink("file") property using links that
388 allow you to download files
389 '''
390 if not self.nodeid:
391 return _('[Download: not called from item]')
392 return self.do_link(property, is_download=1)
395 def do_checklist(self, property, **args):
396 ''' for a Link or Multilink property, display checkboxes for the
397 available choices to permit filtering
398 '''
399 propclass = self.properties[property]
400 if (not isinstance(propclass, hyperdb.Link) and not
401 isinstance(propclass, hyperdb.Multilink)):
402 return _('[Checklist: not a link]')
404 # get our current checkbox state
405 if self.nodeid:
406 # get the info from the node - make sure it's a list
407 if isinstance(propclass, hyperdb.Link):
408 value = [self.cl.get(self.nodeid, property)]
409 else:
410 value = self.cl.get(self.nodeid, property)
411 elif self.filterspec is not None:
412 # get the state from the filter specification (always a list)
413 value = self.filterspec.get(property, [])
414 else:
415 # it's a new node, so there's no state
416 value = []
418 # so we can map to the linked node's "lable" property
419 linkcl = self.db.classes[propclass.classname]
420 l = []
421 k = linkcl.labelprop()
422 for optionid in linkcl.list():
423 option = cgi.escape(linkcl.get(optionid, k))
424 if optionid in value or option in value:
425 checked = 'checked'
426 else:
427 checked = ''
428 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
429 option, checked, property, option))
431 # for Links, allow the "unselected" option too
432 if isinstance(propclass, hyperdb.Link):
433 if value is None or '-1' in value:
434 checked = 'checked'
435 else:
436 checked = ''
437 l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
438 'value="-1">')%(checked, property))
439 return '\n'.join(l)
441 def do_note(self, rows=5, cols=80):
442 ''' display a "note" field, which is a text area for entering a note to
443 go along with a change.
444 '''
445 # TODO: pull the value from the form
446 return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
447 '</textarea>'%(rows, cols)
449 # XXX new function
450 def do_list(self, property, reverse=0):
451 ''' list the items specified by property using the standard index for
452 the class
453 '''
454 propcl = self.properties[property]
455 if not isinstance(propcl, hyperdb.Multilink):
456 return _('[List: not a Multilink]')
458 value = self.determine_value(property)
459 if not value:
460 return ''
462 # sort, possibly revers and then re-stringify
463 value = map(int, value)
464 value.sort()
465 if reverse:
466 value.reverse()
467 value = map(str, value)
469 # render the sub-index into a string
470 fp = StringIO.StringIO()
471 try:
472 write_save = self.client.write
473 self.client.write = fp.write
474 index = IndexTemplate(self.client, self.templates, propcl.classname)
475 index.render(nodeids=value, show_display_form=0)
476 finally:
477 self.client.write = write_save
479 return fp.getvalue()
481 # XXX new function
482 def do_history(self, direction='descending'):
483 ''' list the history of the item
485 If "direction" is 'descending' then the most recent event will
486 be displayed first. If it is 'ascending' then the oldest event
487 will be displayed first.
488 '''
489 if self.nodeid is None:
490 return _("[History: node doesn't exist]")
492 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
493 '<tr class="list-header">',
494 _('<th align=left><span class="list-item">Date</span></th>'),
495 _('<th align=left><span class="list-item">User</span></th>'),
496 _('<th align=left><span class="list-item">Action</span></th>'),
497 _('<th align=left><span class="list-item">Args</span></th>'),
498 '</tr>']
500 comments = {}
501 history = self.cl.history(self.nodeid)
502 history.sort()
503 if direction == 'descending':
504 history.reverse()
505 for id, evt_date, user, action, args in history:
506 date_s = str(evt_date).replace("."," ")
507 arg_s = ''
508 if action == 'link' and type(args) == type(()):
509 if len(args) == 3:
510 linkcl, linkid, key = args
511 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
512 linkcl, linkid, key)
513 else:
514 arg_s = str(arg)
516 elif action == 'unlink' and type(args) == type(()):
517 if len(args) == 3:
518 linkcl, linkid, key = args
519 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
520 linkcl, linkid, key)
521 else:
522 arg_s = str(arg)
524 elif type(args) == type({}):
525 cell = []
526 for k in args.keys():
527 # try to get the relevant property and treat it
528 # specially
529 try:
530 prop = self.properties[k]
531 except:
532 prop = None
533 if prop is not None:
534 if args[k] and (isinstance(prop, hyperdb.Multilink) or
535 isinstance(prop, hyperdb.Link)):
536 # figure what the link class is
537 classname = prop.classname
538 try:
539 linkcl = self.db.classes[classname]
540 except KeyError, message:
541 labelprop = None
542 comments[classname] = _('''The linked class
543 %(classname)s no longer exists''')%locals()
544 labelprop = linkcl.labelprop()
546 if isinstance(prop, hyperdb.Multilink) and \
547 len(args[k]) > 0:
548 ml = []
549 for linkid in args[k]:
550 label = classname + linkid
551 # if we have a label property, try to use it
552 # TODO: test for node existence even when
553 # there's no labelprop!
554 try:
555 if labelprop is not None:
556 label = linkcl.get(linkid, labelprop)
557 except IndexError:
558 comments['no_link'] = _('''<strike>The
559 linked node no longer
560 exists</strike>''')
561 ml.append('<strike>%s</strike>'%label)
562 else:
563 ml.append('<a href="%s%s">%s</a>'%(
564 classname, linkid, label))
565 cell.append('%s:\n %s'%(k, ',\n '.join(ml)))
566 elif isinstance(prop, hyperdb.Link) and args[k]:
567 label = classname + args[k]
568 # if we have a label property, try to use it
569 # TODO: test for node existence even when
570 # there's no labelprop!
571 if labelprop is not None:
572 try:
573 label = linkcl.get(args[k], labelprop)
574 except IndexError:
575 comments['no_link'] = _('''<strike>The
576 linked node no longer
577 exists</strike>''')
578 cell.append(' <strike>%s</strike>,\n'%label)
579 # "flag" this is done .... euwww
580 label = None
581 if label is not None:
582 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
583 classname, args[k], label))
585 elif isinstance(prop, hyperdb.Date) and args[k]:
586 d = date.Date(args[k])
587 cell.append('%s: %s'%(k, str(d)))
589 elif isinstance(prop, hyperdb.Interval) and args[k]:
590 d = date.Interval(args[k])
591 cell.append('%s: %s'%(k, str(d)))
593 elif not args[k]:
594 cell.append('%s: (no value)\n'%k)
596 else:
597 cell.append('%s: %s\n'%(k, str(args[k])))
598 else:
599 # property no longer exists
600 comments['no_exist'] = _('''<em>The indicated property
601 no longer exists</em>''')
602 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
603 arg_s = '<br />'.join(cell)
604 else:
605 # unkown event!!
606 comments['unknown'] = _('''<strong><em>This event is not
607 handled by the history display!</em></strong>''')
608 arg_s = '<strong><em>' + str(args) + '</em></strong>'
609 date_s = date_s.replace(' ', ' ')
610 l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
611 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
612 user, action, arg_s))
613 if comments:
614 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
615 for entry in comments.values():
616 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
617 l.append('</table>')
618 return '\n'.join(l)
620 # XXX new function
621 def do_submit(self):
622 ''' add a submit button for the item
623 '''
624 if self.nodeid:
625 return _('<input type="submit" name="submit" value="Submit Changes">')
626 elif self.form is not None:
627 return _('<input type="submit" name="submit" value="Submit New Entry">')
628 else:
629 return _('[Submit: not called from item]')
632 #
633 # INDEX TEMPLATES
634 #
635 class IndexTemplateReplace:
636 def __init__(self, globals, locals, props):
637 self.globals = globals
638 self.locals = locals
639 self.props = props
641 replace=re.compile(
642 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
643 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
644 def go(self, text):
645 return self.replace.sub(self, text)
647 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
648 if m.group('name'):
649 if m.group('name') in self.props:
650 text = m.group('text')
651 replace = IndexTemplateReplace(self.globals, {}, self.props)
652 return replace.go(m.group('text'))
653 else:
654 return ''
655 if m.group('display'):
656 command = m.group('command')
657 return eval(command, self.globals, self.locals)
658 print '*** unhandled match', m.groupdict()
660 class IndexTemplate(TemplateFunctions):
661 def __init__(self, client, templates, classname):
662 self.client = client
663 self.instance = client.instance
664 self.templates = templates
665 self.classname = classname
667 # derived
668 self.db = self.client.db
669 self.cl = self.db.classes[self.classname]
670 self.properties = self.cl.getprops()
672 TemplateFunctions.__init__(self)
674 col_re=re.compile(r'<property\s+name="([^>]+)">')
675 def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
676 show_display_form=1, nodeids=None, show_customization=1):
677 self.filterspec = filterspec
679 w = self.client.write
681 # get the filter template
682 try:
683 filter_template = open(os.path.join(self.templates,
684 self.classname+'.filter')).read()
685 all_filters = self.col_re.findall(filter_template)
686 except IOError, error:
687 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
688 filter_template = None
689 all_filters = []
691 # XXX deviate from spec here ...
692 # load the index section template and figure the default columns from it
693 template = open(os.path.join(self.templates,
694 self.classname+'.index')).read()
695 all_columns = self.col_re.findall(template)
696 if not columns:
697 columns = []
698 for name in all_columns:
699 columns.append(name)
700 else:
701 # re-sort columns to be the same order as all_columns
702 l = []
703 for name in all_columns:
704 if name in columns:
705 l.append(name)
706 columns = l
708 # display the filter section
709 if (show_display_form and
710 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
711 w('<form action="%s">\n'%self.classname)
712 self.filter_section(filter_template, filter, columns, group,
713 all_filters, all_columns, show_customization)
714 # make sure that the sorting doesn't get lost either
715 if sort:
716 w('<input type="hidden" name=":sort" value="%s">'%
717 ','.join(sort))
718 w('</form>\n')
721 # now display the index section
722 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
723 w('<tr class="list-header">\n')
724 for name in columns:
725 cname = name.capitalize()
726 if show_display_form:
727 sb = self.sortby(name, filterspec, columns, filter, group, sort)
728 anchor = "%s?%s"%(self.classname, sb)
729 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
730 anchor, cname))
731 else:
732 w('<td><span class="list-header">%s</span></td>\n'%cname)
733 w('</tr>\n')
735 # this stuff is used for group headings - optimise the group names
736 old_group = None
737 group_names = []
738 if group:
739 for name in group:
740 if name[0] == '-': group_names.append(name[1:])
741 else: group_names.append(name)
743 # now actually loop through all the nodes we get from the filter and
744 # apply the template
745 if nodeids is None:
746 nodeids = self.cl.filter(filterspec, sort, group)
747 for nodeid in nodeids:
748 # check for a group heading
749 if group_names:
750 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
751 if this_group != old_group:
752 l = []
753 for name in group_names:
754 prop = self.properties[name]
755 if isinstance(prop, hyperdb.Link):
756 group_cl = self.db.classes[prop.classname]
757 key = group_cl.getkey()
758 value = self.cl.get(nodeid, name)
759 if value is None:
760 l.append(_('[unselected %(classname)s]')%{
761 'classname': prop.classname})
762 else:
763 l.append(group_cl.get(self.cl.get(nodeid,
764 name), key))
765 elif isinstance(prop, hyperdb.Multilink):
766 group_cl = self.db.classes[prop.classname]
767 key = group_cl.getkey()
768 for value in self.cl.get(nodeid, name):
769 l.append(group_cl.get(value, key))
770 else:
771 value = self.cl.get(nodeid, name, _('[no value]'))
772 if value is None:
773 value = _('[empty %(name)s]')%locals()
774 else:
775 value = str(value)
776 l.append(value)
777 w('<tr class="section-bar">'
778 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
779 len(columns), ', '.join(l)))
780 old_group = this_group
782 # display this node's row
783 replace = IndexTemplateReplace(self.globals, locals(), columns)
784 self.nodeid = nodeid
785 w(replace.go(template))
786 self.nodeid = None
788 w('</table>')
790 # display the filter section
791 if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
792 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
793 w('<form action="%s">\n'%self.classname)
794 self.filter_section(filter_template, filter, columns, group,
795 all_filters, all_columns, show_customization)
796 # make sure that the sorting doesn't get lost either
797 if sort:
798 w('<input type="hidden" name=":sort" value="%s">'%
799 ','.join(sort))
800 w('</form>\n')
803 def filter_section(self, template, filter, columns, group, all_filters,
804 all_columns, show_customization):
806 w = self.client.write
808 # wrap the template in a single table to ensure the whole widget
809 # is displayed at once
810 w('<table><tr><td>')
812 if template and filter:
813 # display the filter section
814 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
815 w('<tr class="location-bar">')
816 w(_(' <th align="left" colspan="2">Filter specification...</th>'))
817 w('</tr>')
818 replace = IndexTemplateReplace(self.globals, locals(), filter)
819 w(replace.go(template))
820 w('<tr class="location-bar"><td width="1%%"> </td>')
821 w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
822 w('</table>')
824 # now add in the filter/columns/group/etc config table form
825 w('<input type="hidden" name="show_customization" value="%s">' %
826 show_customization )
827 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
828 names = []
829 for name in self.properties.keys():
830 if name in all_filters or name in all_columns:
831 names.append(name)
832 if show_customization:
833 action = '-'
834 else:
835 action = '+'
836 # hide the values for filters, columns and grouping in the form
837 # if the customization widget is not visible
838 for name in names:
839 if all_filters and name in filter:
840 w('<input type="hidden" name=":filter" value="%s">' % name)
841 if all_columns and name in columns:
842 w('<input type="hidden" name=":columns" value="%s">' % name)
843 if all_columns and name in group:
844 w('<input type="hidden" name=":group" value="%s">' % name)
846 # TODO: The widget style can go into the stylesheet
847 w(_('<th align="left" colspan=%s>'
848 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
849 'customisation...</th></tr>\n')%(len(names)+1, action))
851 if not show_customization:
852 w('</table>\n')
853 return
855 w('<tr class="location-bar"><th> </th>')
856 for name in names:
857 w('<th>%s</th>'%name.capitalize())
858 w('</tr>\n')
860 # Filter
861 if all_filters:
862 w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
863 for name in names:
864 if name not in all_filters:
865 w('<td> </td>')
866 continue
867 if name in filter: checked=' checked'
868 else: checked=''
869 w('<td align=middle>\n')
870 w(' <input type="checkbox" name=":filter" value="%s" '
871 '%s></td>\n'%(name, checked))
872 w('</tr>\n')
874 # Columns
875 if all_columns:
876 w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
877 for name in names:
878 if name not in all_columns:
879 w('<td> </td>')
880 continue
881 if name in columns: checked=' checked'
882 else: checked=''
883 w('<td align=middle>\n')
884 w(' <input type="checkbox" name=":columns" value="%s"'
885 '%s></td>\n'%(name, checked))
886 w('</tr>\n')
888 # Grouping
889 w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
890 for name in names:
891 prop = self.properties[name]
892 if name not in all_columns:
893 w('<td> </td>')
894 continue
895 if name in group: checked=' checked'
896 else: checked=''
897 w('<td align=middle>\n')
898 w(' <input type="checkbox" name=":group" value="%s"'
899 '%s></td>\n'%(name, checked))
900 w('</tr>\n')
902 w('<tr class="location-bar"><td width="1%"> </td>')
903 w('<td colspan="%s">'%len(names))
904 w(_('<input type="submit" name="action" value="Redisplay"></td>'))
905 w('</tr>\n')
906 w('</table>\n')
908 # and the outer table
909 w('</td></tr></table>')
912 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
913 l = []
914 w = l.append
915 for k, v in filterspec.items():
916 k = urllib.quote(k)
917 if type(v) == type([]):
918 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
919 else:
920 w('%s=%s'%(k, urllib.quote(v)))
921 if columns:
922 w(':columns=%s'%','.join(map(urllib.quote, columns)))
923 if filter:
924 w(':filter=%s'%','.join(map(urllib.quote, filter)))
925 if group:
926 w(':group=%s'%','.join(map(urllib.quote, group)))
927 m = []
928 s_dir = ''
929 for name in sort:
930 dir = name[0]
931 if dir == '-':
932 name = name[1:]
933 else:
934 dir = ''
935 if sort_name == name:
936 if dir == '-':
937 s_dir = ''
938 else:
939 s_dir = '-'
940 else:
941 m.append(dir+urllib.quote(name))
942 m.insert(0, s_dir+urllib.quote(sort_name))
943 # so things don't get completely out of hand, limit the sort to
944 # two columns
945 w(':sort=%s'%','.join(m[:2]))
946 return '&'.join(l)
948 #
949 # ITEM TEMPLATES
950 #
951 class ItemTemplateReplace:
952 def __init__(self, globals, locals, cl, nodeid):
953 self.globals = globals
954 self.locals = locals
955 self.cl = cl
956 self.nodeid = nodeid
958 replace=re.compile(
959 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
960 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
961 def go(self, text):
962 return self.replace.sub(self, text)
964 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
965 if m.group('name'):
966 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
967 replace = ItemTemplateReplace(self.globals, {}, self.cl,
968 self.nodeid)
969 return replace.go(m.group('text'))
970 else:
971 return ''
972 if m.group('display'):
973 command = m.group('command')
974 return eval(command, self.globals, self.locals)
975 print '*** unhandled match', m.groupdict()
978 class ItemTemplate(TemplateFunctions):
979 def __init__(self, client, templates, classname):
980 self.client = client
981 self.instance = client.instance
982 self.templates = templates
983 self.classname = classname
985 # derived
986 self.db = self.client.db
987 self.cl = self.db.classes[self.classname]
988 self.properties = self.cl.getprops()
990 TemplateFunctions.__init__(self)
992 def render(self, nodeid):
993 self.nodeid = nodeid
995 if (self.properties.has_key('type') and
996 self.properties.has_key('content')):
997 pass
998 # XXX we really want to return this as a downloadable...
999 # currently I handle this at a higher level by detecting 'file'
1000 # designators...
1002 w = self.client.write
1003 w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
1004 self.classname, nodeid))
1005 s = open(os.path.join(self.templates, self.classname+'.item')).read()
1006 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1007 w(replace.go(s))
1008 w('</form>')
1011 class NewItemTemplate(TemplateFunctions):
1012 def __init__(self, client, templates, classname):
1013 self.client = client
1014 self.instance = client.instance
1015 self.templates = templates
1016 self.classname = classname
1018 # derived
1019 self.db = self.client.db
1020 self.cl = self.db.classes[self.classname]
1021 self.properties = self.cl.getprops()
1023 TemplateFunctions.__init__(self)
1025 def render(self, form):
1026 self.form = form
1027 w = self.client.write
1028 c = self.classname
1029 try:
1030 s = open(os.path.join(self.templates, c+'.newitem')).read()
1031 except IOError:
1032 s = open(os.path.join(self.templates, c+'.item')).read()
1033 w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
1034 for key in form.keys():
1035 if key[0] == ':':
1036 value = form[key].value
1037 if type(value) != type([]): value = [value]
1038 for value in value:
1039 w('<input type="hidden" name="%s" value="%s">'%(key, value))
1040 replace = ItemTemplateReplace(self.globals, locals(), None, None)
1041 w(replace.go(s))
1042 w('</form>')
1044 #
1045 # $Log: not supported by cvs2svn $
1046 # Revision 1.70 2002/01/23 05:47:57 richard
1047 # more HTML template cleanup and unit tests
1048 #
1049 # Revision 1.69 2002/01/23 05:10:27 richard
1050 # More HTML template cleanup and unit tests.
1051 # - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1052 # templates, but link(is_download=1) will still work for existing templates]
1053 #
1054 # Revision 1.68 2002/01/22 22:55:28 richard
1055 # . htmltemplate list() wasn't sorting...
1056 #
1057 # Revision 1.67 2002/01/22 22:46:22 richard
1058 # more htmltemplate cleanups and unit tests
1059 #
1060 # Revision 1.66 2002/01/22 06:35:40 richard
1061 # more htmltemplate tests and cleanup
1062 #
1063 # Revision 1.65 2002/01/22 00:12:06 richard
1064 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1065 # off the implementation of some of the functions so they behave sanely.
1066 #
1067 # Revision 1.64 2002/01/21 03:25:59 richard
1068 # oops
1069 #
1070 # Revision 1.63 2002/01/21 02:59:10 richard
1071 # Fixed up the HTML display of history so valid links are actually displayed.
1072 # Oh for some unit tests! :(
1073 #
1074 # Revision 1.62 2002/01/18 08:36:12 grubert
1075 # . add nowrap to history table date cell i.e. <td nowrap ...
1076 #
1077 # Revision 1.61 2002/01/17 23:04:53 richard
1078 # . much nicer history display (actualy real handling of property types etc)
1079 #
1080 # Revision 1.60 2002/01/17 08:48:19 grubert
1081 # . display superseder as html link in history.
1082 #
1083 # Revision 1.59 2002/01/17 07:58:24 grubert
1084 # . display links a html link in history.
1085 #
1086 # Revision 1.58 2002/01/15 00:50:03 richard
1087 # #502949 ] index view for non-issues and redisplay
1088 #
1089 # Revision 1.57 2002/01/14 23:31:21 richard
1090 # reverted the change that had plain() hyperlinking the link displays -
1091 # that's what link() is for!
1092 #
1093 # Revision 1.56 2002/01/14 07:04:36 richard
1094 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1095 # the linked node's page.
1096 # ... this allows a display very similar to bugzilla's where you can actually
1097 # find out information about the linked node.
1098 #
1099 # Revision 1.55 2002/01/14 06:45:03 richard
1100 # . #502953 ] nosy-like treatment of other multilinks
1101 # ... had to revert most of the previous change to the multilink field
1102 # display... not good.
1103 #
1104 # Revision 1.54 2002/01/14 05:16:51 richard
1105 # The submit buttons need a name attribute or mozilla won't submit without a
1106 # file upload. Yeah, that's bloody obscure. Grr.
1107 #
1108 # Revision 1.53 2002/01/14 04:03:32 richard
1109 # How about that ... date fields have never worked ...
1110 #
1111 # Revision 1.52 2002/01/14 02:20:14 richard
1112 # . changed all config accesses so they access either the instance or the
1113 # config attriubute on the db. This means that all config is obtained from
1114 # instance_config instead of the mish-mash of classes. This will make
1115 # switching to a ConfigParser setup easier too, I hope.
1116 #
1117 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1118 # 0.5.0 switch, I hope!)
1119 #
1120 # Revision 1.51 2002/01/10 10:02:15 grubert
1121 # In do_history: replace "." in date by " " so html wraps more sensible.
1122 # Should this be done in date's string converter ?
1123 #
1124 # Revision 1.50 2002/01/05 02:35:10 richard
1125 # I18N'ification
1126 #
1127 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1128 # Features added:
1129 # . Multilink properties are now displayed as comma separated values in
1130 # a textbox
1131 # . The add user link is now only visible to the admin user
1132 # . Modified the mail gateway to reject submissions from unknown
1133 # addresses if ANONYMOUS_ACCESS is denied
1134 #
1135 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1136 # Bugs fixed:
1137 # . Exception handling in hyperdb for strings-that-look-like numbers got
1138 # lost somewhere
1139 # . Internet Explorer submits full path for filename - we now strip away
1140 # the path
1141 # Features added:
1142 # . Link and multilink properties are now displayed sorted in the cgi
1143 # interface
1144 #
1145 # Revision 1.47 2001/11/26 22:55:56 richard
1146 # Feature:
1147 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1148 # the instance.
1149 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1150 # signature info in e-mails.
1151 # . Some more flexibility in the mail gateway and more error handling.
1152 # . Login now takes you to the page you back to the were denied access to.
1153 #
1154 # Fixed:
1155 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1156 #
1157 # Revision 1.46 2001/11/24 00:53:12 jhermann
1158 # "except:" is bad, bad , bad!
1159 #
1160 # Revision 1.45 2001/11/22 15:46:42 jhermann
1161 # Added module docstrings to all modules.
1162 #
1163 # Revision 1.44 2001/11/21 23:35:45 jhermann
1164 # Added globbing for win32, and sample marking in a 2nd file to test it
1165 #
1166 # Revision 1.43 2001/11/21 04:04:43 richard
1167 # *sigh* more missing value handling
1168 #
1169 # Revision 1.42 2001/11/21 03:40:54 richard
1170 # more new property handling
1171 #
1172 # Revision 1.41 2001/11/15 10:26:01 richard
1173 # . missing "return" in filter_section (thanks Roch'e Compaan)
1174 #
1175 # Revision 1.40 2001/11/03 01:56:51 richard
1176 # More HTML compliance fixes. This will probably fix the Netscape problem
1177 # too.
1178 #
1179 # Revision 1.39 2001/11/03 01:43:47 richard
1180 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1181 #
1182 # Revision 1.38 2001/10/31 06:58:51 richard
1183 # Added the wrap="hard" attribute to the textarea of the note field so the
1184 # messages wrap sanely.
1185 #
1186 # Revision 1.37 2001/10/31 06:24:35 richard
1187 # Added do_stext to htmltemplate, thanks Brad Clements.
1188 #
1189 # Revision 1.36 2001/10/28 22:51:38 richard
1190 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1191 #
1192 # Revision 1.35 2001/10/24 00:04:41 richard
1193 # Removed the "infinite authentication loop", thanks Roch'e
1194 #
1195 # Revision 1.34 2001/10/23 22:56:36 richard
1196 # Bugfix in filter "widget" placement, thanks Roch'e
1197 #
1198 # Revision 1.33 2001/10/23 01:00:18 richard
1199 # Re-enabled login and registration access after lopping them off via
1200 # disabling access for anonymous users.
1201 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1202 # a couple of bugs while I was there. Probably introduced a couple, but
1203 # things seem to work OK at the moment.
1204 #
1205 # Revision 1.32 2001/10/22 03:25:01 richard
1206 # Added configuration for:
1207 # . anonymous user access and registration (deny/allow)
1208 # . filter "widget" location on index page (top, bottom, both)
1209 # Updated some documentation.
1210 #
1211 # Revision 1.31 2001/10/21 07:26:35 richard
1212 # feature #473127: Filenames. I modified the file.index and htmltemplate
1213 # source so that the filename is used in the link and the creation
1214 # information is displayed.
1215 #
1216 # Revision 1.30 2001/10/21 04:44:50 richard
1217 # bug #473124: UI inconsistency with Link fields.
1218 # This also prompted me to fix a fairly long-standing usability issue -
1219 # that of being able to turn off certain filters.
1220 #
1221 # Revision 1.29 2001/10/21 00:17:56 richard
1222 # CGI interface view customisation section may now be hidden (patch from
1223 # Roch'e Compaan.)
1224 #
1225 # Revision 1.28 2001/10/21 00:00:16 richard
1226 # Fixed Checklist function - wasn't always working on a list.
1227 #
1228 # Revision 1.27 2001/10/20 12:13:44 richard
1229 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1230 #
1231 # Revision 1.26 2001/10/14 10:55:00 richard
1232 # Handle empty strings in HTML template Link function
1233 #
1234 # Revision 1.25 2001/10/09 07:25:59 richard
1235 # Added the Password property type. See "pydoc roundup.password" for
1236 # implementation details. Have updated some of the documentation too.
1237 #
1238 # Revision 1.24 2001/09/27 06:45:58 richard
1239 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1240 # on the plain() template function to escape the text for HTML.
1241 #
1242 # Revision 1.23 2001/09/10 09:47:18 richard
1243 # Fixed bug in the generation of links to Link/Multilink in indexes.
1244 # (thanks Hubert Hoegl)
1245 # Added AssignedTo to the "classic" schema's item page.
1246 #
1247 # Revision 1.22 2001/08/30 06:01:17 richard
1248 # Fixed missing import in mailgw :(
1249 #
1250 # Revision 1.21 2001/08/16 07:34:59 richard
1251 # better CGI text searching - but hidden filter fields are disappearing...
1252 #
1253 # Revision 1.20 2001/08/15 23:43:18 richard
1254 # Fixed some isFooTypes that I missed.
1255 # Refactored some code in the CGI code.
1256 #
1257 # Revision 1.19 2001/08/12 06:32:36 richard
1258 # using isinstance(blah, Foo) now instead of isFooType
1259 #
1260 # Revision 1.18 2001/08/07 00:24:42 richard
1261 # stupid typo
1262 #
1263 # Revision 1.17 2001/08/07 00:15:51 richard
1264 # Added the copyright/license notice to (nearly) all files at request of
1265 # Bizar Software.
1266 #
1267 # Revision 1.16 2001/08/01 03:52:23 richard
1268 # Checklist was using wrong name.
1269 #
1270 # Revision 1.15 2001/07/30 08:12:17 richard
1271 # Added time logging and file uploading to the templates.
1272 #
1273 # Revision 1.14 2001/07/30 06:17:45 richard
1274 # Features:
1275 # . Added ability for cgi newblah forms to indicate that the new node
1276 # should be linked somewhere.
1277 # Fixed:
1278 # . Fixed the agument handling for the roundup-admin find command.
1279 # . Fixed handling of summary when no note supplied for newblah. Again.
1280 # . Fixed detection of no form in htmltemplate Field display.
1281 #
1282 # Revision 1.13 2001/07/30 02:37:53 richard
1283 # Temporary measure until we have decent schema migration.
1284 #
1285 # Revision 1.12 2001/07/30 01:24:33 richard
1286 # Handles new node display now.
1287 #
1288 # Revision 1.11 2001/07/29 09:31:35 richard
1289 # oops
1290 #
1291 # Revision 1.10 2001/07/29 09:28:23 richard
1292 # Fixed sorting by clicking on column headings.
1293 #
1294 # Revision 1.9 2001/07/29 08:27:40 richard
1295 # Fixed handling of passed-in values in form elements (ie. during a
1296 # drill-down)
1297 #
1298 # Revision 1.8 2001/07/29 07:01:39 richard
1299 # Added vim command to all source so that we don't get no steenkin' tabs :)
1300 #
1301 # Revision 1.7 2001/07/29 05:36:14 richard
1302 # Cleanup of the link label generation.
1303 #
1304 # Revision 1.6 2001/07/29 04:06:42 richard
1305 # Fixed problem in link display when Link value is None.
1306 #
1307 # Revision 1.5 2001/07/28 08:17:09 richard
1308 # fixed use of stylesheet
1309 #
1310 # Revision 1.4 2001/07/28 07:59:53 richard
1311 # Replaced errno integers with their module values.
1312 # De-tabbed templatebuilder.py
1313 #
1314 # Revision 1.3 2001/07/25 03:39:47 richard
1315 # Hrm - displaying links to classes that don't specify a key property. I've
1316 # got it defaulting to 'name', then 'title' and then a "random" property (first
1317 # one returned by getprops().keys().
1318 # Needs to be moved onto the Class I think...
1319 #
1320 # Revision 1.2 2001/07/22 12:09:32 richard
1321 # Final commit of Grande Splite
1322 #
1323 # Revision 1.1 2001/07/22 11:58:35 richard
1324 # More Grande Splite
1325 #
1326 #
1327 # vim: set filetype=python ts=4 sw=4 et si