8892dd9d5f21dad012883c7264000eae5ee7afdc
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.51 2002-01-10 10:02:15 grubert 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(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" value="Submit Changes">')
472 elif self.form is not None:
473 return _('<input type="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.templates = templates
510 self.classname = classname
512 # derived
513 self.db = self.client.db
514 self.cl = self.db.classes[self.classname]
515 self.properties = self.cl.getprops()
517 TemplateFunctions.__init__(self)
519 col_re=re.compile(r'<property\s+name="([^>]+)">')
520 def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
521 show_display_form=1, nodeids=None, show_customization=1):
522 self.filterspec = filterspec
524 w = self.client.write
526 # get the filter template
527 try:
528 filter_template = open(os.path.join(self.templates,
529 self.classname+'.filter')).read()
530 all_filters = self.col_re.findall(filter_template)
531 except IOError, error:
532 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
533 filter_template = None
534 all_filters = []
536 # XXX deviate from spec here ...
537 # load the index section template and figure the default columns from it
538 template = open(os.path.join(self.templates,
539 self.classname+'.index')).read()
540 all_columns = self.col_re.findall(template)
541 if not columns:
542 columns = []
543 for name in all_columns:
544 columns.append(name)
545 else:
546 # re-sort columns to be the same order as all_columns
547 l = []
548 for name in all_columns:
549 if name in columns:
550 l.append(name)
551 columns = l
553 # display the filter section
554 if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
555 self.client.FILTER_POSITION in ('top and bottom', 'top')):
556 w('<form action="index">\n')
557 self.filter_section(filter_template, filter, columns, group,
558 all_filters, all_columns, show_customization)
559 # make sure that the sorting doesn't get lost either
560 if sort:
561 w('<input type="hidden" name=":sort" value="%s">'%
562 ','.join(sort))
563 w('</form>\n')
566 # now display the index section
567 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
568 w('<tr class="list-header">\n')
569 for name in columns:
570 cname = name.capitalize()
571 if show_display_form:
572 sb = self.sortby(name, filterspec, columns, filter, group, sort)
573 anchor = "%s?%s"%(self.classname, sb)
574 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
575 anchor, cname))
576 else:
577 w('<td><span class="list-header">%s</span></td>\n'%cname)
578 w('</tr>\n')
580 # this stuff is used for group headings - optimise the group names
581 old_group = None
582 group_names = []
583 if group:
584 for name in group:
585 if name[0] == '-': group_names.append(name[1:])
586 else: group_names.append(name)
588 # now actually loop through all the nodes we get from the filter and
589 # apply the template
590 if nodeids is None:
591 nodeids = self.cl.filter(filterspec, sort, group)
592 for nodeid in nodeids:
593 # check for a group heading
594 if group_names:
595 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
596 if this_group != old_group:
597 l = []
598 for name in group_names:
599 prop = self.properties[name]
600 if isinstance(prop, hyperdb.Link):
601 group_cl = self.db.classes[prop.classname]
602 key = group_cl.getkey()
603 value = self.cl.get(nodeid, name)
604 if value is None:
605 l.append(_('[unselected %(classname)s]')%{
606 'classname': prop.classname})
607 else:
608 l.append(group_cl.get(self.cl.get(nodeid,
609 name), key))
610 elif isinstance(prop, hyperdb.Multilink):
611 group_cl = self.db.classes[prop.classname]
612 key = group_cl.getkey()
613 for value in self.cl.get(nodeid, name):
614 l.append(group_cl.get(value, key))
615 else:
616 value = self.cl.get(nodeid, name, _('[no value]'))
617 if value is None:
618 value = _('[empty %(name)s]')%locals()
619 else:
620 value = str(value)
621 l.append(value)
622 w('<tr class="section-bar">'
623 '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
624 len(columns), ', '.join(l)))
625 old_group = this_group
627 # display this node's row
628 replace = IndexTemplateReplace(self.globals, locals(), columns)
629 self.nodeid = nodeid
630 w(replace.go(template))
631 self.nodeid = None
633 w('</table>')
635 # display the filter section
636 if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
637 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
638 w('<form action="index">\n')
639 self.filter_section(filter_template, filter, columns, group,
640 all_filters, all_columns, show_customization)
641 # make sure that the sorting doesn't get lost either
642 if sort:
643 w('<input type="hidden" name=":sort" value="%s">'%
644 ','.join(sort))
645 w('</form>\n')
648 def filter_section(self, template, filter, columns, group, all_filters,
649 all_columns, show_customization):
651 w = self.client.write
653 # wrap the template in a single table to ensure the whole widget
654 # is displayed at once
655 w('<table><tr><td>')
657 if template and filter:
658 # display the filter section
659 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
660 w('<tr class="location-bar">')
661 w(_(' <th align="left" colspan="2">Filter specification...</th>'))
662 w('</tr>')
663 replace = IndexTemplateReplace(self.globals, locals(), filter)
664 w(replace.go(template))
665 w('<tr class="location-bar"><td width="1%%"> </td>')
666 w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
667 w('</table>')
669 # now add in the filter/columns/group/etc config table form
670 w('<input type="hidden" name="show_customization" value="%s">' %
671 show_customization )
672 w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
673 names = []
674 for name in self.properties.keys():
675 if name in all_filters or name in all_columns:
676 names.append(name)
677 if show_customization:
678 action = '-'
679 else:
680 action = '+'
681 # hide the values for filters, columns and grouping in the form
682 # if the customization widget is not visible
683 for name in names:
684 if all_filters and name in filter:
685 w('<input type="hidden" name=":filter" value="%s">' % name)
686 if all_columns and name in columns:
687 w('<input type="hidden" name=":columns" value="%s">' % name)
688 if all_columns and name in group:
689 w('<input type="hidden" name=":group" value="%s">' % name)
691 # TODO: The widget style can go into the stylesheet
692 w(_('<th align="left" colspan=%s>'
693 '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s"> View '
694 'customisation...</th></tr>\n')%(len(names)+1, action))
696 if not show_customization:
697 w('</table>\n')
698 return
700 w('<tr class="location-bar"><th> </th>')
701 for name in names:
702 w('<th>%s</th>'%name.capitalize())
703 w('</tr>\n')
705 # Filter
706 if all_filters:
707 w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
708 for name in names:
709 if name not in all_filters:
710 w('<td> </td>')
711 continue
712 if name in filter: checked=' checked'
713 else: checked=''
714 w('<td align=middle>\n')
715 w(' <input type="checkbox" name=":filter" value="%s" '
716 '%s></td>\n'%(name, checked))
717 w('</tr>\n')
719 # Columns
720 if all_columns:
721 w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
722 for name in names:
723 if name not in all_columns:
724 w('<td> </td>')
725 continue
726 if name in columns: checked=' checked'
727 else: checked=''
728 w('<td align=middle>\n')
729 w(' <input type="checkbox" name=":columns" value="%s"'
730 '%s></td>\n'%(name, checked))
731 w('</tr>\n')
733 # Grouping
734 w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
735 for name in names:
736 prop = self.properties[name]
737 if name not in all_columns:
738 w('<td> </td>')
739 continue
740 if name in group: checked=' checked'
741 else: checked=''
742 w('<td align=middle>\n')
743 w(' <input type="checkbox" name=":group" value="%s"'
744 '%s></td>\n'%(name, checked))
745 w('</tr>\n')
747 w('<tr class="location-bar"><td width="1%"> </td>')
748 w('<td colspan="%s">'%len(names))
749 w(_('<input type="submit" name="action" value="Redisplay"></td>'))
750 w('</tr>\n')
751 w('</table>\n')
753 # and the outer table
754 w('</td></tr></table>')
757 def sortby(self, sort_name, filterspec, columns, filter, group, sort):
758 l = []
759 w = l.append
760 for k, v in filterspec.items():
761 k = urllib.quote(k)
762 if type(v) == type([]):
763 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
764 else:
765 w('%s=%s'%(k, urllib.quote(v)))
766 if columns:
767 w(':columns=%s'%','.join(map(urllib.quote, columns)))
768 if filter:
769 w(':filter=%s'%','.join(map(urllib.quote, filter)))
770 if group:
771 w(':group=%s'%','.join(map(urllib.quote, group)))
772 m = []
773 s_dir = ''
774 for name in sort:
775 dir = name[0]
776 if dir == '-':
777 name = name[1:]
778 else:
779 dir = ''
780 if sort_name == name:
781 if dir == '-':
782 s_dir = ''
783 else:
784 s_dir = '-'
785 else:
786 m.append(dir+urllib.quote(name))
787 m.insert(0, s_dir+urllib.quote(sort_name))
788 # so things don't get completely out of hand, limit the sort to
789 # two columns
790 w(':sort=%s'%','.join(m[:2]))
791 return '&'.join(l)
793 #
794 # ITEM TEMPLATES
795 #
796 class ItemTemplateReplace:
797 def __init__(self, globals, locals, cl, nodeid):
798 self.globals = globals
799 self.locals = locals
800 self.cl = cl
801 self.nodeid = nodeid
803 replace=re.compile(
804 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
805 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
806 def go(self, text):
807 return self.replace.sub(self, text)
809 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
810 if m.group('name'):
811 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
812 replace = ItemTemplateReplace(self.globals, {}, self.cl,
813 self.nodeid)
814 return replace.go(m.group('text'))
815 else:
816 return ''
817 if m.group('display'):
818 command = m.group('command')
819 return eval(command, self.globals, self.locals)
820 print '*** unhandled match', m.groupdict()
823 class ItemTemplate(TemplateFunctions):
824 def __init__(self, client, templates, classname):
825 self.client = client
826 self.templates = templates
827 self.classname = classname
829 # derived
830 self.db = self.client.db
831 self.cl = self.db.classes[self.classname]
832 self.properties = self.cl.getprops()
834 TemplateFunctions.__init__(self)
836 def render(self, nodeid):
837 self.nodeid = nodeid
839 if (self.properties.has_key('type') and
840 self.properties.has_key('content')):
841 pass
842 # XXX we really want to return this as a downloadable...
843 # currently I handle this at a higher level by detecting 'file'
844 # designators...
846 w = self.client.write
847 w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
848 self.classname, nodeid))
849 s = open(os.path.join(self.templates, self.classname+'.item')).read()
850 replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
851 w(replace.go(s))
852 w('</form>')
855 class NewItemTemplate(TemplateFunctions):
856 def __init__(self, client, templates, classname):
857 self.client = client
858 self.templates = templates
859 self.classname = classname
861 # derived
862 self.db = self.client.db
863 self.cl = self.db.classes[self.classname]
864 self.properties = self.cl.getprops()
866 TemplateFunctions.__init__(self)
868 def render(self, form):
869 self.form = form
870 w = self.client.write
871 c = self.classname
872 try:
873 s = open(os.path.join(self.templates, c+'.newitem')).read()
874 except IOError:
875 s = open(os.path.join(self.templates, c+'.item')).read()
876 w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
877 for key in form.keys():
878 if key[0] == ':':
879 value = form[key].value
880 if type(value) != type([]): value = [value]
881 for value in value:
882 w('<input type="hidden" name="%s" value="%s">'%(key, value))
883 replace = ItemTemplateReplace(self.globals, locals(), None, None)
884 w(replace.go(s))
885 w('</form>')
887 #
888 # $Log: not supported by cvs2svn $
889 # Revision 1.50 2002/01/05 02:35:10 richard
890 # I18N'ification
891 #
892 # Revision 1.49 2001/12/20 15:43:01 rochecompaan
893 # Features added:
894 # . Multilink properties are now displayed as comma separated values in
895 # a textbox
896 # . The add user link is now only visible to the admin user
897 # . Modified the mail gateway to reject submissions from unknown
898 # addresses if ANONYMOUS_ACCESS is denied
899 #
900 # Revision 1.48 2001/12/20 06:13:24 rochecompaan
901 # Bugs fixed:
902 # . Exception handling in hyperdb for strings-that-look-like numbers got
903 # lost somewhere
904 # . Internet Explorer submits full path for filename - we now strip away
905 # the path
906 # Features added:
907 # . Link and multilink properties are now displayed sorted in the cgi
908 # interface
909 #
910 # Revision 1.47 2001/11/26 22:55:56 richard
911 # Feature:
912 # . Added INSTANCE_NAME to configuration - used in web and email to identify
913 # the instance.
914 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
915 # signature info in e-mails.
916 # . Some more flexibility in the mail gateway and more error handling.
917 # . Login now takes you to the page you back to the were denied access to.
918 #
919 # Fixed:
920 # . Lots of bugs, thanks Roché and others on the devel mailing list!
921 #
922 # Revision 1.46 2001/11/24 00:53:12 jhermann
923 # "except:" is bad, bad , bad!
924 #
925 # Revision 1.45 2001/11/22 15:46:42 jhermann
926 # Added module docstrings to all modules.
927 #
928 # Revision 1.44 2001/11/21 23:35:45 jhermann
929 # Added globbing for win32, and sample marking in a 2nd file to test it
930 #
931 # Revision 1.43 2001/11/21 04:04:43 richard
932 # *sigh* more missing value handling
933 #
934 # Revision 1.42 2001/11/21 03:40:54 richard
935 # more new property handling
936 #
937 # Revision 1.41 2001/11/15 10:26:01 richard
938 # . missing "return" in filter_section (thanks Roch'e Compaan)
939 #
940 # Revision 1.40 2001/11/03 01:56:51 richard
941 # More HTML compliance fixes. This will probably fix the Netscape problem
942 # too.
943 #
944 # Revision 1.39 2001/11/03 01:43:47 richard
945 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
946 #
947 # Revision 1.38 2001/10/31 06:58:51 richard
948 # Added the wrap="hard" attribute to the textarea of the note field so the
949 # messages wrap sanely.
950 #
951 # Revision 1.37 2001/10/31 06:24:35 richard
952 # Added do_stext to htmltemplate, thanks Brad Clements.
953 #
954 # Revision 1.36 2001/10/28 22:51:38 richard
955 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
956 #
957 # Revision 1.35 2001/10/24 00:04:41 richard
958 # Removed the "infinite authentication loop", thanks Roch'e
959 #
960 # Revision 1.34 2001/10/23 22:56:36 richard
961 # Bugfix in filter "widget" placement, thanks Roch'e
962 #
963 # Revision 1.33 2001/10/23 01:00:18 richard
964 # Re-enabled login and registration access after lopping them off via
965 # disabling access for anonymous users.
966 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
967 # a couple of bugs while I was there. Probably introduced a couple, but
968 # things seem to work OK at the moment.
969 #
970 # Revision 1.32 2001/10/22 03:25:01 richard
971 # Added configuration for:
972 # . anonymous user access and registration (deny/allow)
973 # . filter "widget" location on index page (top, bottom, both)
974 # Updated some documentation.
975 #
976 # Revision 1.31 2001/10/21 07:26:35 richard
977 # feature #473127: Filenames. I modified the file.index and htmltemplate
978 # source so that the filename is used in the link and the creation
979 # information is displayed.
980 #
981 # Revision 1.30 2001/10/21 04:44:50 richard
982 # bug #473124: UI inconsistency with Link fields.
983 # This also prompted me to fix a fairly long-standing usability issue -
984 # that of being able to turn off certain filters.
985 #
986 # Revision 1.29 2001/10/21 00:17:56 richard
987 # CGI interface view customisation section may now be hidden (patch from
988 # Roch'e Compaan.)
989 #
990 # Revision 1.28 2001/10/21 00:00:16 richard
991 # Fixed Checklist function - wasn't always working on a list.
992 #
993 # Revision 1.27 2001/10/20 12:13:44 richard
994 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
995 #
996 # Revision 1.26 2001/10/14 10:55:00 richard
997 # Handle empty strings in HTML template Link function
998 #
999 # Revision 1.25 2001/10/09 07:25:59 richard
1000 # Added the Password property type. See "pydoc roundup.password" for
1001 # implementation details. Have updated some of the documentation too.
1002 #
1003 # Revision 1.24 2001/09/27 06:45:58 richard
1004 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1005 # on the plain() template function to escape the text for HTML.
1006 #
1007 # Revision 1.23 2001/09/10 09:47:18 richard
1008 # Fixed bug in the generation of links to Link/Multilink in indexes.
1009 # (thanks Hubert Hoegl)
1010 # Added AssignedTo to the "classic" schema's item page.
1011 #
1012 # Revision 1.22 2001/08/30 06:01:17 richard
1013 # Fixed missing import in mailgw :(
1014 #
1015 # Revision 1.21 2001/08/16 07:34:59 richard
1016 # better CGI text searching - but hidden filter fields are disappearing...
1017 #
1018 # Revision 1.20 2001/08/15 23:43:18 richard
1019 # Fixed some isFooTypes that I missed.
1020 # Refactored some code in the CGI code.
1021 #
1022 # Revision 1.19 2001/08/12 06:32:36 richard
1023 # using isinstance(blah, Foo) now instead of isFooType
1024 #
1025 # Revision 1.18 2001/08/07 00:24:42 richard
1026 # stupid typo
1027 #
1028 # Revision 1.17 2001/08/07 00:15:51 richard
1029 # Added the copyright/license notice to (nearly) all files at request of
1030 # Bizar Software.
1031 #
1032 # Revision 1.16 2001/08/01 03:52:23 richard
1033 # Checklist was using wrong name.
1034 #
1035 # Revision 1.15 2001/07/30 08:12:17 richard
1036 # Added time logging and file uploading to the templates.
1037 #
1038 # Revision 1.14 2001/07/30 06:17:45 richard
1039 # Features:
1040 # . Added ability for cgi newblah forms to indicate that the new node
1041 # should be linked somewhere.
1042 # Fixed:
1043 # . Fixed the agument handling for the roundup-admin find command.
1044 # . Fixed handling of summary when no note supplied for newblah. Again.
1045 # . Fixed detection of no form in htmltemplate Field display.
1046 #
1047 # Revision 1.13 2001/07/30 02:37:53 richard
1048 # Temporary measure until we have decent schema migration.
1049 #
1050 # Revision 1.12 2001/07/30 01:24:33 richard
1051 # Handles new node display now.
1052 #
1053 # Revision 1.11 2001/07/29 09:31:35 richard
1054 # oops
1055 #
1056 # Revision 1.10 2001/07/29 09:28:23 richard
1057 # Fixed sorting by clicking on column headings.
1058 #
1059 # Revision 1.9 2001/07/29 08:27:40 richard
1060 # Fixed handling of passed-in values in form elements (ie. during a
1061 # drill-down)
1062 #
1063 # Revision 1.8 2001/07/29 07:01:39 richard
1064 # Added vim command to all source so that we don't get no steenkin' tabs :)
1065 #
1066 # Revision 1.7 2001/07/29 05:36:14 richard
1067 # Cleanup of the link label generation.
1068 #
1069 # Revision 1.6 2001/07/29 04:06:42 richard
1070 # Fixed problem in link display when Link value is None.
1071 #
1072 # Revision 1.5 2001/07/28 08:17:09 richard
1073 # fixed use of stylesheet
1074 #
1075 # Revision 1.4 2001/07/28 07:59:53 richard
1076 # Replaced errno integers with their module values.
1077 # De-tabbed templatebuilder.py
1078 #
1079 # Revision 1.3 2001/07/25 03:39:47 richard
1080 # Hrm - displaying links to classes that don't specify a key property. I've
1081 # got it defaulting to 'name', then 'title' and then a "random" property (first
1082 # one returned by getprops().keys().
1083 # Needs to be moved onto the Class I think...
1084 #
1085 # Revision 1.2 2001/07/22 12:09:32 richard
1086 # Final commit of Grande Splite
1087 #
1088 # Revision 1.1 2001/07/22 11:58:35 richard
1089 # More Grande Splite
1090 #
1091 #
1092 # vim: set filetype=python ts=4 sw=4 et si