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