Code

8d73e486e872fe544bf42bb8e555b5a188497b19
[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.31 2001-10-21 07:26:35 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     filter_section(w, cl, filter, columns, group, all_filters, all_columns,
570         show_display_form, show_customization)
572     # now display the index section
573     w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
574     w('<tr class="list-header">\n')
575     for name in columns:
576         cname = name.capitalize()
577         if show_display_form:
578             anchor = "%s?%s"%(classname, sortby(name, columns, filter,
579                 sort, group, filterspec))
580             w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
581                 anchor, cname))
582         else:
583             w('<td><span class="list-header">%s</span></td>\n'%cname)
584     w('</tr>\n')
586     # this stuff is used for group headings - optimise the group names
587     old_group = None
588     group_names = []
589     if group:
590         for name in group:
591             if name[0] == '-': group_names.append(name[1:])
592             else: group_names.append(name)
594     # now actually loop through all the nodes we get from the filter and
595     # apply the template
596     if nodeids is None:
597         nodeids = cl.filter(filterspec, sort, group)
598     for nodeid in nodeids:
599         # check for a group heading
600         if group_names:
601             this_group = [cl.get(nodeid, name) for name in group_names]
602             if this_group != old_group:
603                 l = []
604                 for name in group_names:
605                     prop = properties[name]
606                     if isinstance(prop, hyperdb.Link):
607                         group_cl = db.classes[prop.classname]
608                         key = group_cl.getkey()
609                         value = cl.get(nodeid, name)
610                         if value is None:
611                             l.append('[unselected %s]'%prop.classname)
612                         else:
613                             l.append(group_cl.get(cl.get(nodeid, name), key))
614                     elif isinstance(prop, hyperdb.Multilink):
615                         group_cl = db.classes[prop.classname]
616                         key = group_cl.getkey()
617                         for value in cl.get(nodeid, name):
618                             l.append(group_cl.get(value, key))
619                     else:
620                         value = cl.get(nodeid, name)
621                         if value is None:
622                             value = '[empty %s]'%name
623                         else:
624                             value = str(value)
625                         l.append(value)
626                 w('<tr class="section-bar">'
627                   '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
628                     len(columns), ', '.join(l)))
629                 old_group = this_group
631         # display this node's row
632         for value in globals.values():
633             if hasattr(value, 'nodeid'):
634                 value.nodeid = nodeid
635         replace = IndexTemplateReplace(globals, locals(), columns)
636         w(replace.go(template))
638     w('</table>')
641 def filter_section(w, cl, filter, columns, group, all_filters, all_columns,
642         show_display_form, show_customization):
643     # now add in the filter/columns/group/etc config table form
644     w('<input type="hidden" name="show_customization" value="%s">' %
645         show_customization )
646     w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
647     names = []
648     properties = cl.getprops()
649     for name in properties.keys():
650         if name in all_filters or name in all_columns:
651             names.append(name)
652     w('<tr class="location-bar">')
653     if show_customization:
654         action = '-'
655     else:
656         action = '+'
657         # hide the values for filters, columns and grouping in the form
658         # if the customization widget is not visible
659         for name in names:
660             if all_filters and name in filter:
661                 w('<input type="hidden" name=":filter" value="%s">' % name)
662             if all_columns and name in columns:
663                 w('<input type="hidden" name=":columns" value="%s">' % name)
664             if all_columns and name in group:
665                 w('<input type="hidden" name=":group" value="%s">' % name)
667     if show_display_form:
668         # TODO: The widget style can go into the stylesheet
669         w('<th align="left" colspan=%s>'
670           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
671           'customisation...</th></tr>\n'%(len(names)+1, action))
672         if show_customization:
673             w('<tr class="location-bar"><th>&nbsp;</th>')
674             for name in names:
675                 w('<th>%s</th>'%name.capitalize())
676             w('</tr>\n')
678             # Filter
679             if all_filters:
680                 w('<tr><th width="1%" align=right class="location-bar">'
681                   'Filters</th>\n')
682                 for name in names:
683                     if name not in all_filters:
684                         w('<td>&nbsp;</td>')
685                         continue
686                     if name in filter: checked=' checked'
687                     else: checked=''
688                     w('<td align=middle>\n')
689                     w(' <input type="checkbox" name=":filter" value="%s" '
690                       '%s></td>\n'%(name, checked))
691                 w('</tr>\n')
693             # Columns
694             if all_columns:
695                 w('<tr><th width="1%" align=right class="location-bar">'
696                   'Columns</th>\n')
697                 for name in names:
698                     if name not in all_columns:
699                         w('<td>&nbsp;</td>')
700                         continue
701                     if name in columns: checked=' checked'
702                     else: checked=''
703                     w('<td align=middle>\n')
704                     w(' <input type="checkbox" name=":columns" value="%s"'
705                       '%s></td>\n'%(name, checked))
706                 w('</tr>\n')
708                 # Grouping
709                 w('<tr><th width="1%" align=right class="location-bar">'
710                   'Grouping</th>\n')
711                 for name in names:
712                     prop = properties[name]
713                     if name not in all_columns:
714                         w('<td>&nbsp;</td>')
715                         continue
716                     if name in group: checked=' checked'
717                     else: checked=''
718                     w('<td align=middle>\n')
719                     w(' <input type="checkbox" name=":group" value="%s"'
720                       '%s></td>\n'%(name, checked))
721                 w('</tr>\n')
723             w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
724             w('<td colspan="%s">'%len(names))
725             w('<input type="submit" name="action" value="Redisplay"></td>')
726             w('</tr>\n')
728         w('</table>\n')
729         w('</form>\n')
732 #   ITEM TEMPLATES
734 class ItemTemplateReplace:
735     def __init__(self, globals, locals, cl, nodeid):
736         self.globals = globals
737         self.locals = locals
738         self.cl = cl
739         self.nodeid = nodeid
741     def go(self, text, replace=re.compile(
742             r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
743             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
744         return replace.sub(self, text)
746     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
747         if m.group('name'):
748             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
749                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
750                     self.nodeid)
751                 return replace.go(m.group('text'))
752             else:
753                 return ''
754         if m.group('display'):
755             command = m.group('command')
756             return eval(command, self.globals, self.locals)
757         print '*** unhandled match', m.groupdict()
759 def item(client, templates, db, classname, nodeid, replace=re.compile(
760             r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
761             r'(?P<endprop></property>)|'
762             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
764     globals = {
765         'plain': Plain(db, templates, classname, nodeid),
766         'field': Field(db, templates, classname, nodeid),
767         'menu': Menu(db, templates, classname, nodeid),
768         'link': Link(db, templates, classname, nodeid),
769         'count': Count(db, templates, classname, nodeid),
770         'reldate': Reldate(db, templates, classname, nodeid),
771         'download': Download(db, templates, classname, nodeid),
772         'checklist': Checklist(db, templates, classname, nodeid),
773         'list': List(db, templates, classname, nodeid),
774         'history': History(db, templates, classname, nodeid),
775         'submit': Submit(db, templates, classname, nodeid),
776         'note': Note(db, templates, classname, nodeid)
777     }
779     cl = db.classes[classname]
780     properties = cl.getprops()
782     if properties.has_key('type') and properties.has_key('content'):
783         pass
784         # XXX we really want to return this as a downloadable...
785         #  currently I handle this at a higher level by detecting 'file'
786         #  designators...
788     w = client.write
789     w('<form action="%s%s">'%(classname, nodeid))
790     s = open(os.path.join(templates, classname+'.item')).read()
791     replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
792     w(replace.go(s))
793     w('</form>')
796 def newitem(client, templates, db, classname, form, replace=re.compile(
797             r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
798             r'(?P<endprop></property>)|'
799             r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
800     globals = {
801         'plain': Plain(db, templates, classname, form=form),
802         'field': Field(db, templates, classname, form=form),
803         'menu': Menu(db, templates, classname, form=form),
804         'link': Link(db, templates, classname, form=form),
805         'count': Count(db, templates, classname, form=form),
806         'reldate': Reldate(db, templates, classname, form=form),
807         'download': Download(db, templates, classname, form=form),
808         'checklist': Checklist(db, templates, classname, form=form),
809         'list': List(db, templates, classname, form=form),
810         'history': History(db, templates, classname, form=form),
811         'submit': Submit(db, templates, classname, form=form),
812         'note': Note(db, templates, classname, form=form)
813     }
815     cl = db.classes[classname]
816     properties = cl.getprops()
818     w = client.write
819     try:
820         s = open(os.path.join(templates, classname+'.newitem')).read()
821     except:
822         s = open(os.path.join(templates, classname+'.item')).read()
823     w('<form action="new%s" method="POST" enctype="multipart/form-data">'%classname)
824     for key in form.keys():
825         if key[0] == ':':
826             value = form[key].value
827             if type(value) != type([]): value = [value]
828             for value in value:
829                 w('<input type="hidden" name="%s" value="%s">'%(key, value))
830     replace = ItemTemplateReplace(globals, locals(), None, None)
831     w(replace.go(s))
832     w('</form>')
835 # $Log: not supported by cvs2svn $
836 # Revision 1.30  2001/10/21 04:44:50  richard
837 # bug #473124: UI inconsistency with Link fields.
838 #    This also prompted me to fix a fairly long-standing usability issue -
839 #    that of being able to turn off certain filters.
841 # Revision 1.29  2001/10/21 00:17:56  richard
842 # CGI interface view customisation section may now be hidden (patch from
843 #  Roch'e Compaan.)
845 # Revision 1.28  2001/10/21 00:00:16  richard
846 # Fixed Checklist function - wasn't always working on a list.
848 # Revision 1.27  2001/10/20 12:13:44  richard
849 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
851 # Revision 1.26  2001/10/14 10:55:00  richard
852 # Handle empty strings in HTML template Link function
854 # Revision 1.25  2001/10/09 07:25:59  richard
855 # Added the Password property type. See "pydoc roundup.password" for
856 # implementation details. Have updated some of the documentation too.
858 # Revision 1.24  2001/09/27 06:45:58  richard
859 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
860 # on the plain() template function to escape the text for HTML.
862 # Revision 1.23  2001/09/10 09:47:18  richard
863 # Fixed bug in the generation of links to Link/Multilink in indexes.
864 #   (thanks Hubert Hoegl)
865 # Added AssignedTo to the "classic" schema's item page.
867 # Revision 1.22  2001/08/30 06:01:17  richard
868 # Fixed missing import in mailgw :(
870 # Revision 1.21  2001/08/16 07:34:59  richard
871 # better CGI text searching - but hidden filter fields are disappearing...
873 # Revision 1.20  2001/08/15 23:43:18  richard
874 # Fixed some isFooTypes that I missed.
875 # Refactored some code in the CGI code.
877 # Revision 1.19  2001/08/12 06:32:36  richard
878 # using isinstance(blah, Foo) now instead of isFooType
880 # Revision 1.18  2001/08/07 00:24:42  richard
881 # stupid typo
883 # Revision 1.17  2001/08/07 00:15:51  richard
884 # Added the copyright/license notice to (nearly) all files at request of
885 # Bizar Software.
887 # Revision 1.16  2001/08/01 03:52:23  richard
888 # Checklist was using wrong name.
890 # Revision 1.15  2001/07/30 08:12:17  richard
891 # Added time logging and file uploading to the templates.
893 # Revision 1.14  2001/07/30 06:17:45  richard
894 # Features:
895 #  . Added ability for cgi newblah forms to indicate that the new node
896 #    should be linked somewhere.
897 # Fixed:
898 #  . Fixed the agument handling for the roundup-admin find command.
899 #  . Fixed handling of summary when no note supplied for newblah. Again.
900 #  . Fixed detection of no form in htmltemplate Field display.
902 # Revision 1.13  2001/07/30 02:37:53  richard
903 # Temporary measure until we have decent schema migration.
905 # Revision 1.12  2001/07/30 01:24:33  richard
906 # Handles new node display now.
908 # Revision 1.11  2001/07/29 09:31:35  richard
909 # oops
911 # Revision 1.10  2001/07/29 09:28:23  richard
912 # Fixed sorting by clicking on column headings.
914 # Revision 1.9  2001/07/29 08:27:40  richard
915 # Fixed handling of passed-in values in form elements (ie. during a
916 # drill-down)
918 # Revision 1.8  2001/07/29 07:01:39  richard
919 # Added vim command to all source so that we don't get no steenkin' tabs :)
921 # Revision 1.7  2001/07/29 05:36:14  richard
922 # Cleanup of the link label generation.
924 # Revision 1.6  2001/07/29 04:06:42  richard
925 # Fixed problem in link display when Link value is None.
927 # Revision 1.5  2001/07/28 08:17:09  richard
928 # fixed use of stylesheet
930 # Revision 1.4  2001/07/28 07:59:53  richard
931 # Replaced errno integers with their module values.
932 # De-tabbed templatebuilder.py
934 # Revision 1.3  2001/07/25 03:39:47  richard
935 # Hrm - displaying links to classes that don't specify a key property. I've
936 # got it defaulting to 'name', then 'title' and then a "random" property (first
937 # one returned by getprops().keys().
938 # Needs to be moved onto the Class I think...
940 # Revision 1.2  2001/07/22 12:09:32  richard
941 # Final commit of Grande Splite
943 # Revision 1.1  2001/07/22 11:58:35  richard
944 # More Grande Splite
947 # vim: set filetype=python ts=4 sw=4 et si