1 # $Id: template.py,v 1.3 2001-07-19 05:52:22 anthonybaxter Exp $
3 import os, re, StringIO, urllib
5 import hyperdb, date
7 class Base:
8 def __init__(self, db, classname, nodeid=None, form=None):
9 self.db, self.classname, self.nodeid = db, classname, nodeid
10 self.form = form
11 self.cl = self.db.classes[self.classname]
12 self.properties = self.cl.getprops()
14 class Plain(Base):
15 ''' display a String property directly;
17 display a Date property in a specified time zone with an option to
18 omit the time from the date stamp;
20 for a Link or Multilink property, display the key strings of the
21 linked nodes (or the ids if the linked class has no key property)
22 '''
23 def __call__(self, property):
24 if not self.nodeid and self.form is None:
25 return '[Field: not called from item]'
26 propclass = self.properties[property]
27 if self.nodeid:
28 value = self.cl.get(self.nodeid, property)
29 else:
30 # TODO: pull the value from the form
31 if propclass.isMultilinkType: value = []
32 else: value = ''
33 if propclass.isStringType:
34 if value is None: value = ''
35 else: value = str(value)
36 elif propclass.isDateType:
37 value = str(value)
38 elif propclass.isIntervalType:
39 value = str(value)
40 elif propclass.isLinkType:
41 linkcl = self.db.classes[propclass.classname]
42 if value: value = str(linkcl.get(value, linkcl.getkey()))
43 else: value = '[unselected]'
44 elif propclass.isMultilinkType:
45 linkcl = self.db.classes[propclass.classname]
46 k = linkcl.getkey()
47 value = ', '.join([linkcl.get(i, k) for i in value])
48 else:
49 s = 'Plain: bad propclass "%s"'%propclass
50 return value
52 class Field(Base):
53 ''' display a property like the plain displayer, but in a text field
54 to be edited
55 '''
56 def __call__(self, property, size=None, height=None, showid=0):
57 if not self.nodeid and self.form is None:
58 return '[Field: not called from item]'
59 propclass = self.properties[property]
60 if self.nodeid:
61 value = self.cl.get(self.nodeid, property)
62 else:
63 # TODO: pull the value from the form
64 if propclass.isMultilinkType: value = []
65 else: value = ''
66 if (propclass.isStringType or propclass.isDateType or
67 propclass.isIntervalType):
68 size = size or 30
69 if value is None:
70 value = ''
71 s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
72 elif propclass.isLinkType:
73 linkcl = self.db.classes[propclass.classname]
74 l = ['<select name="%s">'%property]
75 k = linkcl.getkey()
76 for optionid in linkcl.list():
77 option = linkcl.get(optionid, k)
78 s = ''
79 if optionid == value:
80 s = 'selected '
81 if showid:
82 lab = '%s%s: %s'%(propclass.classname, optionid, option)
83 else:
84 lab = option
85 if size is not None and len(lab) > size:
86 lab = lab[:size-3] + '...'
87 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
88 l.append('</select>')
89 s = '\n'.join(l)
90 elif propclass.isMultilinkType:
91 linkcl = self.db.classes[propclass.classname]
92 list = linkcl.list()
93 height = height or min(len(list), 7)
94 l = ['<select multiple name="%s" size="%s">'%(property, height)]
95 k = linkcl.getkey()
96 for optionid in list:
97 option = linkcl.get(optionid, k)
98 s = ''
99 if optionid in value:
100 s = 'selected '
101 if showid:
102 lab = '%s%s: %s'%(propclass.classname, optionid, option)
103 else:
104 lab = option
105 if size is not None and len(lab) > size:
106 lab = lab[:size-3] + '...'
107 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
108 l.append('</select>')
109 s = '\n'.join(l)
110 else:
111 s = 'Plain: bad propclass "%s"'%propclass
112 return s
114 class Menu(Base):
115 ''' for a Link property, display a menu of the available choices
116 '''
117 def __call__(self, property, size=None, height=None, showid=0):
118 propclass = self.properties[property]
119 if self.nodeid:
120 value = self.cl.get(self.nodeid, property)
121 else:
122 # TODO: pull the value from the form
123 if propclass.isMultilinkType: value = []
124 else: value = None
125 if propclass.isLinkType:
126 linkcl = self.db.classes[propclass.classname]
127 l = ['<select name="%s">'%property]
128 k = linkcl.getkey()
129 for optionid in linkcl.list():
130 option = linkcl.get(optionid, k)
131 s = ''
132 if optionid == value:
133 s = 'selected '
134 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
135 l.append('</select>')
136 return '\n'.join(l)
137 if propclass.isMultilinkType:
138 linkcl = self.db.classes[propclass.classname]
139 list = linkcl.list()
140 height = height or min(len(list), 7)
141 l = ['<select multiple name="%s" size="%s">'%(property, height)]
142 k = linkcl.getkey()
143 for optionid in list:
144 option = linkcl.get(optionid, k)
145 s = ''
146 if optionid in value:
147 s = 'selected '
148 if showid:
149 lab = '%s%s: %s'%(propclass.classname, optionid, option)
150 else:
151 lab = option
152 if size is not None and len(lab) > size:
153 lab = lab[:size-3] + '...'
154 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
155 l.append('</select>')
156 return '\n'.join(l)
157 return '[Menu: not a link]'
159 #XXX deviates from spec
160 class Link(Base):
161 ''' for a Link or Multilink property, display the names of the linked
162 nodes, hyperlinked to the item views on those nodes
163 for other properties, link to this node with the property as the text
164 '''
165 def __call__(self, property=None, **args):
166 if not self.nodeid and self.form is None:
167 return '[Link: not called from item]'
168 propclass = self.properties[property]
169 if self.nodeid:
170 value = self.cl.get(self.nodeid, property)
171 else:
172 if propclass.isMultilinkType: value = []
173 else: value = ''
174 if propclass.isLinkType:
175 linkcl = self.db.classes[propclass.classname]
176 linkvalue = linkcl.get(value, k)
177 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
178 if propclass.isMultilinkType:
179 linkcl = self.db.classes[propclass.classname]
180 l = []
181 for value in value:
182 linkvalue = linkcl.get(value, k)
183 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
184 return ', '.join(l)
185 return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
187 class Count(Base):
188 ''' for a Multilink property, display a count of the number of links in
189 the list
190 '''
191 def __call__(self, property, **args):
192 if not self.nodeid:
193 return '[Count: not called from item]'
194 propclass = self.properties[property]
195 value = self.cl.get(self.nodeid, property)
196 if propclass.isMultilinkType:
197 return str(len(value))
198 return '[Count: not a Multilink]'
200 # XXX pretty is definitely new ;)
201 class Reldate(Base):
202 ''' display a Date property in terms of an interval relative to the
203 current date (e.g. "+ 3w", "- 2d").
205 with the 'pretty' flag, make it pretty
206 '''
207 def __call__(self, property, pretty=0):
208 if not self.nodeid and self.form is None:
209 return '[Reldate: not called from item]'
210 propclass = self.properties[property]
211 if not propclass.isDateType:
212 return '[Reldate: not a Date]'
213 if self.nodeid:
214 value = self.cl.get(self.nodeid, property)
215 else:
216 value = date.Date('.')
217 interval = value - date.Date('.')
218 if pretty:
219 if not self.nodeid:
220 return 'now'
221 pretty = interval.pretty()
222 if pretty is None:
223 pretty = value.pretty()
224 return pretty
225 return str(interval)
227 class Download(Base):
228 ''' show a Link("file") or Multilink("file") property using links that
229 allow you to download files
230 '''
231 def __call__(self, property, **args):
232 if not self.nodeid:
233 return '[Download: not called from item]'
234 propclass = self.properties[property]
235 value = self.cl.get(self.nodeid, property)
236 if propclass.isLinkType:
237 linkcl = self.db.classes[propclass.classname]
238 linkvalue = linkcl.get(value, k)
239 return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
240 if propclass.isMultilinkType:
241 linkcl = self.db.classes[propclass.classname]
242 l = []
243 for value in value:
244 linkvalue = linkcl.get(value, k)
245 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
246 return ', '.join(l)
247 return '[Download: not a link]'
250 class Checklist(Base):
251 ''' for a Link or Multilink property, display checkboxes for the available
252 choices to permit filtering
253 '''
254 def __call__(self, property, **args):
255 propclass = self.properties[property]
256 if self.nodeid:
257 value = self.cl.get(self.nodeid, property)
258 else:
259 value = []
260 if propclass.isLinkType or propclass.isMultilinkType:
261 linkcl = self.db.classes[propclass.classname]
262 l = []
263 k = linkcl.getkey()
264 for optionid in linkcl.list():
265 option = linkcl.get(optionid, k)
266 if optionid in value:
267 checked = 'checked'
268 else:
269 checked = ''
270 l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
271 option, checked, propclass.classname, option))
272 return '\n'.join(l)
273 return '[Checklist: not a link]'
275 class Note(Base):
276 ''' display a "note" field, which is a text area for entering a note to
277 go along with a change.
278 '''
279 def __call__(self, rows=5, cols=80):
280 # TODO: pull the value from the form
281 return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
282 cols)
284 # XXX new function
285 class List(Base):
286 ''' list the items specified by property using the standard index for
287 the class
288 '''
289 def __call__(self, property, **args):
290 propclass = self.properties[property]
291 if not propclass.isMultilinkType:
292 return '[List: not a Multilink]'
293 fp = StringIO.StringIO()
294 args['show_display_form'] = 0
295 value = self.cl.get(self.nodeid, property)
296 index(fp, self.db, propclass.classname, nodeids=value,
297 show_display_form=0)
298 return fp.getvalue()
300 # XXX new function
301 class History(Base):
302 ''' list the history of the item
303 '''
304 def __call__(self, **args):
305 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
306 '<tr class="list-header">',
307 '<td><span class="list-item"><strong>Date</strong></span></td>',
308 '<td><span class="list-item"><strong>User</strong></span></td>',
309 '<td><span class="list-item"><strong>Action</strong></span></td>',
310 '<td><span class="list-item"><strong>Args</strong></span></td>']
312 for id, date, user, action, args in self.cl.history(self.nodeid):
313 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
314 date, user, action, args))
315 l.append('</table>')
316 return '\n'.join(l)
318 # XXX new function
319 class Submit(Base):
320 ''' add a submit button for the item
321 '''
322 def __call__(self):
323 if self.nodeid:
324 return '<input type="submit" value="Submit Changes">'
325 elif self.form is not None:
326 return '<input type="submit" value="Submit New Entry">'
327 else:
328 return '[Submit: not called from item]'
331 #
332 # INDEX TEMPLATES
333 #
334 class IndexTemplateReplace:
335 def __init__(self, globals, locals, props):
336 self.globals = globals
337 self.locals = locals
338 self.props = props
340 def go(self, text, replace=re.compile(
341 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
342 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
343 return replace.sub(self, text)
345 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
346 if m.group('name'):
347 if m.group('name') in self.props:
348 text = m.group('text')
349 replace = IndexTemplateReplace(self.globals, {}, self.props)
350 return replace.go(m.group('text'))
351 else:
352 return ''
353 if m.group('display'):
354 command = m.group('command')
355 return eval(command, self.globals, self.locals)
356 print '*** unhandled match', m.groupdict()
358 def sortby(sort_name, columns, filter, sort, group, filterspec):
359 l = []
360 w = l.append
361 for k, v in filterspec.items():
362 k = urllib.quote(k)
363 if type(v) == type([]):
364 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
365 else:
366 w('%s=%s'%(k, urllib.quote(v)))
367 if columns:
368 w(':columns=%s'%','.join(map(urllib.quote, columns)))
369 if filter:
370 w(':filter=%s'%','.join(map(urllib.quote, filter)))
371 if group:
372 w(':group=%s'%','.join(map(urllib.quote, group)))
373 m = []
374 s_dir = ''
375 for name in sort:
376 dir = name[0]
377 if dir == '-':
378 dir = ''
379 else:
380 name = name[1:]
381 if sort_name == name:
382 if dir == '':
383 s_dir = '-'
384 elif dir == '-':
385 s_dir = ''
386 else:
387 m.append(dir+urllib.quote(name))
388 m.insert(0, s_dir+urllib.quote(sort_name))
389 # so things don't get completely out of hand, limit the sort to two columns
390 w(':sort=%s'%','.join(m[:2]))
391 return '&'.join(l)
393 def index(fp, db, classname, filterspec={}, filter=[], columns=[], sort=[],
394 group=[], show_display_form=1, nodeids=None,
395 col_re=re.compile(r'<property\s+name="([^>]+)">')):
397 globals = {
398 'plain': Plain(db, classname, form={}),
399 'field': Field(db, classname, form={}),
400 'menu': Menu(db, classname, form={}),
401 'link': Link(db, classname, form={}),
402 'count': Count(db, classname, form={}),
403 'reldate': Reldate(db, classname, form={}),
404 'download': Download(db, classname, form={}),
405 'checklist': Checklist(db, classname, form={}),
406 'list': List(db, classname, form={}),
407 'history': History(db, classname, form={}),
408 'submit': Submit(db, classname, form={}),
409 'note': Note(db, classname, form={})
410 }
411 cl = db.classes[classname]
412 properties = cl.getprops()
413 w = fp.write
415 try:
416 template = open(os.path.join('templates', classname+'.filter')).read()
417 all_filters = col_re.findall(template)
418 except IOError, error:
419 if error.errno != 2: raise
420 template = None
421 all_filters = []
422 if template and filter:
423 # display the filter section
424 w('<form>')
425 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
426 w('<tr class="location-bar">')
427 w(' <th align="left" colspan="2">Filter specification...</th>')
428 w('</tr>')
429 replace = IndexTemplateReplace(globals, locals(), filter)
430 w(replace.go(template))
431 if columns:
432 w('<input type="hidden" name=":columns" value="%s">'%','.join(columns))
433 if filter:
434 w('<input type="hidden" name=":filter" value="%s">'%','.join(filter))
435 if sort:
436 w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
437 if group:
438 w('<input type="hidden" name=":group" value="%s">'%','.join(group))
439 for k, v in filterspec.items():
440 if type(v) == type([]): v = ','.join(v)
441 w('<input type="hidden" name="%s" value="%s">'%(k, v))
442 w('<tr class="location-bar"><td width="1%%"> </td>')
443 w('<td><input type="submit" value="Redisplay"></td></tr>')
444 w('</table>')
445 w('</form>')
447 # XXX deviate from spec here ...
448 # load the index section template and figure the default columns from it
449 template = open(os.path.join('templates', classname+'.index')).read()
450 all_columns = col_re.findall(template)
451 if not columns:
452 columns = []
453 for name in all_columns:
454 columns.append(name)
455 else:
456 # re-sort columns to be the same order as all_columns
457 l = []
458 for name in all_columns:
459 if name in columns:
460 l.append(name)
461 columns = l
463 # now display the index section
464 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
465 w('<tr class="list-header">')
466 for name in columns:
467 cname = name.capitalize()
468 if show_display_form:
469 anchor = "%s?%s"%(classname, sortby(name, columns, filter,
470 sort, group, filterspec))
471 w('<td><span class="list-item"><a href="%s">%s</a></span></td>'%(
472 anchor, cname))
473 else:
474 w('<td><span class="list-item">%s</span></td>'%cname)
475 w('</tr>')
477 # this stuff is used for group headings - optimise the group names
478 old_group = None
479 group_names = []
480 if group:
481 for name in group:
482 if name[0] == '-': group_names.append(name[1:])
483 else: group_names.append(name)
485 # now actually loop through all the nodes we get from the filter and
486 # apply the template
487 if nodeids is None:
488 nodeids = cl.filter(filterspec, sort, group)
489 for nodeid in nodeids:
490 # check for a group heading
491 if group_names:
492 this_group = [cl.get(nodeid, name) for name in group_names]
493 if this_group != old_group:
494 l = []
495 for name in group_names:
496 prop = properties[name]
497 if prop.isLinkType:
498 group_cl = db.classes[prop.classname]
499 key = group_cl.getkey()
500 value = cl.get(nodeid, name)
501 if value is None:
502 l.append('[unselected %s]'%prop.classname)
503 else:
504 l.append(group_cl.get(cl.get(nodeid, name), key))
505 elif prop.isMultilinkType:
506 group_cl = db.classes[prop.classname]
507 key = group_cl.getkey()
508 for value in cl.get(nodeid, name):
509 l.append(group_cl.get(value, key))
510 else:
511 value = cl.get(nodeid, name)
512 if value is None:
513 value = '[empty %s]'%name
514 l.append(value)
515 w('<tr class="list-header">'
516 '<td align=left colspan=%s><strong>%s</strong></td></tr>'%(
517 len(columns), ', '.join(l)))
518 old_group = this_group
520 # display this node's row
521 for value in globals.values():
522 if hasattr(value, 'nodeid'):
523 value.nodeid = nodeid
524 replace = IndexTemplateReplace(globals, locals(), columns)
525 w(replace.go(template))
527 w('</table>')
529 if not show_display_form:
530 return
532 # now add in the filter/columns/group/etc config table form
533 w('<p><form>')
534 w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
535 for k,v in filterspec.items():
536 if type(v) == type([]): v = ','.join(v)
537 w('<input type="hidden" name="%s" value="%s">'%(k, v))
538 if sort:
539 w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
540 names = []
541 for name in cl.getprops().keys():
542 if name in all_filters or name in all_columns:
543 names.append(name)
544 w('<tr class="location-bar">')
545 w('<th align="left" colspan=%s>View customisation...</th></tr>'%
546 (len(names)+1))
547 w('<tr class="location-bar"><th> </th>')
548 for name in names:
549 w('<th>%s</th>'%name.capitalize())
550 w('</tr>')
552 # filter
553 if all_filters:
554 w('<tr><th width="1%" align=right class="location-bar">Filters</th>')
555 for name in names:
556 if name not in all_filters:
557 w('<td> </td>')
558 continue
559 if name in filter: checked=' checked'
560 else: checked=''
561 w('<td align=middle>')
562 w('<input type="checkbox" name=":filter" value="%s" %s></td>'%(name,
563 checked))
564 w('</tr>')
566 # columns
567 if all_columns:
568 w('<tr><th width="1%" align=right class="location-bar">Columns</th>')
569 for name in names:
570 if name not in all_columns:
571 w('<td> </td>')
572 continue
573 if name in columns: checked=' checked'
574 else: checked=''
575 w('<td align=middle>')
576 w('<input type="checkbox" name=":columns" value="%s" %s></td>'%(
577 name, checked))
578 w('</tr>')
580 # group
581 w('<tr><th width="1%" align=right class="location-bar">Grouping</th>')
582 for name in names:
583 prop = properties[name]
584 if name not in all_columns:
585 w('<td> </td>')
586 continue
587 if name in group: checked=' checked'
588 else: checked=''
589 w('<td align=middle>')
590 w('<input type="checkbox" name=":group" value="%s" %s></td>'%(
591 name, checked))
592 w('</tr>')
594 w('<tr class="location-bar"><td width="1%"> </td>')
595 w('<td colspan="%s">'%len(names))
596 w('<input type="submit" value="Redisplay"></td></tr>')
597 w('</table>')
598 w('</form>')
601 #
602 # ITEM TEMPLATES
603 #
604 class ItemTemplateReplace:
605 def __init__(self, globals, locals, cl, nodeid):
606 self.globals = globals
607 self.locals = locals
608 self.cl = cl
609 self.nodeid = nodeid
611 def go(self, text, replace=re.compile(
612 r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
613 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
614 return replace.sub(self, text)
616 def __call__(self, m, filter=None, columns=None, sort=None, group=None):
617 if m.group('name'):
618 if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
619 replace = ItemTemplateReplace(self.globals, {}, self.cl,
620 self.nodeid)
621 return replace.go(m.group('text'))
622 else:
623 return ''
624 if m.group('display'):
625 command = m.group('command')
626 return eval(command, self.globals, self.locals)
627 print '*** unhandled match', m.groupdict()
629 def item(fp, db, classname, nodeid, replace=re.compile(
630 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
631 r'(?P<endprop></property>)|'
632 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
634 globals = {
635 'plain': Plain(db, classname, nodeid),
636 'field': Field(db, classname, nodeid),
637 'menu': Menu(db, classname, nodeid),
638 'link': Link(db, classname, nodeid),
639 'count': Count(db, classname, nodeid),
640 'reldate': Reldate(db, classname, nodeid),
641 'download': Download(db, classname, nodeid),
642 'checklist': Checklist(db, classname, nodeid),
643 'list': List(db, classname, nodeid),
644 'history': History(db, classname, nodeid),
645 'submit': Submit(db, classname, nodeid),
646 'note': Note(db, classname, nodeid)
647 }
649 cl = db.classes[classname]
650 properties = cl.getprops()
652 if properties.has_key('type') and properties.has_key('content'):
653 pass
654 # XXX we really want to return this as a downloadable...
655 # currently I handle this at a higher level by detecting 'file'
656 # designators...
658 w = fp.write
659 w('<form action="%s%s">'%(classname, nodeid))
660 s = open(os.path.join('templates', classname+'.item')).read()
661 replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
662 w(replace.go(s))
663 w('</form>')
666 def newitem(fp, db, classname, form, replace=re.compile(
667 r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
668 r'(?P<endprop></property>)|'
669 r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
670 globals = {
671 'plain': Plain(db, classname, form=form),
672 'field': Field(db, classname, form=form),
673 'menu': Menu(db, classname, form=form),
674 'link': Link(db, classname, form=form),
675 'count': Count(db, classname, form=form),
676 'reldate': Reldate(db, classname, form=form),
677 'download': Download(db, classname, form=form),
678 'checklist': Checklist(db, classname, form=form),
679 'list': List(db, classname, form=form),
680 'history': History(db, classname, form=form),
681 'submit': Submit(db, classname, form=form),
682 'note': Note(db, classname, form=form)
683 }
685 cl = db.classes[classname]
686 properties = cl.getprops()
688 w = fp.write
689 try:
690 s = open(os.path.join('templates', classname+'.newitem')).read()
691 except:
692 s = open(os.path.join('templates', classname+'.item')).read()
693 w('<form action="new%s">'%classname)
694 replace = ItemTemplateReplace(globals, locals(), None, None)
695 w(replace.go(s))
696 w('</form>')
698 #
699 # $Log: not supported by cvs2svn $
700 #