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