042e7e53ea63da840887cb9f7ba906c71390e1d6
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.47 2001-11-26 22:55:56 richard 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 # wrap the template in a single table to ensure the whole widget
638 # is displayed at once
639 w('<table><tr><td>')
641 if template and filter:
642 # display the filter section
643 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
644 w('<tr class="location-bar">')
645 w(' <th align="left" colspan="2">Filter specification...</th>')
646 w('</tr>')
647 replace = IndexTemplateReplace(self.globals, locals(), filter)
648 w(replace.go(template))
649 w('<tr class="location-bar"><td width="1%%"> </td>')
650 w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
651 w('</table>')
653 # now add in the filter/columns/group/etc config table form
654 w('<input type="hidden" name="show_customization" value="%s">' %
655 show_customization )
656 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
657 names = []
658 for name in self.properties.keys():
659 if name in all_filters or name in all_columns:
660 names.append(name)
661 if show_customization:
662 action = '-'
663 else:
664 action = '+'
665 # hide the values for filters, columns and grouping in the form
666 # if the customization widget is not visible
667 for name in names:
668 if all_filters and name in filter:
669 w('<input type="hidden" name=":filter" value="%s">' % name)
670 if all_columns and name in columns:
671 w('<input type="hidden" name=":columns" value="%s">' % name)
672 if all_columns and name in group:
673 w('<input type="hidden" name=":group" value="%s">' % name)
675 # TODO: The widget style can go into the stylesheet
676 w('<th align="left" colspan=%s>'
677 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
678 'customisation...</th></tr>\n'%(len(names)+1, action))
680 if not show_customization:
681 w('</table>\n')
682 return
684 w('<tr class="location-bar"><th> </th>')
685 for name in names:
686 w('<th>%s</th>'%name.capitalize())
687 w('</tr>\n')
689 # Filter
690 if all_filters:
691 w('<tr><th width="1%" align=right class="location-bar">'
692 'Filters</th>\n')
693 for name in names:
694 if name not in all_filters:
695 w('<td> </td>')
696 continue
697 if name in filter: checked=' checked'
698 else: checked=''
699 w('<td align=middle>\n')
700 w(' <input type="checkbox" name=":filter" value="%s" '
701 '%s></td>\n'%(name, checked))
702 w('</tr>\n')
704 # Columns
705 if all_columns:
706 w('<tr><th width="1%" align=right class="location-bar">'
707 'Columns</th>\n')
708 for name in names:
709 if name not in all_columns:
710 w('<td> </td>')
711 continue
712 if name in columns: checked=' checked'
713 else: checked=''
714 w('<td align=middle>\n')
715 w(' <input type="checkbox" name=":columns" value="%s"'
716 '%s></td>\n'%(name, checked))
717 w('</tr>\n')
719 # Grouping
720 w('<tr><th width="1%" align=right class="location-bar">'
721 'Grouping</th>\n')
722 for name in names:
723 prop = self.properties[name]
724 if name not in all_columns:
725 w('<td> </td>')
726 continue
727 if name in group: checked=' checked'
728 else: checked=''
729 w('<td align=middle>\n')
730 w(' <input type="checkbox" name=":group" value="%s"'
731 '%s></td>\n'%(name, checked))
732 w('</tr>\n')
734 w('<tr class="location-bar"><td width="1%"> </td>')
735 w('<td colspan="%s">'%len(names))
736 w('<input type="submit" name="action" value="Redisplay"></td>')
737 w('</tr>\n')
738 w('</table>\n')
740 # and the outer table
741 w('</td></tr></table>')
744 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
745 l = []
746 w = l.append
747 for k, v in filterspec.items():
748 k = urllib.quote(k)
749 if type(v) == type([]):
750 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
751 else:
752 w('%s=%s'%(k, urllib.quote(v)))
753 if columns:
754 w(':columns=%s'%','.join(map(urllib.quote, columns)))
755 if filter:
756 w(':filter=%s'%','.join(map(urllib.quote, filter)))
757 if group:
758 w(':group=%s'%','.join(map(urllib.quote, group)))
759 m = []
760 s_dir = ''
761 for name in sort:
762 dir = name[0]
763 if dir == '-':
764 name = name[1:]
765 else:
766 dir = ''
767 if sort_name == name:
768 if dir == '-':
769 s_dir = ''
770 else:
771 s_dir = '-'
772 else:
773 m.append(dir+urllib.quote(name))
774 m.insert(0, s_dir+urllib.quote(sort_name))
775 # so things don't get completely out of hand, limit the sort to
776 # two columns
777 w(':sort=%s'%','.join(m[:2]))
778 return '&'.join(l)
780 #
781 # ITEM TEMPLATES
782 #
783 class ItemTemplateReplace:
784 def __init__(self, globals, locals, cl, nodeid):
785 self.globals = globals
786 self.locals = locals
787 self.cl = cl
788 self.nodeid = nodeid
790 replace=re.compile(
791 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
792 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
793 def go(self, text):
794 return self.replace.sub(self, text)
796 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
797 if m.group('name'):
798 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
799 replace = ItemTemplateReplace(self.globals, {}, self.cl,
800 self.nodeid)
801 return replace.go(m.group('text'))
802 else:
803 return ''
804 if m.group('display'):
805 command = m.group('command')
806 return eval(command, self.globals, self.locals)
807 print '*** unhandled match', m.groupdict()
810 class ItemTemplate(TemplateFunctions):
811 def __init__(self, client, templates, classname):
812 self.client = client
813 self.templates = templates
814 self.classname = classname
816 # derived
817 self.db = self.client.db
818 self.cl = self.db.classes[self.classname]
819 self.properties = self.cl.getprops()
821 TemplateFunctions.__init__(self)
823 def render(self, nodeid):
824 self.nodeid = nodeid
826 if (self.properties.has_key('type') and
827 self.properties.has_key('content')):
828 pass
829 # XXX we really want to return this as a downloadable...
830 # currently I handle this at a higher level by detecting 'file'
831 # designators...
833 w = self.client.write
834 w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
835 self.classname, nodeid))
836 s = open(os.path.join(self.templates, self.classname+'.item')).read()
837 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
838 w(replace.go(s))
839 w('</form>')
842 class NewItemTemplate(TemplateFunctions):
843 def __init__(self, client, templates, classname):
844 self.client = client
845 self.templates = templates
846 self.classname = classname
848 # derived
849 self.db = self.client.db
850 self.cl = self.db.classes[self.classname]
851 self.properties = self.cl.getprops()
853 TemplateFunctions.__init__(self)
855 def render(self, form):
856 self.form = form
857 w = self.client.write
858 c = self.classname
859 try:
860 s = open(os.path.join(self.templates, c+'.newitem')).read()
861 except IOError:
862 s = open(os.path.join(self.templates, c+'.item')).read()
863 w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
864 for key in form.keys():
865 if key[0] == ':':
866 value = form[key].value
867 if type(value) != type([]): value = [value]
868 for value in value:
869 w('<input type="hidden" name="%s" value="%s">'%(key, value))
870 replace = ItemTemplateReplace(self.globals, locals(), None, None)
871 w(replace.go(s))
872 w('</form>')
874 #
875 # $Log: not supported by cvs2svn $
876 # Revision 1.46 2001/11/24 00:53:12 jhermann
877 # "except:" is bad, bad , bad!
878 #
879 # Revision 1.45 2001/11/22 15:46:42 jhermann
880 # Added module docstrings to all modules.
881 #
882 # Revision 1.44 2001/11/21 23:35:45 jhermann
883 # Added globbing for win32, and sample marking in a 2nd file to test it
884 #
885 # Revision 1.43 2001/11/21 04:04:43 richard
886 # *sigh* more missing value handling
887 #
888 # Revision 1.42 2001/11/21 03:40:54 richard
889 # more new property handling
890 #
891 # Revision 1.41 2001/11/15 10:26:01 richard
892 # . missing "return" in filter_section (thanks Roch'e Compaan)
893 #
894 # Revision 1.40 2001/11/03 01:56:51 richard
895 # More HTML compliance fixes. This will probably fix the Netscape problem
896 # too.
897 #
898 # Revision 1.39 2001/11/03 01:43:47 richard
899 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
900 #
901 # Revision 1.38 2001/10/31 06:58:51 richard
902 # Added the wrap="hard" attribute to the textarea of the note field so the
903 # messages wrap sanely.
904 #
905 # Revision 1.37 2001/10/31 06:24:35 richard
906 # Added do_stext to htmltemplate, thanks Brad Clements.
907 #
908 # Revision 1.36 2001/10/28 22:51:38 richard
909 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
910 #
911 # Revision 1.35 2001/10/24 00:04:41 richard
912 # Removed the "infinite authentication loop", thanks Roch'e
913 #
914 # Revision 1.34 2001/10/23 22:56:36 richard
915 # Bugfix in filter "widget" placement, thanks Roch'e
916 #
917 # Revision 1.33 2001/10/23 01:00:18 richard
918 # Re-enabled login and registration access after lopping them off via
919 # disabling access for anonymous users.
920 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
921 # a couple of bugs while I was there. Probably introduced a couple, but
922 # things seem to work OK at the moment.
923 #
924 # Revision 1.32 2001/10/22 03:25:01 richard
925 # Added configuration for:
926 # . anonymous user access and registration (deny/allow)
927 # . filter "widget" location on index page (top, bottom, both)
928 # Updated some documentation.
929 #
930 # Revision 1.31 2001/10/21 07:26:35 richard
931 # feature #473127: Filenames. I modified the file.index and htmltemplate
932 # source so that the filename is used in the link and the creation
933 # information is displayed.
934 #
935 # Revision 1.30 2001/10/21 04:44:50 richard
936 # bug #473124: UI inconsistency with Link fields.
937 # This also prompted me to fix a fairly long-standing usability issue -
938 # that of being able to turn off certain filters.
939 #
940 # Revision 1.29 2001/10/21 00:17:56 richard
941 # CGI interface view customisation section may now be hidden (patch from
942 # Roch'e Compaan.)
943 #
944 # Revision 1.28 2001/10/21 00:00:16 richard
945 # Fixed Checklist function - wasn't always working on a list.
946 #
947 # Revision 1.27 2001/10/20 12:13:44 richard
948 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
949 #
950 # Revision 1.26 2001/10/14 10:55:00 richard
951 # Handle empty strings in HTML template Link function
952 #
953 # Revision 1.25 2001/10/09 07:25:59 richard
954 # Added the Password property type. See "pydoc roundup.password" for
955 # implementation details. Have updated some of the documentation too.
956 #
957 # Revision 1.24 2001/09/27 06:45:58 richard
958 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
959 # on the plain() template function to escape the text for HTML.
960 #
961 # Revision 1.23 2001/09/10 09:47:18 richard
962 # Fixed bug in the generation of links to Link/Multilink in indexes.
963 # (thanks Hubert Hoegl)
964 # Added AssignedTo to the "classic" schema's item page.
965 #
966 # Revision 1.22 2001/08/30 06:01:17 richard
967 # Fixed missing import in mailgw :(
968 #
969 # Revision 1.21 2001/08/16 07:34:59 richard
970 # better CGI text searching - but hidden filter fields are disappearing...
971 #
972 # Revision 1.20 2001/08/15 23:43:18 richard
973 # Fixed some isFooTypes that I missed.
974 # Refactored some code in the CGI code.
975 #
976 # Revision 1.19 2001/08/12 06:32:36 richard
977 # using isinstance(blah, Foo) now instead of isFooType
978 #
979 # Revision 1.18 2001/08/07 00:24:42 richard
980 # stupid typo
981 #
982 # Revision 1.17 2001/08/07 00:15:51 richard
983 # Added the copyright/license notice to (nearly) all files at request of
984 # Bizar Software.
985 #
986 # Revision 1.16 2001/08/01 03:52:23 richard
987 # Checklist was using wrong name.
988 #
989 # Revision 1.15 2001/07/30 08:12:17 richard
990 # Added time logging and file uploading to the templates.
991 #
992 # Revision 1.14 2001/07/30 06:17:45 richard
993 # Features:
994 # . Added ability for cgi newblah forms to indicate that the new node
995 # should be linked somewhere.
996 # Fixed:
997 # . Fixed the agument handling for the roundup-admin find command.
998 # . Fixed handling of summary when no note supplied for newblah. Again.
999 # . Fixed detection of no form in htmltemplate Field display.
1000 #
1001 # Revision 1.13 2001/07/30 02:37:53 richard
1002 # Temporary measure until we have decent schema migration.
1003 #
1004 # Revision 1.12 2001/07/30 01:24:33 richard
1005 # Handles new node display now.
1006 #
1007 # Revision 1.11 2001/07/29 09:31:35 richard
1008 # oops
1009 #
1010 # Revision 1.10 2001/07/29 09:28:23 richard
1011 # Fixed sorting by clicking on column headings.
1012 #
1013 # Revision 1.9 2001/07/29 08:27:40 richard
1014 # Fixed handling of passed-in values in form elements (ie. during a
1015 # drill-down)
1016 #
1017 # Revision 1.8 2001/07/29 07:01:39 richard
1018 # Added vim command to all source so that we don't get no steenkin' tabs :)
1019 #
1020 # Revision 1.7 2001/07/29 05:36:14 richard
1021 # Cleanup of the link label generation.
1022 #
1023 # Revision 1.6 2001/07/29 04:06:42 richard
1024 # Fixed problem in link display when Link value is None.
1025 #
1026 # Revision 1.5 2001/07/28 08:17:09 richard
1027 # fixed use of stylesheet
1028 #
1029 # Revision 1.4 2001/07/28 07:59:53 richard
1030 # Replaced errno integers with their module values.
1031 # De-tabbed templatebuilder.py
1032 #
1033 # Revision 1.3 2001/07/25 03:39:47 richard
1034 # Hrm - displaying links to classes that don't specify a key property. I've
1035 # got it defaulting to 'name', then 'title' and then a "random" property (first
1036 # one returned by getprops().keys().
1037 # Needs to be moved onto the Class I think...
1038 #
1039 # Revision 1.2 2001/07/22 12:09:32 richard
1040 # Final commit of Grande Splite
1041 #
1042 # Revision 1.1 2001/07/22 11:58:35 richard
1043 # More Grande Splite
1044 #
1045 #
1046 # vim: set filetype=python ts=4 sw=4 et si