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