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.65 2002-01-22 00:12:06 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]')
302 propclass = self.properties[property]
303 if self.nodeid:
304 value = self.cl.get(self.nodeid, property)
305 else:
306 if isinstance(propclass, hyperdb.Multilink): value = []
307 elif isinstance(propclass, hyperdb.Link): value = None
308 else: value = ''
309 if isinstance(propclass, hyperdb.Link):
310 linkname = propclass.classname
311 if value is None: return '[no %s]'%property.capitalize()
312 linkcl = self.db.classes[linkname]
313 k = linkcl.labelprop()
314 linkvalue = linkcl.get(value, k)
315 if is_download:
316 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
317 linkvalue, linkvalue)
318 else:
319 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
320 if isinstance(propclass, hyperdb.Multilink):
321 linkname = propclass.classname
322 linkcl = self.db.classes[linkname]
323 k = linkcl.labelprop()
324 if not value:
325 return _('[no %(propname)s]')%{'propname': property.capitalize()}
326 l = []
327 for value in value:
328 linkvalue = linkcl.get(value, k)
329 if is_download:
330 l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
331 linkvalue, linkvalue))
332 else:
333 l.append('<a href="%s%s">%s</a>'%(linkname, value,
334 linkvalue))
335 return ', '.join(l)
336 if isinstance(propclass, hyperdb.String) and value == '':
337 return _('[no %(propname)s]')%{'propname': property.capitalize()}
338 if is_download:
339 return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
340 value, value)
341 else:
342 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
344 def do_count(self, property, **args):
345 ''' for a Multilink property, display a count of the number of links in
346 the list
347 '''
348 if not self.nodeid:
349 return _('[Count: not called from item]')
350 propclass = self.properties[property]
351 value = self.cl.get(self.nodeid, property)
352 if isinstance(propclass, hyperdb.Multilink):
353 return str(len(value))
354 return _('[Count: not a Multilink]')
356 # XXX pretty is definitely new ;)
357 def do_reldate(self, property, pretty=0):
358 ''' display a Date property in terms of an interval relative to the
359 current date (e.g. "+ 3w", "- 2d").
361 with the 'pretty' flag, make it pretty
362 '''
363 if not self.nodeid and self.form is None:
364 return _('[Reldate: not called from item]')
365 propclass = self.properties[property]
366 if isinstance(not propclass, hyperdb.Date):
367 return _('[Reldate: not a Date]')
368 if self.nodeid:
369 value = self.cl.get(self.nodeid, property)
370 else:
371 value = date.Date('.')
372 interval = value - date.Date('.')
373 if pretty:
374 if not self.nodeid:
375 return _('now')
376 pretty = interval.pretty()
377 if pretty is None:
378 pretty = value.pretty()
379 return pretty
380 return str(interval)
382 def do_download(self, property, **args):
383 ''' show a Link("file") or Multilink("file") property using links that
384 allow you to download files
385 '''
386 if not self.nodeid:
387 return _('[Download: not called from item]')
388 propclass = self.properties[property]
389 value = self.cl.get(self.nodeid, property)
390 if isinstance(propclass, hyperdb.Link):
391 linkcl = self.db.classes[propclass.classname]
392 linkvalue = linkcl.get(value, k)
393 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
394 if isinstance(propclass, hyperdb.Multilink):
395 linkcl = self.db.classes[propclass.classname]
396 l = []
397 for value in value:
398 linkvalue = linkcl.get(value, k)
399 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
400 return ', '.join(l)
401 return _('[Download: not a link]')
404 def do_checklist(self, property, **args):
405 ''' for a Link or Multilink property, display checkboxes for the
406 available choices to permit filtering
407 '''
408 propclass = self.properties[property]
409 if (not isinstance(propclass, hyperdb.Link) and not
410 isinstance(propclass, hyperdb.Multilink)):
411 return _('[Checklist: not a link]')
413 # get our current checkbox state
414 if self.nodeid:
415 # get the info from the node - make sure it's a list
416 if isinstance(propclass, hyperdb.Link):
417 value = [self.cl.get(self.nodeid, property)]
418 else:
419 value = self.cl.get(self.nodeid, property)
420 elif self.filterspec is not None:
421 # get the state from the filter specification (always a list)
422 value = self.filterspec.get(property, [])
423 else:
424 # it's a new node, so there's no state
425 value = []
427 # so we can map to the linked node's "lable" property
428 linkcl = self.db.classes[propclass.classname]
429 l = []
430 k = linkcl.labelprop()
431 for optionid in linkcl.list():
432 option = linkcl.get(optionid, k)
433 if optionid in value or option in value:
434 checked = 'checked'
435 else:
436 checked = ''
437 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
438 option, checked, property, option))
440 # for Links, allow the "unselected" option too
441 if isinstance(propclass, hyperdb.Link):
442 if value is None or '-1' in value:
443 checked = 'checked'
444 else:
445 checked = ''
446 l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
447 'value="-1">')%(checked, property))
448 return '\n'.join(l)
450 def do_note(self, rows=5, cols=80):
451 ''' display a "note" field, which is a text area for entering a note to
452 go along with a change.
453 '''
454 # TODO: pull the value from the form
455 return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
456 '</textarea>'%(rows, cols)
458 # XXX new function
459 def do_list(self, property, reverse=0):
460 ''' list the items specified by property using the standard index for
461 the class
462 '''
463 propcl = self.properties[property]
464 if not isinstance(propcl, hyperdb.Multilink):
465 return _('[List: not a Multilink]')
466 value = self.cl.get(self.nodeid, property)
467 if reverse:
468 value.reverse()
470 # render the sub-index into a string
471 fp = StringIO.StringIO()
472 try:
473 write_save = self.client.write
474 self.client.write = fp.write
475 index = IndexTemplate(self.client, self.templates, propcl.classname)
476 index.render(nodeids=value, show_display_form=0)
477 finally:
478 self.client.write = write_save
480 return fp.getvalue()
482 # XXX new function
483 def do_history(self, direction='descending'):
484 ''' list the history of the item
486 If "direction" is 'descending' then the most recent event will
487 be displayed first. If it is 'ascending' then the oldest event
488 will be displayed first.
489 '''
490 if self.nodeid is None:
491 return _("[History: node doesn't exist]")
493 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
494 '<tr class="list-header">',
495 _('<th align=left><span class="list-item">Date</span></th>'),
496 _('<th align=left><span class="list-item">User</span></th>'),
497 _('<th align=left><span class="list-item">Action</span></th>'),
498 _('<th align=left><span class="list-item">Args</span></th>'),
499 '</tr>']
501 comments = {}
502 history = self.cl.history(self.nodeid)
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.64 2002/01/21 03:25:59 richard
1047 # oops
1048 #
1049 # Revision 1.63 2002/01/21 02:59:10 richard
1050 # Fixed up the HTML display of history so valid links are actually displayed.
1051 # Oh for some unit tests! :(
1052 #
1053 # Revision 1.62 2002/01/18 08:36:12 grubert
1054 # . add nowrap to history table date cell i.e. <td nowrap ...
1055 #
1056 # Revision 1.61 2002/01/17 23:04:53 richard
1057 # . much nicer history display (actualy real handling of property types etc)
1058 #
1059 # Revision 1.60 2002/01/17 08:48:19 grubert
1060 # . display superseder as html link in history.
1061 #
1062 # Revision 1.59 2002/01/17 07:58:24 grubert
1063 # . display links a html link in history.
1064 #
1065 # Revision 1.58 2002/01/15 00:50:03 richard
1066 # #502949 ] index view for non-issues and redisplay
1067 #
1068 # Revision 1.57 2002/01/14 23:31:21 richard
1069 # reverted the change that had plain() hyperlinking the link displays -
1070 # that's what link() is for!
1071 #
1072 # Revision 1.56 2002/01/14 07:04:36 richard
1073 # . plain rendering of links in the htmltemplate now generate a hyperlink to
1074 # the linked node's page.
1075 # ... this allows a display very similar to bugzilla's where you can actually
1076 # find out information about the linked node.
1077 #
1078 # Revision 1.55 2002/01/14 06:45:03 richard
1079 # . #502953 ] nosy-like treatment of other multilinks
1080 # ... had to revert most of the previous change to the multilink field
1081 # display... not good.
1082 #
1083 # Revision 1.54 2002/01/14 05:16:51 richard
1084 # The submit buttons need a name attribute or mozilla won't submit without a
1085 # file upload. Yeah, that's bloody obscure. Grr.
1086 #
1087 # Revision 1.53 2002/01/14 04:03:32 richard
1088 # How about that ... date fields have never worked ...
1089 #
1090 # Revision 1.52 2002/01/14 02:20:14 richard
1091 # . changed all config accesses so they access either the instance or the
1092 # config attriubute on the db. This means that all config is obtained from
1093 # instance_config instead of the mish-mash of classes. This will make
1094 # switching to a ConfigParser setup easier too, I hope.
1095 #
1096 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1097 # 0.5.0 switch, I hope!)
1098 #
1099 # Revision 1.51 2002/01/10 10:02:15 grubert
1100 # In do_history: replace "." in date by " " so html wraps more sensible.
1101 # Should this be done in date's string converter ?
1102 #
1103 # Revision 1.50 2002/01/05 02:35:10 richard
1104 # I18N'ification
1105 #
1106 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
1107 # Features added:
1108 # . Multilink properties are now displayed as comma separated values in
1109 # a textbox
1110 # . The add user link is now only visible to the admin user
1111 # . Modified the mail gateway to reject submissions from unknown
1112 # addresses if ANONYMOUS_ACCESS is denied
1113 #
1114 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
1115 # Bugs fixed:
1116 # . Exception handling in hyperdb for strings-that-look-like numbers got
1117 # lost somewhere
1118 # . Internet Explorer submits full path for filename - we now strip away
1119 # the path
1120 # Features added:
1121 # . Link and multilink properties are now displayed sorted in the cgi
1122 # interface
1123 #
1124 # Revision 1.47 2001/11/26 22:55:56 richard
1125 # Feature:
1126 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1127 # the instance.
1128 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1129 # signature info in e-mails.
1130 # . Some more flexibility in the mail gateway and more error handling.
1131 # . Login now takes you to the page you back to the were denied access to.
1132 #
1133 # Fixed:
1134 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1135 #
1136 # Revision 1.46 2001/11/24 00:53:12 jhermann
1137 # "except:" is bad, bad , bad!
1138 #
1139 # Revision 1.45 2001/11/22 15:46:42 jhermann
1140 # Added module docstrings to all modules.
1141 #
1142 # Revision 1.44 2001/11/21 23:35:45 jhermann
1143 # Added globbing for win32, and sample marking in a 2nd file to test it
1144 #
1145 # Revision 1.43 2001/11/21 04:04:43 richard
1146 # *sigh* more missing value handling
1147 #
1148 # Revision 1.42 2001/11/21 03:40:54 richard
1149 # more new property handling
1150 #
1151 # Revision 1.41 2001/11/15 10:26:01 richard
1152 # . missing "return" in filter_section (thanks Roch'e Compaan)
1153 #
1154 # Revision 1.40 2001/11/03 01:56:51 richard
1155 # More HTML compliance fixes. This will probably fix the Netscape problem
1156 # too.
1157 #
1158 # Revision 1.39 2001/11/03 01:43:47 richard
1159 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1160 #
1161 # Revision 1.38 2001/10/31 06:58:51 richard
1162 # Added the wrap="hard" attribute to the textarea of the note field so the
1163 # messages wrap sanely.
1164 #
1165 # Revision 1.37 2001/10/31 06:24:35 richard
1166 # Added do_stext to htmltemplate, thanks Brad Clements.
1167 #
1168 # Revision 1.36 2001/10/28 22:51:38 richard
1169 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1170 #
1171 # Revision 1.35 2001/10/24 00:04:41 richard
1172 # Removed the "infinite authentication loop", thanks Roch'e
1173 #
1174 # Revision 1.34 2001/10/23 22:56:36 richard
1175 # Bugfix in filter "widget" placement, thanks Roch'e
1176 #
1177 # Revision 1.33 2001/10/23 01:00:18 richard
1178 # Re-enabled login and registration access after lopping them off via
1179 # disabling access for anonymous users.
1180 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1181 # a couple of bugs while I was there. Probably introduced a couple, but
1182 # things seem to work OK at the moment.
1183 #
1184 # Revision 1.32 2001/10/22 03:25:01 richard
1185 # Added configuration for:
1186 # . anonymous user access and registration (deny/allow)
1187 # . filter "widget" location on index page (top, bottom, both)
1188 # Updated some documentation.
1189 #
1190 # Revision 1.31 2001/10/21 07:26:35 richard
1191 # feature #473127: Filenames. I modified the file.index and htmltemplate
1192 # source so that the filename is used in the link and the creation
1193 # information is displayed.
1194 #
1195 # Revision 1.30 2001/10/21 04:44:50 richard
1196 # bug #473124: UI inconsistency with Link fields.
1197 # This also prompted me to fix a fairly long-standing usability issue -
1198 # that of being able to turn off certain filters.
1199 #
1200 # Revision 1.29 2001/10/21 00:17:56 richard
1201 # CGI interface view customisation section may now be hidden (patch from
1202 # Roch'e Compaan.)
1203 #
1204 # Revision 1.28 2001/10/21 00:00:16 richard
1205 # Fixed Checklist function - wasn't always working on a list.
1206 #
1207 # Revision 1.27 2001/10/20 12:13:44 richard
1208 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1209 #
1210 # Revision 1.26 2001/10/14 10:55:00 richard
1211 # Handle empty strings in HTML template Link function
1212 #
1213 # Revision 1.25 2001/10/09 07:25:59 richard
1214 # Added the Password property type. See "pydoc roundup.password" for
1215 # implementation details. Have updated some of the documentation too.
1216 #
1217 # Revision 1.24 2001/09/27 06:45:58 richard
1218 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1219 # on the plain() template function to escape the text for HTML.
1220 #
1221 # Revision 1.23 2001/09/10 09:47:18 richard
1222 # Fixed bug in the generation of links to Link/Multilink in indexes.
1223 # (thanks Hubert Hoegl)
1224 # Added AssignedTo to the "classic" schema's item page.
1225 #
1226 # Revision 1.22 2001/08/30 06:01:17 richard
1227 # Fixed missing import in mailgw :(
1228 #
1229 # Revision 1.21 2001/08/16 07:34:59 richard
1230 # better CGI text searching - but hidden filter fields are disappearing...
1231 #
1232 # Revision 1.20 2001/08/15 23:43:18 richard
1233 # Fixed some isFooTypes that I missed.
1234 # Refactored some code in the CGI code.
1235 #
1236 # Revision 1.19 2001/08/12 06:32:36 richard
1237 # using isinstance(blah, Foo) now instead of isFooType
1238 #
1239 # Revision 1.18 2001/08/07 00:24:42 richard
1240 # stupid typo
1241 #
1242 # Revision 1.17 2001/08/07 00:15:51 richard
1243 # Added the copyright/license notice to (nearly) all files at request of
1244 # Bizar Software.
1245 #
1246 # Revision 1.16 2001/08/01 03:52:23 richard
1247 # Checklist was using wrong name.
1248 #
1249 # Revision 1.15 2001/07/30 08:12:17 richard
1250 # Added time logging and file uploading to the templates.
1251 #
1252 # Revision 1.14 2001/07/30 06:17:45 richard
1253 # Features:
1254 # . Added ability for cgi newblah forms to indicate that the new node
1255 # should be linked somewhere.
1256 # Fixed:
1257 # . Fixed the agument handling for the roundup-admin find command.
1258 # . Fixed handling of summary when no note supplied for newblah. Again.
1259 # . Fixed detection of no form in htmltemplate Field display.
1260 #
1261 # Revision 1.13 2001/07/30 02:37:53 richard
1262 # Temporary measure until we have decent schema migration.
1263 #
1264 # Revision 1.12 2001/07/30 01:24:33 richard
1265 # Handles new node display now.
1266 #
1267 # Revision 1.11 2001/07/29 09:31:35 richard
1268 # oops
1269 #
1270 # Revision 1.10 2001/07/29 09:28:23 richard
1271 # Fixed sorting by clicking on column headings.
1272 #
1273 # Revision 1.9 2001/07/29 08:27:40 richard
1274 # Fixed handling of passed-in values in form elements (ie. during a
1275 # drill-down)
1276 #
1277 # Revision 1.8 2001/07/29 07:01:39 richard
1278 # Added vim command to all source so that we don't get no steenkin' tabs :)
1279 #
1280 # Revision 1.7 2001/07/29 05:36:14 richard
1281 # Cleanup of the link label generation.
1282 #
1283 # Revision 1.6 2001/07/29 04:06:42 richard
1284 # Fixed problem in link display when Link value is None.
1285 #
1286 # Revision 1.5 2001/07/28 08:17:09 richard
1287 # fixed use of stylesheet
1288 #
1289 # Revision 1.4 2001/07/28 07:59:53 richard
1290 # Replaced errno integers with their module values.
1291 # De-tabbed templatebuilder.py
1292 #
1293 # Revision 1.3 2001/07/25 03:39:47 richard
1294 # Hrm - displaying links to classes that don't specify a key property. I've
1295 # got it defaulting to 'name', then 'title' and then a "random" property (first
1296 # one returned by getprops().keys().
1297 # Needs to be moved onto the Class I think...
1298 #
1299 # Revision 1.2 2001/07/22 12:09:32 richard
1300 # Final commit of Grande Splite
1301 #
1302 # Revision 1.1 2001/07/22 11:58:35 richard
1303 # More Grande Splite
1304 #
1305 #
1306 # vim: set filetype=python ts=4 sw=4 et si