Code

. Fixed bug in re generation in the filter (I hadn't finished the code ;)
[roundup.git] / template.py
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]'
330 #   INDEX TEMPLATES
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)
342         
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%%">&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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%">&nbsp;</td>')
593     w('<td colspan="%s">'%len(names))
594     w('<input type="submit" value="Redisplay"></td></tr>')
595     w('</table>')
596     w('</form>')
600 #   ITEM TEMPLATES
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>')