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