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