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