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