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