159041051d006b97e815c3a22349bbb18902bbf0
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.69 2002-01-23 05:10:27 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 = 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]')
457 value = self.cl.get(self.nodeid, property)
458 value.sort()
459 if reverse:
460 value.reverse()
462 # render the sub-index into a string
463 fp = StringIO.StringIO()
464 try:
465 write_save = self.client.write
466 self.client.write = fp.write
467 index = IndexTemplate(self.client, self.templates, propcl.classname)
468 index.render(nodeids=value, show_display_form=0)
469 finally:
470 self.client.write = write_save
472 return fp.getvalue()
474 # XXX new function
475 def do_history(self, direction='descending'):
476 ''' list the history of the item
478 If "direction" is 'descending' then the most recent event will
479 be displayed first. If it is 'ascending' then the oldest event
480 will be displayed first.
481 '''
482 if self.nodeid is None:
483 return _("[History: node doesn't exist]")
485 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
486 '<tr class="list-header">',
487 _('<th align=left><span class="list-item">Date</span></th>'),
488 _('<th align=left><span class="list-item">User</span></th>'),
489 _('<th align=left><span class="list-item">Action</span></th>'),
490 _('<th align=left><span class="list-item">Args</span></th>'),
491 '</tr>']
493 comments = {}
494 history = self.cl.history(self.nodeid)
495 history.sort()
496 if direction == 'descending':
497 history.reverse()
498 for id, evt_date, user, action, args in history:
499 date_s = str(evt_date).replace("."," ")
500 arg_s = ''
501 if action == 'link' and type(args) == type(()):
502 if len(args) == 3:
503 linkcl, linkid, key = args
504 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
505 linkcl, linkid, key)
506 else:
507 arg_s = str(arg)
509 elif action == 'unlink' and type(args) == type(()):
510 if len(args) == 3:
511 linkcl, linkid, key = args
512 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
513 linkcl, linkid, key)
514 else:
515 arg_s = str(arg)
517 elif type(args) == type({}):
518 cell = []
519 for k in args.keys():
520 # try to get the relevant property and treat it
521 # specially
522 try:
523 prop = self.properties[k]
524 except:
525 prop = None
526 if prop is not None:
527 if args[k] and (isinstance(prop, hyperdb.Multilink) or
528 isinstance(prop, hyperdb.Link)):
529 # figure what the link class is
530 classname = prop.classname
531 try:
532 linkcl = self.db.classes[classname]
533 except KeyError, message:
534 labelprop = None
535 comments[classname] = _('''The linked class
536 %(classname)s no longer exists''')%locals()
537 labelprop = linkcl.labelprop()
539 if isinstance(prop, hyperdb.Multilink) and \
540 len(args[k]) > 0:
541 ml = []
542 for linkid in args[k]:
543 label = classname + linkid
544 # if we have a label property, try to use it
545 # TODO: test for node existence even when
546 # there's no labelprop!
547 try:
548 if labelprop is not None:
549 label = linkcl.get(linkid, labelprop)
550 except IndexError:
551 comments['no_link'] = _('''<strike>The
552 linked node no longer
553 exists</strike>''')
554 ml.append('<strike>%s</strike>'%label)
555 else:
556 ml.append('<a href="%s%s">%s</a>'%(
557 classname, linkid, label))
558 cell.append('%s:\n %s'%(k, ',\n '.join(ml)))
559 elif isinstance(prop, hyperdb.Link) and args[k]:
560 label = classname + args[k]
561 # if we have a label property, try to use it
562 # TODO: test for node existence even when
563 # there's no labelprop!
564 if labelprop is not None:
565 try:
566 label = linkcl.get(args[k], labelprop)
567 except IndexError:
568 comments['no_link'] = _('''<strike>The
569 linked node no longer
570 exists</strike>''')
571 cell.append(' <strike>%s</strike>,\n'%label)
572 # "flag" this is done .... euwww
573 label = None
574 if label is not None:
575 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
576 classname, args[k], label))
578 elif isinstance(prop, hyperdb.Date) and args[k]:
579 d = date.Date(args[k])
580 cell.append('%s: %s'%(k, str(d)))
582 elif isinstance(prop, hyperdb.Interval) and args[k]:
583 d = date.Interval(args[k])
584 cell.append('%s: %s'%(k, str(d)))
586 elif not args[k]:
587 cell.append('%s: (no value)\n'%k)
589 else:
590 cell.append('%s: %s\n'%(k, str(args[k])))
591 else:
592 # property no longer exists
593 comments['no_exist'] = _('''<em>The indicated property
594 no longer exists</em>''')
595 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
596 arg_s = '<br />'.join(cell)
597 else:
598 # unkown event!!
599 comments['unknown'] = _('''<strong><em>This event is not
600 handled by the history display!</em></strong>''')
601 arg_s = '<strong><em>' + str(args) + '</em></strong>'
602 date_s = date_s.replace(' ', ' ')
603 l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
604 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
605 user, action, arg_s))
606 if comments:
607 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
608 for entry in comments.values():
609 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
610 l.append('</table>')
611 return '\n'.join(l)
613 # XXX new function
614 def do_submit(self):
615 ''' add a submit button for the item
616 '''
617 if self.nodeid:
618 return _('<input type="submit" name="submit" value="Submit Changes">')
619 elif self.form is not None:
620 return _('<input type="submit" name="submit" value="Submit New Entry">')
621 else:
622 return _('[Submit: not called from item]')
625 #
626 # INDEX TEMPLATES
627 #
628 class IndexTemplateReplace:
629 def __init__(self, globals, locals, props):
630 self.globals = globals
631 self.locals = locals
632 self.props = props
634 replace=re.compile(
635 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
636 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
637 def go(self, text):
638 return self.replace.sub(self, text)
640 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
641 if m.group('name'):
642 if m.group('name') in self.props:
643 text = m.group('text')
644 replace = IndexTemplateReplace(self.globals, {}, self.props)
645 return replace.go(m.group('text'))
646 else:
647 return ''
648 if m.group('display'):
649 command = m.group('command')
650 return eval(command, self.globals, self.locals)
651 print '*** unhandled match', m.groupdict()
653 class IndexTemplate(TemplateFunctions):
654 def __init__(self, client, templates, classname):
655 self.client = client
656 self.instance = client.instance
657 self.templates = templates
658 self.classname = classname
660 # derived
661 self.db = self.client.db
662 self.cl = self.db.classes[self.classname]
663 self.properties = self.cl.getprops()
665 TemplateFunctions.__init__(self)
667 col_re=re.compile(r'<property\s+name="([^>]+)">')
668 def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
669 show_display_form=1, nodeids=None, show_customization=1):
670 self.filterspec = filterspec
672 w = self.client.write
674 # get the filter template
675 try:
676 filter_template = open(os.path.join(self.templates,
677 self.classname+'.filter')).read()
678 all_filters = self.col_re.findall(filter_template)
679 except IOError, error:
680 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
681 filter_template = None
682 all_filters = []
684 # XXX deviate from spec here ...
685 # load the index section template and figure the default columns from it
686 template = open(os.path.join(self.templates,
687 self.classname+'.index')).read()
688 all_columns = self.col_re.findall(template)
689 if not columns:
690 columns = []
691 for name in all_columns:
692 columns.append(name)
693 else:
694 # re-sort columns to be the same order as all_columns
695 l = []
696 for name in all_columns:
697 if name in columns:
698 l.append(name)
699 columns = l
701 # display the filter section
702 if (show_display_form and
703 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
704 w('<form action="%s">\n'%self.classname)
705 self.filter_section(filter_template, filter, columns, group,
706 all_filters, all_columns, show_customization)
707 # make sure that the sorting doesn't get lost either
708 if sort:
709 w('<input type="hidden" name=":sort" value="%s">'%
710 ','.join(sort))
711 w('</form>\n')
714 # now display the index section
715 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
716 w('<tr class="list-header">\n')
717 for name in columns:
718 cname = name.capitalize()
719 if show_display_form:
720 sb = self.sortby(name, filterspec, columns, filter, group, sort)
721 anchor = "%s?%s"%(self.classname, sb)
722 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
723 anchor, cname))
724 else:
725 w('<td><span class="list-header">%s</span></td>\n'%cname)
726 w('</tr>\n')
728 # this stuff is used for group headings - optimise the group names
729 old_group = None
730 group_names = []
731 if group:
732 for name in group:
733 if name[0] == '-': group_names.append(name[1:])
734 else: group_names.append(name)
736 # now actually loop through all the nodes we get from the filter and
737 # apply the template
738 if nodeids is None:
739 nodeids = self.cl.filter(filterspec, sort, group)
740 for nodeid in nodeids:
741 # check for a group heading
742 if group_names:
743 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
744 if this_group != old_group:
745 l = []
746 for name in group_names:
747 prop = self.properties[name]
748 if isinstance(prop, hyperdb.Link):
749 group_cl = self.db.classes[prop.classname]
750 key = group_cl.getkey()
751 value = self.cl.get(nodeid, name)
752 if value is None:
753 l.append(_('[unselected %(classname)s]')%{
754 'classname': prop.classname})
755 else:
756 l.append(group_cl.get(self.cl.get(nodeid,
757 name), key))
758 elif isinstance(prop, hyperdb.Multilink):
759 group_cl = self.db.classes[prop.classname]
760 key = group_cl.getkey()
761 for value in self.cl.get(nodeid, name):
762 l.append(group_cl.get(value, key))
763 else:
764 value = self.cl.get(nodeid, name, _('[no value]'))
765 if value is None:
766 value = _('[empty %(name)s]')%locals()
767 else:
768 value = str(value)
769 l.append(value)
770 w('<tr class="section-bar">'
771 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
772 len(columns), ', '.join(l)))
773 old_group = this_group
775 # display this node's row
776 replace = IndexTemplateReplace(self.globals, locals(), columns)
777 self.nodeid = nodeid
778 w(replace.go(template))
779 self.nodeid = None
781 w('</table>')
783 # display the filter section
784 if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
785 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
786 w('<form action="%s">\n'%self.classname)
787 self.filter_section(filter_template, filter, columns, group,
788 all_filters, all_columns, show_customization)
789 # make sure that the sorting doesn't get lost either
790 if sort:
791 w('<input type="hidden" name=":sort" value="%s">'%
792 ','.join(sort))
793 w('</form>\n')
796 def filter_section(self, template, filter, columns, group, all_filters,
797 all_columns, show_customization):
799 w = self.client.write
801 # wrap the template in a single table to ensure the whole widget
802 # is displayed at once
803 w('<table><tr><td>')
805 if template and filter:
806 # display the filter section
807 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
808 w('<tr class="location-bar">')
809 w(_(' <th align="left" colspan="2">Filter specification...</th>'))
810 w('</tr>')
811 replace = IndexTemplateReplace(self.globals, locals(), filter)
812 w(replace.go(template))
813 w('<tr class="location-bar"><td width="1%%"> </td>')
814 w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
815 w('</table>')
817 # now add in the filter/columns/group/etc config table form
818 w('<input type="hidden" name="show_customization" value="%s">' %
819 show_customization )
820 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
821 names = []
822 for name in self.properties.keys():
823 if name in all_filters or name in all_columns:
824 names.append(name)
825 if show_customization:
826 action = '-'
827 else:
828 action = '+'
829 # hide the values for filters, columns and grouping in the form
830 # if the customization widget is not visible
831 for name in names:
832 if all_filters and name in filter:
833 w('<input type="hidden" name=":filter" value="%s">' % name)
834 if all_columns and name in columns:
835 w('<input type="hidden" name=":columns" value="%s">' % name)
836 if all_columns and name in group:
837 w('<input type="hidden" name=":group" value="%s">' % name)
839 # TODO: The widget style can go into the stylesheet
840 w(_('<th align="left" colspan=%s>'
841 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
842 'customisation...</th></tr>\n')%(len(names)+1, action))
844 if not show_customization:
845 w('</table>\n')
846 return
848 w('<tr class="location-bar"><th> </th>')
849 for name in names:
850 w('<th>%s</th>'%name.capitalize())
851 w('</tr>\n')
853 # Filter
854 if all_filters:
855 w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
856 for name in names:
857 if name not in all_filters:
858 w('<td> </td>')
859 continue
860 if name in filter: checked=' checked'
861 else: checked=''
862 w('<td align=middle>\n')
863 w(' <input type="checkbox" name=":filter" value="%s" '
864 '%s></td>\n'%(name, checked))
865 w('</tr>\n')
867 # Columns
868 if all_columns:
869 w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
870 for name in names:
871 if name not in all_columns:
872 w('<td> </td>')
873 continue
874 if name in columns: checked=' checked'
875 else: checked=''
876 w('<td align=middle>\n')
877 w(' <input type="checkbox" name=":columns" value="%s"'
878 '%s></td>\n'%(name, checked))
879 w('</tr>\n')
881 # Grouping
882 w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
883 for name in names:
884 prop = self.properties[name]
885 if name not in all_columns:
886 w('<td> </td>')
887 continue
888 if name in group: checked=' checked'
889 else: checked=''
890 w('<td align=middle>\n')
891 w(' <input type="checkbox" name=":group" value="%s"'
892 '%s></td>\n'%(name, checked))
893 w('</tr>\n')
895 w('<tr class="location-bar"><td width="1%"> </td>')
896 w('<td colspan="%s">'%len(names))
897 w(_('<input type="submit" name="action" value="Redisplay"></td>'))
898 w('</tr>\n')
899 w('</table>\n')
901 # and the outer table
902 w('</td></tr></table>')
905 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
906 l = []
907 w = l.append
908 for k, v in filterspec.items():
909 k = urllib.quote(k)
910 if type(v) == type([]):
911 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
912 else:
913 w('%s=%s'%(k, urllib.quote(v)))
914 if columns:
915 w(':columns=%s'%','.join(map(urllib.quote, columns)))
916 if filter:
917 w(':filter=%s'%','.join(map(urllib.quote, filter)))
918 if group:
919 w(':group=%s'%','.join(map(urllib.quote, group)))
920 m = []
921 s_dir = ''
922 for name in sort:
923 dir = name[0]
924 if dir == '-':
925 name = name[1:]
926 else:
927 dir = ''
928 if sort_name == name:
929 if dir == '-':
930 s_dir = ''
931 else:
932 s_dir = '-'
933 else:
934 m.append(dir+urllib.quote(name))
935 m.insert(0, s_dir+urllib.quote(sort_name))
936 # so things don't get completely out of hand, limit the sort to
937 # two columns
938 w(':sort=%s'%','.join(m[:2]))
939 return '&'.join(l)
941 #
942 # ITEM TEMPLATES
943 #
944 class ItemTemplateReplace:
945 def __init__(self, globals, locals, cl, nodeid):
946 self.globals = globals
947 self.locals = locals
948 self.cl = cl
949 self.nodeid = nodeid
951 replace=re.compile(
952 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
953 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
954 def go(self, text):
955 return self.replace.sub(self, text)
957 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
958 if m.group('name'):
959 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
960 replace = ItemTemplateReplace(self.globals, {}, self.cl,
961 self.nodeid)
962 return replace.go(m.group('text'))
963 else:
964 return ''
965 if m.group('display'):
966 command = m.group('command')
967 return eval(command, self.globals, self.locals)
968 print '*** unhandled match', m.groupdict()
971 class ItemTemplate(TemplateFunctions):
972 def __init__(self, client, templates, classname):
973 self.client = client
974 self.instance = client.instance
975 self.templates = templates
976 self.classname = classname
978 # derived
979 self.db = self.client.db
980 self.cl = self.db.classes[self.classname]
981 self.properties = self.cl.getprops()
983 TemplateFunctions.__init__(self)
985 def render(self, nodeid):
986 self.nodeid = nodeid
988 if (self.properties.has_key('type') and
989 self.properties.has_key('content')):
990 pass
991 # XXX we really want to return this as a downloadable...
992 # currently I handle this at a higher level by detecting 'file'
993 # designators...
995 w = self.client.write
996 w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
997 self.classname, nodeid))
998 s = open(os.path.join(self.templates, self.classname+'.item')).read()
999 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1000 w(replace.go(s))
1001 w('</form>')
1004 class NewItemTemplate(TemplateFunctions):
1005 def __init__(self, client, templates, classname):
1006 self.client = client
1007 self.instance = client.instance
1008 self.templates = templates
1009 self.classname = classname
1011 # derived
1012 self.db = self.client.db
1013 self.cl = self.db.classes[self.classname]
1014 self.properties = self.cl.getprops()
1016 TemplateFunctions.__init__(self)
1018 def render(self, form):
1019 self.form = form
1020 w = self.client.write
1021 c = self.classname
1022 try:
1023 s = open(os.path.join(self.templates, c+'.newitem')).read()
1024 except IOError:
1025 s = open(os.path.join(self.templates, c+'.item')).read()
1026 w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
1027 for key in form.keys():
1028 if key[0] == ':':
1029 value = form[key].value
1030 if type(value) != type([]): value = [value]
1031 for value in value:
1032 w('<input type="hidden" name="%s" value="%s">'%(key, value))
1033 replace = ItemTemplateReplace(self.globals, locals(), None, None)
1034 w(replace.go(s))
1035 w('</form>')
1037 #
1038 # $Log: not supported by cvs2svn $
1039 # Revision 1.68 2002/01/22 22:55:28 richard
1040 # . htmltemplate list() wasn't sorting...
1041 #
1042 # Revision 1.67 2002/01/22 22:46:22 richard
1043 # more htmltemplate cleanups and unit tests
1044 #
1045 # Revision 1.66 2002/01/22 06:35:40 richard
1046 # more htmltemplate tests and cleanup
1047 #
1048 # Revision 1.65 2002/01/22 00:12:06 richard
1049 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1050 # off the implementation of some of the functions so they behave sanely.
1051 #
1052 # Revision 1.64 2002/01/21 03:25:59 richard
1053 # oops
1054 #
1055 # Revision 1.63 2002/01/21 02:59:10 richard
1056 # Fixed up the HTML display of history so valid links are actually displayed.
1057 # Oh for some unit tests! :(
1058 #
1059 # Revision 1.62 2002/01/18 08:36:12 grubert
1060 # . add nowrap to history table date cell i.e. <td nowrap ...
1061 #
1062 # Revision 1.61 2002/01/17 23:04:53 richard
1063 # . much nicer history display (actualy real handling of property types etc)
1064 #
1065 # Revision 1.60 2002/01/17 08:48:19 grubert
1066 # . display superseder as html link in history.
1067 #
1068 # Revision 1.59 2002/01/17 07:58:24 grubert
1069 # . display links a html link in history.
1070 #
1071 # Revision 1.58 2002/01/15 00:50:03 richard
1072 # #502949 ] index view for non-issues and redisplay
1073 #
1074 # Revision 1.57 2002/01/14 23:31:21 richard
1075 # reverted the change that had plain() hyperlinking the link displays -
1076 # that's what link() is for!
1077 #
1078 # Revision 1.56 2002/01/14 07:04:36 richard
1079 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1080 # the linked node's page.
1081 # ... this allows a display very similar to bugzilla's where you can actually
1082 # find out information about the linked node.
1083 #
1084 # Revision 1.55 2002/01/14 06:45:03 richard
1085 # . #502953 ] nosy-like treatment of other multilinks
1086 # ... had to revert most of the previous change to the multilink field
1087 # display... not good.
1088 #
1089 # Revision 1.54 2002/01/14 05:16:51 richard
1090 # The submit buttons need a name attribute or mozilla won't submit without a
1091 # file upload. Yeah, that's bloody obscure. Grr.
1092 #
1093 # Revision 1.53 2002/01/14 04:03:32 richard
1094 # How about that ... date fields have never worked ...
1095 #
1096 # Revision 1.52 2002/01/14 02:20:14 richard
1097 # . changed all config accesses so they access either the instance or the
1098 # config attriubute on the db. This means that all config is obtained from
1099 # instance_config instead of the mish-mash of classes. This will make
1100 # switching to a ConfigParser setup easier too, I hope.
1101 #
1102 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1103 # 0.5.0 switch, I hope!)
1104 #
1105 # Revision 1.51 2002/01/10 10:02:15 grubert
1106 # In do_history: replace "." in date by " " so html wraps more sensible.
1107 # Should this be done in date's string converter ?
1108 #
1109 # Revision 1.50 2002/01/05 02:35:10 richard
1110 # I18N'ification
1111 #
1112 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1113 # Features added:
1114 # . Multilink properties are now displayed as comma separated values in
1115 # a textbox
1116 # . The add user link is now only visible to the admin user
1117 # . Modified the mail gateway to reject submissions from unknown
1118 # addresses if ANONYMOUS_ACCESS is denied
1119 #
1120 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1121 # Bugs fixed:
1122 # . Exception handling in hyperdb for strings-that-look-like numbers got
1123 # lost somewhere
1124 # . Internet Explorer submits full path for filename - we now strip away
1125 # the path
1126 # Features added:
1127 # . Link and multilink properties are now displayed sorted in the cgi
1128 # interface
1129 #
1130 # Revision 1.47 2001/11/26 22:55:56 richard
1131 # Feature:
1132 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1133 # the instance.
1134 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1135 # signature info in e-mails.
1136 # . Some more flexibility in the mail gateway and more error handling.
1137 # . Login now takes you to the page you back to the were denied access to.
1138 #
1139 # Fixed:
1140 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1141 #
1142 # Revision 1.46 2001/11/24 00:53:12 jhermann
1143 # "except:" is bad, bad , bad!
1144 #
1145 # Revision 1.45 2001/11/22 15:46:42 jhermann
1146 # Added module docstrings to all modules.
1147 #
1148 # Revision 1.44 2001/11/21 23:35:45 jhermann
1149 # Added globbing for win32, and sample marking in a 2nd file to test it
1150 #
1151 # Revision 1.43 2001/11/21 04:04:43 richard
1152 # *sigh* more missing value handling
1153 #
1154 # Revision 1.42 2001/11/21 03:40:54 richard
1155 # more new property handling
1156 #
1157 # Revision 1.41 2001/11/15 10:26:01 richard
1158 # . missing "return" in filter_section (thanks Roch'e Compaan)
1159 #
1160 # Revision 1.40 2001/11/03 01:56:51 richard
1161 # More HTML compliance fixes. This will probably fix the Netscape problem
1162 # too.
1163 #
1164 # Revision 1.39 2001/11/03 01:43:47 richard
1165 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1166 #
1167 # Revision 1.38 2001/10/31 06:58:51 richard
1168 # Added the wrap="hard" attribute to the textarea of the note field so the
1169 # messages wrap sanely.
1170 #
1171 # Revision 1.37 2001/10/31 06:24:35 richard
1172 # Added do_stext to htmltemplate, thanks Brad Clements.
1173 #
1174 # Revision 1.36 2001/10/28 22:51:38 richard
1175 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1176 #
1177 # Revision 1.35 2001/10/24 00:04:41 richard
1178 # Removed the "infinite authentication loop", thanks Roch'e
1179 #
1180 # Revision 1.34 2001/10/23 22:56:36 richard
1181 # Bugfix in filter "widget" placement, thanks Roch'e
1182 #
1183 # Revision 1.33 2001/10/23 01:00:18 richard
1184 # Re-enabled login and registration access after lopping them off via
1185 # disabling access for anonymous users.
1186 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1187 # a couple of bugs while I was there. Probably introduced a couple, but
1188 # things seem to work OK at the moment.
1189 #
1190 # Revision 1.32 2001/10/22 03:25:01 richard
1191 # Added configuration for:
1192 # . anonymous user access and registration (deny/allow)
1193 # . filter "widget" location on index page (top, bottom, both)
1194 # Updated some documentation.
1195 #
1196 # Revision 1.31 2001/10/21 07:26:35 richard
1197 # feature #473127: Filenames. I modified the file.index and htmltemplate
1198 # source so that the filename is used in the link and the creation
1199 # information is displayed.
1200 #
1201 # Revision 1.30 2001/10/21 04:44:50 richard
1202 # bug #473124: UI inconsistency with Link fields.
1203 # This also prompted me to fix a fairly long-standing usability issue -
1204 # that of being able to turn off certain filters.
1205 #
1206 # Revision 1.29 2001/10/21 00:17:56 richard
1207 # CGI interface view customisation section may now be hidden (patch from
1208 # Roch'e Compaan.)
1209 #
1210 # Revision 1.28 2001/10/21 00:00:16 richard
1211 # Fixed Checklist function - wasn't always working on a list.
1212 #
1213 # Revision 1.27 2001/10/20 12:13:44 richard
1214 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1215 #
1216 # Revision 1.26 2001/10/14 10:55:00 richard
1217 # Handle empty strings in HTML template Link function
1218 #
1219 # Revision 1.25 2001/10/09 07:25:59 richard
1220 # Added the Password property type. See "pydoc roundup.password" for
1221 # implementation details. Have updated some of the documentation too.
1222 #
1223 # Revision 1.24 2001/09/27 06:45:58 richard
1224 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1225 # on the plain() template function to escape the text for HTML.
1226 #
1227 # Revision 1.23 2001/09/10 09:47:18 richard
1228 # Fixed bug in the generation of links to Link/Multilink in indexes.
1229 # (thanks Hubert Hoegl)
1230 # Added AssignedTo to the "classic" schema's item page.
1231 #
1232 # Revision 1.22 2001/08/30 06:01:17 richard
1233 # Fixed missing import in mailgw :(
1234 #
1235 # Revision 1.21 2001/08/16 07:34:59 richard
1236 # better CGI text searching - but hidden filter fields are disappearing...
1237 #
1238 # Revision 1.20 2001/08/15 23:43:18 richard
1239 # Fixed some isFooTypes that I missed.
1240 # Refactored some code in the CGI code.
1241 #
1242 # Revision 1.19 2001/08/12 06:32:36 richard
1243 # using isinstance(blah, Foo) now instead of isFooType
1244 #
1245 # Revision 1.18 2001/08/07 00:24:42 richard
1246 # stupid typo
1247 #
1248 # Revision 1.17 2001/08/07 00:15:51 richard
1249 # Added the copyright/license notice to (nearly) all files at request of
1250 # Bizar Software.
1251 #
1252 # Revision 1.16 2001/08/01 03:52:23 richard
1253 # Checklist was using wrong name.
1254 #
1255 # Revision 1.15 2001/07/30 08:12:17 richard
1256 # Added time logging and file uploading to the templates.
1257 #
1258 # Revision 1.14 2001/07/30 06:17:45 richard
1259 # Features:
1260 # . Added ability for cgi newblah forms to indicate that the new node
1261 # should be linked somewhere.
1262 # Fixed:
1263 # . Fixed the agument handling for the roundup-admin find command.
1264 # . Fixed handling of summary when no note supplied for newblah. Again.
1265 # . Fixed detection of no form in htmltemplate Field display.
1266 #
1267 # Revision 1.13 2001/07/30 02:37:53 richard
1268 # Temporary measure until we have decent schema migration.
1269 #
1270 # Revision 1.12 2001/07/30 01:24:33 richard
1271 # Handles new node display now.
1272 #
1273 # Revision 1.11 2001/07/29 09:31:35 richard
1274 # oops
1275 #
1276 # Revision 1.10 2001/07/29 09:28:23 richard
1277 # Fixed sorting by clicking on column headings.
1278 #
1279 # Revision 1.9 2001/07/29 08:27:40 richard
1280 # Fixed handling of passed-in values in form elements (ie. during a
1281 # drill-down)
1282 #
1283 # Revision 1.8 2001/07/29 07:01:39 richard
1284 # Added vim command to all source so that we don't get no steenkin' tabs :)
1285 #
1286 # Revision 1.7 2001/07/29 05:36:14 richard
1287 # Cleanup of the link label generation.
1288 #
1289 # Revision 1.6 2001/07/29 04:06:42 richard
1290 # Fixed problem in link display when Link value is None.
1291 #
1292 # Revision 1.5 2001/07/28 08:17:09 richard
1293 # fixed use of stylesheet
1294 #
1295 # Revision 1.4 2001/07/28 07:59:53 richard
1296 # Replaced errno integers with their module values.
1297 # De-tabbed templatebuilder.py
1298 #
1299 # Revision 1.3 2001/07/25 03:39:47 richard
1300 # Hrm - displaying links to classes that don't specify a key property. I've
1301 # got it defaulting to 'name', then 'title' and then a "random" property (first
1302 # one returned by getprops().keys().
1303 # Needs to be moved onto the Class I think...
1304 #
1305 # Revision 1.2 2001/07/22 12:09:32 richard
1306 # Final commit of Grande Splite
1307 #
1308 # Revision 1.1 2001/07/22 11:58:35 richard
1309 # More Grande Splite
1310 #
1311 #
1312 # vim: set filetype=python ts=4 sw=4 et si