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