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