Code

0a78595a5784b5efe7eb4673e7c84b0c8fd7d339
[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.32 2001-10-22 03:25:01 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
216        text.
218        If is_download is true, append the property value to the generated
219        URL so that the link may be used as a download link and the
220        downloaded file name is correct.
221     '''
222     def __call__(self, property=None, is_download=0):
223         if not self.nodeid and self.form is None:
224             return '[Link: not called from item]'
225         propclass = self.properties[property]
226         if self.nodeid:
227             value = self.cl.get(self.nodeid, property)
228         else:
229             if isinstance(propclass, hyperdb.Multilink): value = []
230             elif isinstance(propclass, hyperdb.Link): value = None
231             else: value = ''
232         if isinstance(propclass, hyperdb.Link):
233             linkname = propclass.classname
234             if value is None: return '[no %s]'%property.capitalize()
235             linkcl = self.db.classes[linkname]
236             k = linkcl.labelprop()
237             linkvalue = linkcl.get(value, k)
238             if is_download:
239                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
240                     linkvalue, linkvalue)
241             else:
242                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
243         if isinstance(propclass, hyperdb.Multilink):
244             linkname = propclass.classname
245             linkcl = self.db.classes[linkname]
246             k = linkcl.labelprop()
247             if not value : return '[no %s]'%property.capitalize()
248             l = []
249             for value in value:
250                 linkvalue = linkcl.get(value, k)
251                 if is_download:
252                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
253                         linkvalue, linkvalue))
254                 else:
255                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
256                         linkvalue))
257             return ', '.join(l)
258         if isinstance(propclass, hyperdb.String):
259             if value == '': value = '[no %s]'%property.capitalize()
260         if is_download:
261             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
262                 value, value)
263         else:
264             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
266 class Count(Base):
267     ''' for a Multilink property, display a count of the number of links in
268         the list
269     '''
270     def __call__(self, property, **args):
271         if not self.nodeid:
272             return '[Count: not called from item]'
273         propclass = self.properties[property]
274         value = self.cl.get(self.nodeid, property)
275         if isinstance(propclass, hyperdb.Multilink):
276             return str(len(value))
277         return '[Count: not a Multilink]'
279 # XXX pretty is definitely new ;)
280 class Reldate(Base):
281     ''' display a Date property in terms of an interval relative to the
282         current date (e.g. "+ 3w", "- 2d").
284         with the 'pretty' flag, make it pretty
285     '''
286     def __call__(self, property, pretty=0):
287         if not self.nodeid and self.form is None:
288             return '[Reldate: not called from item]'
289         propclass = self.properties[property]
290         if isinstance(not propclass, hyperdb.Date):
291             return '[Reldate: not a Date]'
292         if self.nodeid:
293             value = self.cl.get(self.nodeid, property)
294         else:
295             value = date.Date('.')
296         interval = value - date.Date('.')
297         if pretty:
298             if not self.nodeid:
299                 return 'now'
300             pretty = interval.pretty()
301             if pretty is None:
302                 pretty = value.pretty()
303             return pretty
304         return str(interval)
306 class Download(Base):
307     ''' show a Link("file") or Multilink("file") property using links that
308         allow you to download files
309     '''
310     def __call__(self, property, **args):
311         if not self.nodeid:
312             return '[Download: not called from item]'
313         propclass = self.properties[property]
314         value = self.cl.get(self.nodeid, property)
315         if isinstance(propclass, hyperdb.Link):
316             linkcl = self.db.classes[propclass.classname]
317             linkvalue = linkcl.get(value, k)
318             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
319         if isinstance(propclass, hyperdb.Multilink):
320             linkcl = self.db.classes[propclass.classname]
321             l = []
322             for value in value:
323                 linkvalue = linkcl.get(value, k)
324                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
325             return ', '.join(l)
326         return '[Download: not a link]'
329 class Checklist(Base):
330     ''' for a Link or Multilink property, display checkboxes for the available
331         choices to permit filtering
332     '''
333     def __call__(self, property, **args):
334         propclass = self.properties[property]
335         if (not isinstance(propclass, hyperdb.Link) and not
336                 isinstance(propclass, hyperdb.Multilink)):
337             return '[Checklist: not a link]'
339         # get our current checkbox state
340         if self.nodeid:
341             # get the info from the node - make sure it's a list
342             if isinstance(propclass, hyperdb.Link):
343                 value = [self.cl.get(self.nodeid, property)]
344             else:
345                 value = self.cl.get(self.nodeid, property)
346         elif self.filterspec is not None:
347             # get the state from the filter specification (always a list)
348             value = self.filterspec.get(property, [])
349         else:
350             # it's a new node, so there's no state
351             value = []
353         # so we can map to the linked node's "lable" property
354         linkcl = self.db.classes[propclass.classname]
355         l = []
356         k = linkcl.labelprop()
357         for optionid in linkcl.list():
358             option = linkcl.get(optionid, k)
359             if optionid in value or option in value:
360                 checked = 'checked'
361             else:
362                 checked = ''
363             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
364                 option, checked, property, option))
366         # for Links, allow the "unselected" option too
367         if isinstance(propclass, hyperdb.Link):
368             if value is None or '-1' in value:
369                 checked = 'checked'
370             else:
371                 checked = ''
372             l.append('[unselected]:<input type="checkbox" %s name="%s" '
373                 'value="-1">'%(checked, property))
374         return '\n'.join(l)
376 class Note(Base):
377     ''' display a "note" field, which is a text area for entering a note to
378         go along with a change. 
379     '''
380     def __call__(self, rows=5, cols=80):
381        # TODO: pull the value from the form
382         return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
383             cols)
385 # XXX new function
386 class List(Base):
387     ''' list the items specified by property using the standard index for
388         the class
389     '''
390     def __call__(self, property, reverse=0):
391         propclass = self.properties[property]
392         if isinstance(not propclass, hyperdb.Multilink):
393             return '[List: not a Multilink]'
394         fp = StringIO.StringIO()
395         value = self.cl.get(self.nodeid, property)
396         if reverse:
397             value.reverse()
398         # TODO: really not happy with the way templates is passed on here
399         index(fp, self.templates, self.db, propclass.classname, nodeids=value,
400             show_display_form=0)
401         return fp.getvalue()
403 # XXX new function
404 class History(Base):
405     ''' list the history of the item
406     '''
407     def __call__(self, **args):
408         if self.nodeid is None:
409             return "[History: node doesn't exist]"
411         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
412             '<tr class="list-header">',
413             '<td><span class="list-item"><strong>Date</strong></span></td>',
414             '<td><span class="list-item"><strong>User</strong></span></td>',
415             '<td><span class="list-item"><strong>Action</strong></span></td>',
416             '<td><span class="list-item"><strong>Args</strong></span></td>']
418         for id, date, user, action, args in self.cl.history(self.nodeid):
419             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
420                date, user, action, args))
421         l.append('</table>')
422         return '\n'.join(l)
424 # XXX new function
425 class Submit(Base):
426     ''' add a submit button for the item
427     '''
428     def __call__(self):
429         if self.nodeid:
430             return '<input type="submit" value="Submit Changes">'
431         elif self.form is not None:
432             return '<input type="submit" value="Submit New Entry">'
433         else:
434             return '[Submit: not called from item]'
438 #   INDEX TEMPLATES
440 class IndexTemplateReplace:
441     def __init__(self, globals, locals, props):
442         self.globals = globals
443         self.locals = locals
444         self.props = props
446     def go(self, text, replace=re.compile(
447             r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
448             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
449         return replace.sub(self, text)
451     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
452         if m.group('name'):
453             if m.group('name') in self.props:
454                 text = m.group('text')
455                 replace = IndexTemplateReplace(self.globals, {}, self.props)
456                 return replace.go(m.group('text'))
457             else:
458                 return ''
459         if m.group('display'):
460             command = m.group('command')
461             return eval(command, self.globals, self.locals)
462         print '*** unhandled match', m.groupdict()
464 def sortby(sort_name, columns, filter, sort, group, filterspec):
465     l = []
466     w = l.append
467     for k, v in filterspec.items():
468         k = urllib.quote(k)
469         if type(v) == type([]):
470             w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
471         else:
472             w('%s=%s'%(k, urllib.quote(v)))
473     if columns:
474         w(':columns=%s'%','.join(map(urllib.quote, columns)))
475     if filter:
476         w(':filter=%s'%','.join(map(urllib.quote, filter)))
477     if group:
478         w(':group=%s'%','.join(map(urllib.quote, group)))
479     m = []
480     s_dir = ''
481     for name in sort:
482         dir = name[0]
483         if dir == '-':
484             name = name[1:]
485         else:
486             dir = ''
487         if sort_name == name:
488             if dir == '-':
489                 s_dir = ''
490             else:
491                 s_dir = '-'
492         else:
493             m.append(dir+urllib.quote(name))
494     m.insert(0, s_dir+urllib.quote(sort_name))
495     # so things don't get completely out of hand, limit the sort to two columns
496     w(':sort=%s'%','.join(m[:2]))
497     return '&'.join(l)
499 def index(client, templates, db, classname, filterspec={}, filter=[],
500         columns=[], sort=[], group=[], show_display_form=1, nodeids=None,
501         show_customization=1,
502         col_re=re.compile(r'<property\s+name="([^>]+)">')):
503     globals = {
504         'plain': Plain(db, templates, classname, filterspec=filterspec),
505         'field': Field(db, templates, classname, filterspec=filterspec),
506         'menu': Menu(db, templates, classname, filterspec=filterspec),
507         'link': Link(db, templates, classname, filterspec=filterspec),
508         'count': Count(db, templates, classname, filterspec=filterspec),
509         'reldate': Reldate(db, templates, classname, filterspec=filterspec),
510         'download': Download(db, templates, classname, filterspec=filterspec),
511         'checklist': Checklist(db, templates, classname, filterspec=filterspec),
512         'list': List(db, templates, classname, filterspec=filterspec),
513         'history': History(db, templates, classname, filterspec=filterspec),
514         'submit': Submit(db, templates, classname, filterspec=filterspec),
515         'note': Note(db, templates, classname, filterspec=filterspec)
516     }
517     cl = db.classes[classname]
518     properties = cl.getprops()
519     w = client.write
520     w('<form>')
522     try:
523         template = open(os.path.join(templates, classname+'.filter')).read()
524         all_filters = col_re.findall(template)
525     except IOError, error:
526         if error.errno != errno.ENOENT: raise
527         template = None
528         all_filters = []
529     if template and filter:
530         # display the filter section
531         w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
532         w('<tr class="location-bar">')
533         w(' <th align="left" colspan="2">Filter specification...</th>')
534         w('</tr>')
535         replace = IndexTemplateReplace(globals, locals(), filter)
536         w(replace.go(template))
537         w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
538         w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
539         w('</table>')
541     # If the filters aren't being displayed, then hide their current
542     # value in the form
543     if not filter:
544         for k, v in filterspec.items():
545             if type(v) == type([]): v = ','.join(v)
546             w('<input type="hidden" name="%s" value="%s">'%(k, v))
548     # make sure that the sorting doesn't get lost either
549     if sort:
550         w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
552     # XXX deviate from spec here ...
553     # load the index section template and figure the default columns from it
554     template = open(os.path.join(templates, classname+'.index')).read()
555     all_columns = col_re.findall(template)
556     if not columns:
557         columns = []
558         for name in all_columns:
559             columns.append(name)
560     else:
561         # re-sort columns to be the same order as all_columns
562         l = []
563         for name in all_columns:
564             if name in columns:
565                 l.append(name)
566         columns = l
568     # display the filter section
569     if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'top'):
570         filter_section(w, cl, filter, columns, group, all_filters, all_columns,
571             show_display_form, show_customization)
573     # now display the index section
574     w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
575     w('<tr class="list-header">\n')
576     for name in columns:
577         cname = name.capitalize()
578         if show_display_form:
579             anchor = "%s?%s"%(classname, sortby(name, columns, filter,
580                 sort, group, filterspec))
581             w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
582                 anchor, cname))
583         else:
584             w('<td><span class="list-header">%s</span></td>\n'%cname)
585     w('</tr>\n')
587     # this stuff is used for group headings - optimise the group names
588     old_group = None
589     group_names = []
590     if group:
591         for name in group:
592             if name[0] == '-': group_names.append(name[1:])
593             else: group_names.append(name)
595     # now actually loop through all the nodes we get from the filter and
596     # apply the template
597     if nodeids is None:
598         nodeids = cl.filter(filterspec, sort, group)
599     for nodeid in nodeids:
600         # check for a group heading
601         if group_names:
602             this_group = [cl.get(nodeid, name) for name in group_names]
603             if this_group != old_group:
604                 l = []
605                 for name in group_names:
606                     prop = properties[name]
607                     if isinstance(prop, hyperdb.Link):
608                         group_cl = db.classes[prop.classname]
609                         key = group_cl.getkey()
610                         value = cl.get(nodeid, name)
611                         if value is None:
612                             l.append('[unselected %s]'%prop.classname)
613                         else:
614                             l.append(group_cl.get(cl.get(nodeid, name), key))
615                     elif isinstance(prop, hyperdb.Multilink):
616                         group_cl = db.classes[prop.classname]
617                         key = group_cl.getkey()
618                         for value in cl.get(nodeid, name):
619                             l.append(group_cl.get(value, key))
620                     else:
621                         value = cl.get(nodeid, name)
622                         if value is None:
623                             value = '[empty %s]'%name
624                         else:
625                             value = str(value)
626                         l.append(value)
627                 w('<tr class="section-bar">'
628                   '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
629                     len(columns), ', '.join(l)))
630                 old_group = this_group
632         # display this node's row
633         for value in globals.values():
634             if hasattr(value, 'nodeid'):
635                 value.nodeid = nodeid
636         replace = IndexTemplateReplace(globals, locals(), columns)
637         w(replace.go(template))
639     w('</table>')
641     # display the filter section
642     if hasattr(client, 'FILTER_POSITION') and client.FILTER_POSITION in ('top and bottom', 'bottom'):
643         filter_section(w, cl, filter, columns, group, all_filters, all_columns,
644             show_display_form, show_customization)
647 def filter_section(w, cl, filter, columns, group, all_filters, all_columns,
648         show_display_form, show_customization):
649     # now add in the filter/columns/group/etc config table form
650     w('<input type="hidden" name="show_customization" value="%s">' %
651         show_customization )
652     w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
653     names = []
654     properties = cl.getprops()
655     for name in properties.keys():
656         if name in all_filters or name in all_columns:
657             names.append(name)
658     w('<tr class="location-bar">')
659     if show_customization:
660         action = '-'
661     else:
662         action = '+'
663         # hide the values for filters, columns and grouping in the form
664         # if the customization widget is not visible
665         for name in names:
666             if all_filters and name in filter:
667                 w('<input type="hidden" name=":filter" value="%s">' % name)
668             if all_columns and name in columns:
669                 w('<input type="hidden" name=":columns" value="%s">' % name)
670             if all_columns and name in group:
671                 w('<input type="hidden" name=":group" value="%s">' % name)
673     if show_display_form:
674         # TODO: The widget style can go into the stylesheet
675         w('<th align="left" colspan=%s>'
676           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
677           'customisation...</th></tr>\n'%(len(names)+1, action))
678         if show_customization:
679             w('<tr class="location-bar"><th>&nbsp;</th>')
680             for name in names:
681                 w('<th>%s</th>'%name.capitalize())
682             w('</tr>\n')
684             # Filter
685             if all_filters:
686                 w('<tr><th width="1%" align=right class="location-bar">'
687                   'Filters</th>\n')
688                 for name in names:
689                     if name not in all_filters:
690                         w('<td>&nbsp;</td>')
691                         continue
692                     if name in filter: checked=' checked'
693                     else: checked=''
694                     w('<td align=middle>\n')
695                     w(' <input type="checkbox" name=":filter" value="%s" '
696                       '%s></td>\n'%(name, checked))
697                 w('</tr>\n')
699             # Columns
700             if all_columns:
701                 w('<tr><th width="1%" align=right class="location-bar">'
702                   'Columns</th>\n')
703                 for name in names:
704                     if name not in all_columns:
705                         w('<td>&nbsp;</td>')
706                         continue
707                     if name in columns: checked=' checked'
708                     else: checked=''
709                     w('<td align=middle>\n')
710                     w(' <input type="checkbox" name=":columns" value="%s"'
711                       '%s></td>\n'%(name, checked))
712                 w('</tr>\n')
714                 # Grouping
715                 w('<tr><th width="1%" align=right class="location-bar">'
716                   'Grouping</th>\n')
717                 for name in names:
718                     prop = properties[name]
719                     if name not in all_columns:
720                         w('<td>&nbsp;</td>')
721                         continue
722                     if name in group: checked=' checked'
723                     else: checked=''
724                     w('<td align=middle>\n')
725                     w(' <input type="checkbox" name=":group" value="%s"'
726                       '%s></td>\n'%(name, checked))
727                 w('</tr>\n')
729             w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
730             w('<td colspan="%s">'%len(names))
731             w('<input type="submit" name="action" value="Redisplay"></td>')
732             w('</tr>\n')
734         w('</table>\n')
735         w('</form>\n')
738 #   ITEM TEMPLATES
740 class ItemTemplateReplace:
741     def __init__(self, globals, locals, cl, nodeid):
742         self.globals = globals
743         self.locals = locals
744         self.cl = cl
745         self.nodeid = nodeid
747     def go(self, text, replace=re.compile(
748             r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
749             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
750         return replace.sub(self, text)
752     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
753         if m.group('name'):
754             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
755                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
756                     self.nodeid)
757                 return replace.go(m.group('text'))
758             else:
759                 return ''
760         if m.group('display'):
761             command = m.group('command')
762             return eval(command, self.globals, self.locals)
763         print '*** unhandled match', m.groupdict()
765 def item(client, templates, db, classname, nodeid, replace=re.compile(
766             r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
767             r'(?P<endprop></property>)|'
768             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
770     globals = {
771         'plain': Plain(db, templates, classname, nodeid),
772         'field': Field(db, templates, classname, nodeid),
773         'menu': Menu(db, templates, classname, nodeid),
774         'link': Link(db, templates, classname, nodeid),
775         'count': Count(db, templates, classname, nodeid),
776         'reldate': Reldate(db, templates, classname, nodeid),
777         'download': Download(db, templates, classname, nodeid),
778         'checklist': Checklist(db, templates, classname, nodeid),
779         'list': List(db, templates, classname, nodeid),
780         'history': History(db, templates, classname, nodeid),
781         'submit': Submit(db, templates, classname, nodeid),
782         'note': Note(db, templates, classname, nodeid)
783     }
785     cl = db.classes[classname]
786     properties = cl.getprops()
788     if properties.has_key('type') and properties.has_key('content'):
789         pass
790         # XXX we really want to return this as a downloadable...
791         #  currently I handle this at a higher level by detecting 'file'
792         #  designators...
794     w = client.write
795     w('<form action="%s%s">'%(classname, nodeid))
796     s = open(os.path.join(templates, classname+'.item')).read()
797     replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
798     w(replace.go(s))
799     w('</form>')
802 def newitem(client, templates, db, classname, form, replace=re.compile(
803             r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
804             r'(?P<endprop></property>)|'
805             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
806     globals = {
807         'plain': Plain(db, templates, classname, form=form),
808         'field': Field(db, templates, classname, form=form),
809         'menu': Menu(db, templates, classname, form=form),
810         'link': Link(db, templates, classname, form=form),
811         'count': Count(db, templates, classname, form=form),
812         'reldate': Reldate(db, templates, classname, form=form),
813         'download': Download(db, templates, classname, form=form),
814         'checklist': Checklist(db, templates, classname, form=form),
815         'list': List(db, templates, classname, form=form),
816         'history': History(db, templates, classname, form=form),
817         'submit': Submit(db, templates, classname, form=form),
818         'note': Note(db, templates, classname, form=form)
819     }
821     cl = db.classes[classname]
822     properties = cl.getprops()
824     w = client.write
825     try:
826         s = open(os.path.join(templates, classname+'.newitem')).read()
827     except:
828         s = open(os.path.join(templates, classname+'.item')).read()
829     w('<form action="new%s" method="POST" enctype="multipart/form-data">'%classname)
830     for key in form.keys():
831         if key[0] == ':':
832             value = form[key].value
833             if type(value) != type([]): value = [value]
834             for value in value:
835                 w('<input type="hidden" name="%s" value="%s">'%(key, value))
836     replace = ItemTemplateReplace(globals, locals(), None, None)
837     w(replace.go(s))
838     w('</form>')
841 # $Log: not supported by cvs2svn $
842 # Revision 1.31  2001/10/21 07:26:35  richard
843 # feature #473127: Filenames. I modified the file.index and htmltemplate
844 #  source so that the filename is used in the link and the creation
845 #  information is displayed.
847 # Revision 1.30  2001/10/21 04:44:50  richard
848 # bug #473124: UI inconsistency with Link fields.
849 #    This also prompted me to fix a fairly long-standing usability issue -
850 #    that of being able to turn off certain filters.
852 # Revision 1.29  2001/10/21 00:17:56  richard
853 # CGI interface view customisation section may now be hidden (patch from
854 #  Roch'e Compaan.)
856 # Revision 1.28  2001/10/21 00:00:16  richard
857 # Fixed Checklist function - wasn't always working on a list.
859 # Revision 1.27  2001/10/20 12:13:44  richard
860 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
862 # Revision 1.26  2001/10/14 10:55:00  richard
863 # Handle empty strings in HTML template Link function
865 # Revision 1.25  2001/10/09 07:25:59  richard
866 # Added the Password property type. See "pydoc roundup.password" for
867 # implementation details. Have updated some of the documentation too.
869 # Revision 1.24  2001/09/27 06:45:58  richard
870 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
871 # on the plain() template function to escape the text for HTML.
873 # Revision 1.23  2001/09/10 09:47:18  richard
874 # Fixed bug in the generation of links to Link/Multilink in indexes.
875 #   (thanks Hubert Hoegl)
876 # Added AssignedTo to the "classic" schema's item page.
878 # Revision 1.22  2001/08/30 06:01:17  richard
879 # Fixed missing import in mailgw :(
881 # Revision 1.21  2001/08/16 07:34:59  richard
882 # better CGI text searching - but hidden filter fields are disappearing...
884 # Revision 1.20  2001/08/15 23:43:18  richard
885 # Fixed some isFooTypes that I missed.
886 # Refactored some code in the CGI code.
888 # Revision 1.19  2001/08/12 06:32:36  richard
889 # using isinstance(blah, Foo) now instead of isFooType
891 # Revision 1.18  2001/08/07 00:24:42  richard
892 # stupid typo
894 # Revision 1.17  2001/08/07 00:15:51  richard
895 # Added the copyright/license notice to (nearly) all files at request of
896 # Bizar Software.
898 # Revision 1.16  2001/08/01 03:52:23  richard
899 # Checklist was using wrong name.
901 # Revision 1.15  2001/07/30 08:12:17  richard
902 # Added time logging and file uploading to the templates.
904 # Revision 1.14  2001/07/30 06:17:45  richard
905 # Features:
906 #  . Added ability for cgi newblah forms to indicate that the new node
907 #    should be linked somewhere.
908 # Fixed:
909 #  . Fixed the agument handling for the roundup-admin find command.
910 #  . Fixed handling of summary when no note supplied for newblah. Again.
911 #  . Fixed detection of no form in htmltemplate Field display.
913 # Revision 1.13  2001/07/30 02:37:53  richard
914 # Temporary measure until we have decent schema migration.
916 # Revision 1.12  2001/07/30 01:24:33  richard
917 # Handles new node display now.
919 # Revision 1.11  2001/07/29 09:31:35  richard
920 # oops
922 # Revision 1.10  2001/07/29 09:28:23  richard
923 # Fixed sorting by clicking on column headings.
925 # Revision 1.9  2001/07/29 08:27:40  richard
926 # Fixed handling of passed-in values in form elements (ie. during a
927 # drill-down)
929 # Revision 1.8  2001/07/29 07:01:39  richard
930 # Added vim command to all source so that we don't get no steenkin' tabs :)
932 # Revision 1.7  2001/07/29 05:36:14  richard
933 # Cleanup of the link label generation.
935 # Revision 1.6  2001/07/29 04:06:42  richard
936 # Fixed problem in link display when Link value is None.
938 # Revision 1.5  2001/07/28 08:17:09  richard
939 # fixed use of stylesheet
941 # Revision 1.4  2001/07/28 07:59:53  richard
942 # Replaced errno integers with their module values.
943 # De-tabbed templatebuilder.py
945 # Revision 1.3  2001/07/25 03:39:47  richard
946 # Hrm - displaying links to classes that don't specify a key property. I've
947 # got it defaulting to 'name', then 'title' and then a "random" property (first
948 # one returned by getprops().keys().
949 # Needs to be moved onto the Class I think...
951 # Revision 1.2  2001/07/22 12:09:32  richard
952 # Final commit of Grande Splite
954 # Revision 1.1  2001/07/22 11:58:35  richard
955 # More Grande Splite
958 # vim: set filetype=python ts=4 sw=4 et si