Code

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