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