28f4f40040ff7b1da352887f8e3b6ea4d4298e60
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.50 2002-01-05 02:35:10 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: value = str(linkcl.get(value, k))
89 else: value = _('[unselected]')
90 elif isinstance(propclass, hyperdb.Multilink):
91 linkcl = self.db.classes[propclass.classname]
92 k = linkcl.labelprop()
93 value = ', '.join([linkcl.get(i, k) for i in value])
94 else:
95 s = _('Plain: bad propclass "%(propclass)s"')%locals()
96 if escape:
97 value = cgi.escape(value)
98 return value
100 def do_stext(self, property, escape=0):
101 '''Render as structured text using the StructuredText module
102 (see above for details)
103 '''
104 s = self.do_plain(property, escape=escape)
105 if not StructuredText:
106 return s
107 return StructuredText(s,level=1,header=0)
109 def do_field(self, property, size=None, height=None, showid=0):
110 ''' display a property like the plain displayer, but in a text field
111 to be edited
112 '''
113 if not self.nodeid and self.form is None and self.filterspec is None:
114 return _('[Field: not called from item]')
115 propclass = self.properties[property]
116 if (isinstance(propclass, hyperdb.Link) or
117 isinstance(propclass, hyperdb.Multilink)):
118 linkcl = self.db.classes[propclass.classname]
119 def sortfunc(a, b, cl=linkcl):
120 if cl.getprops().has_key('order'):
121 sort_on = 'order'
122 else:
123 sort_on = cl.labelprop()
124 r = cmp(cl.get(a, sort_on), cl.get(b, sort_on))
125 return r
126 if self.nodeid:
127 value = self.cl.get(self.nodeid, property, None)
128 # TODO: remove this from the code ... it's only here for
129 # handling schema changes, and they should be handled outside
130 # of this code...
131 if isinstance(propclass, hyperdb.Multilink) and value is None:
132 value = []
133 elif self.filterspec is not None:
134 if isinstance(propclass, hyperdb.Multilink):
135 value = self.filterspec.get(property, [])
136 else:
137 value = self.filterspec.get(property, '')
138 else:
139 # TODO: pull the value from the form
140 if isinstance(propclass, hyperdb.Multilink): value = []
141 else: value = ''
142 if (isinstance(propclass, hyperdb.String) or
143 isinstance(propclass, hyperdb.Date) or
144 isinstance(propclass, hyperdb.Interval)):
145 size = size or 30
146 if value is None:
147 value = ''
148 else:
149 value = cgi.escape(value)
150 value = '"'.join(value.split('"'))
151 s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
152 elif isinstance(propclass, hyperdb.Password):
153 size = size or 30
154 s = '<input type="password" name="%s" size="%s">'%(property, size)
155 elif isinstance(propclass, hyperdb.Link):
156 l = ['<select name="%s">'%property]
157 k = linkcl.labelprop()
158 if value is None:
159 s = 'selected '
160 else:
161 s = ''
162 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
163 options = linkcl.list()
164 options.sort(sortfunc)
165 for optionid in options:
166 option = linkcl.get(optionid, k)
167 s = ''
168 if optionid == value:
169 s = 'selected '
170 if showid:
171 lab = '%s%s: %s'%(propclass.classname, optionid, option)
172 else:
173 lab = option
174 if size is not None and len(lab) > size:
175 lab = lab[:size-3] + '...'
176 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
177 l.append('</select>')
178 s = '\n'.join(l)
179 elif isinstance(propclass, hyperdb.Multilink):
180 list = linkcl.list()
181 list.sort(sortfunc)
182 k = linkcl.labelprop()
183 l = []
184 # special treatment for nosy list
185 if property == 'nosy':
186 input_value = []
187 else:
188 input_value = value
189 for v in value:
190 lab = linkcl.get(v, k)
191 if property != 'nosy':
192 l.append('<a href="issue%s">%s: %s</a>'%(v,v,lab))
193 else:
194 input_value.append(lab)
195 if size is None:
196 size = '10'
197 l.insert(0,'<input name="%s" size="%s" value="%s">'%(property,
198 size, ','.join(input_value)))
199 s = "<br>\n".join(l)
200 else:
201 s = _('Plain: bad propclass "%(propclass)s"')%locals()
202 return s
204 def do_menu(self, property, size=None, height=None, showid=0):
205 ''' for a Link property, display a menu of the available choices
206 '''
207 propclass = self.properties[property]
208 if self.nodeid:
209 value = self.cl.get(self.nodeid, property)
210 else:
211 # TODO: pull the value from the form
212 if isinstance(propclass, hyperdb.Multilink): value = []
213 else: value = None
214 if isinstance(propclass, hyperdb.Link):
215 linkcl = self.db.classes[propclass.classname]
216 l = ['<select name="%s">'%property]
217 k = linkcl.labelprop()
218 s = ''
219 if value is None:
220 s = 'selected '
221 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
222 for optionid in linkcl.list():
223 option = linkcl.get(optionid, k)
224 s = ''
225 if optionid == value:
226 s = 'selected '
227 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
228 l.append('</select>')
229 return '\n'.join(l)
230 if isinstance(propclass, hyperdb.Multilink):
231 linkcl = self.db.classes[propclass.classname]
232 list = linkcl.list()
233 height = height or min(len(list), 7)
234 l = ['<select multiple name="%s" size="%s">'%(property, height)]
235 k = linkcl.labelprop()
236 for optionid in list:
237 option = linkcl.get(optionid, k)
238 s = ''
239 if optionid in value:
240 s = 'selected '
241 if showid:
242 lab = '%s%s: %s'%(propclass.classname, optionid, option)
243 else:
244 lab = option
245 if size is not None and len(lab) > size:
246 lab = lab[:size-3] + '...'
247 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
248 l.append('</select>')
249 return '\n'.join(l)
250 return _('[Menu: not a link]')
252 #XXX deviates from spec
253 def do_link(self, property=None, is_download=0):
254 '''For a Link or Multilink property, display the names of the linked
255 nodes, hyperlinked to the item views on those nodes.
256 For other properties, link to this node with the property as the
257 text.
259 If is_download is true, append the property value to the generated
260 URL so that the link may be used as a download link and the
261 downloaded file name is correct.
262 '''
263 if not self.nodeid and self.form is None:
264 return _('[Link: not called from item]')
265 propclass = self.properties[property]
266 if self.nodeid:
267 value = self.cl.get(self.nodeid, property)
268 else:
269 if isinstance(propclass, hyperdb.Multilink): value = []
270 elif isinstance(propclass, hyperdb.Link): value = None
271 else: value = ''
272 if isinstance(propclass, hyperdb.Link):
273 linkname = propclass.classname
274 if value is None: return '[no %s]'%property.capitalize()
275 linkcl = self.db.classes[linkname]
276 k = linkcl.labelprop()
277 linkvalue = linkcl.get(value, k)
278 if is_download:
279 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
280 linkvalue, linkvalue)
281 else:
282 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
283 if isinstance(propclass, hyperdb.Multilink):
284 linkname = propclass.classname
285 linkcl = self.db.classes[linkname]
286 k = linkcl.labelprop()
287 if not value:
288 return _('[no %(propname)s]')%{'propname': property.capitalize()}
289 l = []
290 for value in value:
291 linkvalue = linkcl.get(value, k)
292 if is_download:
293 l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
294 linkvalue, linkvalue))
295 else:
296 l.append('<a href="%s%s">%s</a>'%(linkname, value,
297 linkvalue))
298 return ', '.join(l)
299 if isinstance(propclass, hyperdb.String) and value == '':
300 return _('[no %(propname)s]')%{'propname': property.capitalize()}
301 if is_download:
302 return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
303 value, value)
304 else:
305 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
307 def do_count(self, property, **args):
308 ''' for a Multilink property, display a count of the number of links in
309 the list
310 '''
311 if not self.nodeid:
312 return _('[Count: not called from item]')
313 propclass = self.properties[property]
314 value = self.cl.get(self.nodeid, property)
315 if isinstance(propclass, hyperdb.Multilink):
316 return str(len(value))
317 return _('[Count: not a Multilink]')
319 # XXX pretty is definitely new ;)
320 def do_reldate(self, property, pretty=0):
321 ''' display a Date property in terms of an interval relative to the
322 current date (e.g. "+ 3w", "- 2d").
324 with the 'pretty' flag, make it pretty
325 '''
326 if not self.nodeid and self.form is None:
327 return _('[Reldate: not called from item]')
328 propclass = self.properties[property]
329 if isinstance(not propclass, hyperdb.Date):
330 return _('[Reldate: not a Date]')
331 if self.nodeid:
332 value = self.cl.get(self.nodeid, property)
333 else:
334 value = date.Date('.')
335 interval = value - date.Date('.')
336 if pretty:
337 if not self.nodeid:
338 return _('now')
339 pretty = interval.pretty()
340 if pretty is None:
341 pretty = value.pretty()
342 return pretty
343 return str(interval)
345 def do_download(self, property, **args):
346 ''' show a Link("file") or Multilink("file") property using links that
347 allow you to download files
348 '''
349 if not self.nodeid:
350 return _('[Download: not called from item]')
351 propclass = self.properties[property]
352 value = self.cl.get(self.nodeid, property)
353 if isinstance(propclass, hyperdb.Link):
354 linkcl = self.db.classes[propclass.classname]
355 linkvalue = linkcl.get(value, k)
356 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
357 if isinstance(propclass, hyperdb.Multilink):
358 linkcl = self.db.classes[propclass.classname]
359 l = []
360 for value in value:
361 linkvalue = linkcl.get(value, k)
362 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
363 return ', '.join(l)
364 return _('[Download: not a link]')
367 def do_checklist(self, property, **args):
368 ''' for a Link or Multilink property, display checkboxes for the
369 available choices to permit filtering
370 '''
371 propclass = self.properties[property]
372 if (not isinstance(propclass, hyperdb.Link) and not
373 isinstance(propclass, hyperdb.Multilink)):
374 return _('[Checklist: not a link]')
376 # get our current checkbox state
377 if self.nodeid:
378 # get the info from the node - make sure it's a list
379 if isinstance(propclass, hyperdb.Link):
380 value = [self.cl.get(self.nodeid, property)]
381 else:
382 value = self.cl.get(self.nodeid, property)
383 elif self.filterspec is not None:
384 # get the state from the filter specification (always a list)
385 value = self.filterspec.get(property, [])
386 else:
387 # it's a new node, so there's no state
388 value = []
390 # so we can map to the linked node's "lable" property
391 linkcl = self.db.classes[propclass.classname]
392 l = []
393 k = linkcl.labelprop()
394 for optionid in linkcl.list():
395 option = linkcl.get(optionid, k)
396 if optionid in value or option in value:
397 checked = 'checked'
398 else:
399 checked = ''
400 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
401 option, checked, property, option))
403 # for Links, allow the "unselected" option too
404 if isinstance(propclass, hyperdb.Link):
405 if value is None or '-1' in value:
406 checked = 'checked'
407 else:
408 checked = ''
409 l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
410 'value="-1">')%(checked, property))
411 return '\n'.join(l)
413 def do_note(self, rows=5, cols=80):
414 ''' display a "note" field, which is a text area for entering a note to
415 go along with a change.
416 '''
417 # TODO: pull the value from the form
418 return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
419 '</textarea>'%(rows, cols)
421 # XXX new function
422 def do_list(self, property, reverse=0):
423 ''' list the items specified by property using the standard index for
424 the class
425 '''
426 propcl = self.properties[property]
427 if not isinstance(propcl, hyperdb.Multilink):
428 return _('[List: not a Multilink]')
429 value = self.cl.get(self.nodeid, property)
430 if reverse:
431 value.reverse()
433 # render the sub-index into a string
434 fp = StringIO.StringIO()
435 try:
436 write_save = self.client.write
437 self.client.write = fp.write
438 index = IndexTemplate(self.client, self.templates, propcl.classname)
439 index.render(nodeids=value, show_display_form=0)
440 finally:
441 self.client.write = write_save
443 return fp.getvalue()
445 # XXX new function
446 def do_history(self, **args):
447 ''' list the history of the item
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 _('<td><span class="list-item"><strong>Date</strong></span></td>'),
455 _('<td><span class="list-item"><strong>User</strong></span></td>'),
456 _('<td><span class="list-item"><strong>Action</strong></span></td>'),
457 _('<td><span class="list-item"><strong>Args</strong></span></td>')]
459 for id, date, user, action, args in self.cl.history(self.nodeid):
460 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
461 date, user, action, args))
462 l.append('</table>')
463 return '\n'.join(l)
465 # XXX new function
466 def do_submit(self):
467 ''' add a submit button for the item
468 '''
469 if self.nodeid:
470 return _('<input type="submit" value="Submit Changes">')
471 elif self.form is not None:
472 return _('<input type="submit" value="Submit New Entry">')
473 else:
474 return _('[Submit: not called from item]')
477 #
478 # INDEX TEMPLATES
479 #
480 class IndexTemplateReplace:
481 def __init__(self, globals, locals, props):
482 self.globals = globals
483 self.locals = locals
484 self.props = props
486 replace=re.compile(
487 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
488 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
489 def go(self, text):
490 return self.replace.sub(self, text)
492 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
493 if m.group('name'):
494 if m.group('name') in self.props:
495 text = m.group('text')
496 replace = IndexTemplateReplace(self.globals, {}, self.props)
497 return replace.go(m.group('text'))
498 else:
499 return ''
500 if m.group('display'):
501 command = m.group('command')
502 return eval(command, self.globals, self.locals)
503 print '*** unhandled match', m.groupdict()
505 class IndexTemplate(TemplateFunctions):
506 def __init__(self, client, templates, classname):
507 self.client = client
508 self.templates = templates
509 self.classname = classname
511 # derived
512 self.db = self.client.db
513 self.cl = self.db.classes[self.classname]
514 self.properties = self.cl.getprops()
516 TemplateFunctions.__init__(self)
518 col_re=re.compile(r'<property\s+name="([^>]+)">')
519 def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
520 show_display_form=1, nodeids=None, show_customization=1):
521 self.filterspec = filterspec
523 w = self.client.write
525 # get the filter template
526 try:
527 filter_template = open(os.path.join(self.templates,
528 self.classname+'.filter')).read()
529 all_filters = self.col_re.findall(filter_template)
530 except IOError, error:
531 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
532 filter_template = None
533 all_filters = []
535 # XXX deviate from spec here ...
536 # load the index section template and figure the default columns from it
537 template = open(os.path.join(self.templates,
538 self.classname+'.index')).read()
539 all_columns = self.col_re.findall(template)
540 if not columns:
541 columns = []
542 for name in all_columns:
543 columns.append(name)
544 else:
545 # re-sort columns to be the same order as all_columns
546 l = []
547 for name in all_columns:
548 if name in columns:
549 l.append(name)
550 columns = l
552 # display the filter section
553 if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
554 self.client.FILTER_POSITION in ('top and bottom', 'top')):
555 w('<form action="index">\n')
556 self.filter_section(filter_template, filter, columns, group,
557 all_filters, all_columns, show_customization)
558 # make sure that the sorting doesn't get lost either
559 if sort:
560 w('<input type="hidden" name=":sort" value="%s">'%
561 ','.join(sort))
562 w('</form>\n')
565 # now display the index section
566 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
567 w('<tr class="list-header">\n')
568 for name in columns:
569 cname = name.capitalize()
570 if show_display_form:
571 sb = self.sortby(name, filterspec, columns, filter, group, sort)
572 anchor = "%s?%s"%(self.classname, sb)
573 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
574 anchor, cname))
575 else:
576 w('<td><span class="list-header">%s</span></td>\n'%cname)
577 w('</tr>\n')
579 # this stuff is used for group headings - optimise the group names
580 old_group = None
581 group_names = []
582 if group:
583 for name in group:
584 if name[0] == '-': group_names.append(name[1:])
585 else: group_names.append(name)
587 # now actually loop through all the nodes we get from the filter and
588 # apply the template
589 if nodeids is None:
590 nodeids = self.cl.filter(filterspec, sort, group)
591 for nodeid in nodeids:
592 # check for a group heading
593 if group_names:
594 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
595 if this_group != old_group:
596 l = []
597 for name in group_names:
598 prop = self.properties[name]
599 if isinstance(prop, hyperdb.Link):
600 group_cl = self.db.classes[prop.classname]
601 key = group_cl.getkey()
602 value = self.cl.get(nodeid, name)
603 if value is None:
604 l.append(_('[unselected %(classname)s]')%{
605 'classname': prop.classname})
606 else:
607 l.append(group_cl.get(self.cl.get(nodeid,
608 name), key))
609 elif isinstance(prop, hyperdb.Multilink):
610 group_cl = self.db.classes[prop.classname]
611 key = group_cl.getkey()
612 for value in self.cl.get(nodeid, name):
613 l.append(group_cl.get(value, key))
614 else:
615 value = self.cl.get(nodeid, name, _('[no value]'))
616 if value is None:
617 value = _('[empty %(name)s]')%locals()
618 else:
619 value = str(value)
620 l.append(value)
621 w('<tr class="section-bar">'
622 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
623 len(columns), ', '.join(l)))
624 old_group = this_group
626 # display this node's row
627 replace = IndexTemplateReplace(self.globals, locals(), columns)
628 self.nodeid = nodeid
629 w(replace.go(template))
630 self.nodeid = None
632 w('</table>')
634 # display the filter section
635 if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
636 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
637 w('<form action="index">\n')
638 self.filter_section(filter_template, filter, columns, group,
639 all_filters, all_columns, show_customization)
640 # make sure that the sorting doesn't get lost either
641 if sort:
642 w('<input type="hidden" name=":sort" value="%s">'%
643 ','.join(sort))
644 w('</form>\n')
647 def filter_section(self, template, filter, columns, group, all_filters,
648 all_columns, show_customization):
650 w = self.client.write
652 # wrap the template in a single table to ensure the whole widget
653 # is displayed at once
654 w('<table><tr><td>')
656 if template and filter:
657 # display the filter section
658 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
659 w('<tr class="location-bar">')
660 w(_(' <th align="left" colspan="2">Filter specification...</th>'))
661 w('</tr>')
662 replace = IndexTemplateReplace(self.globals, locals(), filter)
663 w(replace.go(template))
664 w('<tr class="location-bar"><td width="1%%"> </td>')
665 w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
666 w('</table>')
668 # now add in the filter/columns/group/etc config table form
669 w('<input type="hidden" name="show_customization" value="%s">' %
670 show_customization )
671 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
672 names = []
673 for name in self.properties.keys():
674 if name in all_filters or name in all_columns:
675 names.append(name)
676 if show_customization:
677 action = '-'
678 else:
679 action = '+'
680 # hide the values for filters, columns and grouping in the form
681 # if the customization widget is not visible
682 for name in names:
683 if all_filters and name in filter:
684 w('<input type="hidden" name=":filter" value="%s">' % name)
685 if all_columns and name in columns:
686 w('<input type="hidden" name=":columns" value="%s">' % name)
687 if all_columns and name in group:
688 w('<input type="hidden" name=":group" value="%s">' % name)
690 # TODO: The widget style can go into the stylesheet
691 w(_('<th align="left" colspan=%s>'
692 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
693 'customisation...</th></tr>\n')%(len(names)+1, action))
695 if not show_customization:
696 w('</table>\n')
697 return
699 w('<tr class="location-bar"><th> </th>')
700 for name in names:
701 w('<th>%s</th>'%name.capitalize())
702 w('</tr>\n')
704 # Filter
705 if all_filters:
706 w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
707 for name in names:
708 if name not in all_filters:
709 w('<td> </td>')
710 continue
711 if name in filter: checked=' checked'
712 else: checked=''
713 w('<td align=middle>\n')
714 w(' <input type="checkbox" name=":filter" value="%s" '
715 '%s></td>\n'%(name, checked))
716 w('</tr>\n')
718 # Columns
719 if all_columns:
720 w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
721 for name in names:
722 if name not in all_columns:
723 w('<td> </td>')
724 continue
725 if name in columns: checked=' checked'
726 else: checked=''
727 w('<td align=middle>\n')
728 w(' <input type="checkbox" name=":columns" value="%s"'
729 '%s></td>\n'%(name, checked))
730 w('</tr>\n')
732 # Grouping
733 w(_('<tr><th width="1%" align=right class="location-bar">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.49 2001/12/20 15:43:01 rochecompaan
889 # Features added:
890 # . Multilink properties are now displayed as comma separated values in
891 # a textbox
892 # . The add user link is now only visible to the admin user
893 # . Modified the mail gateway to reject submissions from unknown
894 # addresses if ANONYMOUS_ACCESS is denied
895 #
896 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
897 # Bugs fixed:
898 # . Exception handling in hyperdb for strings-that-look-like numbers got
899 # lost somewhere
900 # . Internet Explorer submits full path for filename - we now strip away
901 # the path
902 # Features added:
903 # . Link and multilink properties are now displayed sorted in the cgi
904 # interface
905 #
906 # Revision 1.47 2001/11/26 22:55:56 richard
907 # Feature:
908 # . Added INSTANCE_NAME to configuration - used in web and email to identify
909 # the instance.
910 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
911 # signature info in e-mails.
912 # . Some more flexibility in the mail gateway and more error handling.
913 # . Login now takes you to the page you back to the were denied access to.
914 #
915 # Fixed:
916 # . Lots of bugs, thanks Roché and others on the devel mailing list!
917 #
918 # Revision 1.46 2001/11/24 00:53:12 jhermann
919 # "except:" is bad, bad , bad!
920 #
921 # Revision 1.45 2001/11/22 15:46:42 jhermann
922 # Added module docstrings to all modules.
923 #
924 # Revision 1.44 2001/11/21 23:35:45 jhermann
925 # Added globbing for win32, and sample marking in a 2nd file to test it
926 #
927 # Revision 1.43 2001/11/21 04:04:43 richard
928 # *sigh* more missing value handling
929 #
930 # Revision 1.42 2001/11/21 03:40:54 richard
931 # more new property handling
932 #
933 # Revision 1.41 2001/11/15 10:26:01 richard
934 # . missing "return" in filter_section (thanks Roch'e Compaan)
935 #
936 # Revision 1.40 2001/11/03 01:56:51 richard
937 # More HTML compliance fixes. This will probably fix the Netscape problem
938 # too.
939 #
940 # Revision 1.39 2001/11/03 01:43:47 richard
941 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
942 #
943 # Revision 1.38 2001/10/31 06:58:51 richard
944 # Added the wrap="hard" attribute to the textarea of the note field so the
945 # messages wrap sanely.
946 #
947 # Revision 1.37 2001/10/31 06:24:35 richard
948 # Added do_stext to htmltemplate, thanks Brad Clements.
949 #
950 # Revision 1.36 2001/10/28 22:51:38 richard
951 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
952 #
953 # Revision 1.35 2001/10/24 00:04:41 richard
954 # Removed the "infinite authentication loop", thanks Roch'e
955 #
956 # Revision 1.34 2001/10/23 22:56:36 richard
957 # Bugfix in filter "widget" placement, thanks Roch'e
958 #
959 # Revision 1.33 2001/10/23 01:00:18 richard
960 # Re-enabled login and registration access after lopping them off via
961 # disabling access for anonymous users.
962 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
963 # a couple of bugs while I was there. Probably introduced a couple, but
964 # things seem to work OK at the moment.
965 #
966 # Revision 1.32 2001/10/22 03:25:01 richard
967 # Added configuration for:
968 # . anonymous user access and registration (deny/allow)
969 # . filter "widget" location on index page (top, bottom, both)
970 # Updated some documentation.
971 #
972 # Revision 1.31 2001/10/21 07:26:35 richard
973 # feature #473127: Filenames. I modified the file.index and htmltemplate
974 # source so that the filename is used in the link and the creation
975 # information is displayed.
976 #
977 # Revision 1.30 2001/10/21 04:44:50 richard
978 # bug #473124: UI inconsistency with Link fields.
979 # This also prompted me to fix a fairly long-standing usability issue -
980 # that of being able to turn off certain filters.
981 #
982 # Revision 1.29 2001/10/21 00:17:56 richard
983 # CGI interface view customisation section may now be hidden (patch from
984 # Roch'e Compaan.)
985 #
986 # Revision 1.28 2001/10/21 00:00:16 richard
987 # Fixed Checklist function - wasn't always working on a list.
988 #
989 # Revision 1.27 2001/10/20 12:13:44 richard
990 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
991 #
992 # Revision 1.26 2001/10/14 10:55:00 richard
993 # Handle empty strings in HTML template Link function
994 #
995 # Revision 1.25 2001/10/09 07:25:59 richard
996 # Added the Password property type. See "pydoc roundup.password" for
997 # implementation details. Have updated some of the documentation too.
998 #
999 # Revision 1.24 2001/09/27 06:45:58 richard
1000 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1001 # on the plain() template function to escape the text for HTML.
1002 #
1003 # Revision 1.23 2001/09/10 09:47:18 richard
1004 # Fixed bug in the generation of links to Link/Multilink in indexes.
1005 # (thanks Hubert Hoegl)
1006 # Added AssignedTo to the "classic" schema's item page.
1007 #
1008 # Revision 1.22 2001/08/30 06:01:17 richard
1009 # Fixed missing import in mailgw :(
1010 #
1011 # Revision 1.21 2001/08/16 07:34:59 richard
1012 # better CGI text searching - but hidden filter fields are disappearing...
1013 #
1014 # Revision 1.20 2001/08/15 23:43:18 richard
1015 # Fixed some isFooTypes that I missed.
1016 # Refactored some code in the CGI code.
1017 #
1018 # Revision 1.19 2001/08/12 06:32:36 richard
1019 # using isinstance(blah, Foo) now instead of isFooType
1020 #
1021 # Revision 1.18 2001/08/07 00:24:42 richard
1022 # stupid typo
1023 #
1024 # Revision 1.17 2001/08/07 00:15:51 richard
1025 # Added the copyright/license notice to (nearly) all files at request of
1026 # Bizar Software.
1027 #
1028 # Revision 1.16 2001/08/01 03:52:23 richard
1029 # Checklist was using wrong name.
1030 #
1031 # Revision 1.15 2001/07/30 08:12:17 richard
1032 # Added time logging and file uploading to the templates.
1033 #
1034 # Revision 1.14 2001/07/30 06:17:45 richard
1035 # Features:
1036 # . Added ability for cgi newblah forms to indicate that the new node
1037 # should be linked somewhere.
1038 # Fixed:
1039 # . Fixed the agument handling for the roundup-admin find command.
1040 # . Fixed handling of summary when no note supplied for newblah. Again.
1041 # . Fixed detection of no form in htmltemplate Field display.
1042 #
1043 # Revision 1.13 2001/07/30 02:37:53 richard
1044 # Temporary measure until we have decent schema migration.
1045 #
1046 # Revision 1.12 2001/07/30 01:24:33 richard
1047 # Handles new node display now.
1048 #
1049 # Revision 1.11 2001/07/29 09:31:35 richard
1050 # oops
1051 #
1052 # Revision 1.10 2001/07/29 09:28:23 richard
1053 # Fixed sorting by clicking on column headings.
1054 #
1055 # Revision 1.9 2001/07/29 08:27:40 richard
1056 # Fixed handling of passed-in values in form elements (ie. during a
1057 # drill-down)
1058 #
1059 # Revision 1.8 2001/07/29 07:01:39 richard
1060 # Added vim command to all source so that we don't get no steenkin' tabs :)
1061 #
1062 # Revision 1.7 2001/07/29 05:36:14 richard
1063 # Cleanup of the link label generation.
1064 #
1065 # Revision 1.6 2001/07/29 04:06:42 richard
1066 # Fixed problem in link display when Link value is None.
1067 #
1068 # Revision 1.5 2001/07/28 08:17:09 richard
1069 # fixed use of stylesheet
1070 #
1071 # Revision 1.4 2001/07/28 07:59:53 richard
1072 # Replaced errno integers with their module values.
1073 # De-tabbed templatebuilder.py
1074 #
1075 # Revision 1.3 2001/07/25 03:39:47 richard
1076 # Hrm - displaying links to classes that don't specify a key property. I've
1077 # got it defaulting to 'name', then 'title' and then a "random" property (first
1078 # one returned by getprops().keys().
1079 # Needs to be moved onto the Class I think...
1080 #
1081 # Revision 1.2 2001/07/22 12:09:32 richard
1082 # Final commit of Grande Splite
1083 #
1084 # Revision 1.1 2001/07/22 11:58:35 richard
1085 # More Grande Splite
1086 #
1087 #
1088 # vim: set filetype=python ts=4 sw=4 et si