Code

Remove the '=' padding from cookie value so quoting isn't an issue.
[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.40 2001-11-03 01:56:51 richard Exp $
20 import os, re, StringIO, urllib, cgi, errno
22 import hyperdb, date, password
24 # This imports the StructureText functionality for the do_stext function
25 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
26 try:
27     from StructuredText.StructuredText import HTML as StructuredText
28 except ImportError:
29     StructuredText = None
31 class TemplateFunctions:
32     def __init__(self):
33         self.form = None
34         self.nodeid = None
35         self.filterspec = None
36         self.globals = {}
37         for key in TemplateFunctions.__dict__.keys():
38             if key[:3] == 'do_':
39                 self.globals[key[3:]] = getattr(self, key)
41     def do_plain(self, property, escape=0):
42         ''' display a String property directly;
44             display a Date property in a specified time zone with an option to
45             omit the time from the date stamp;
47             for a Link or Multilink property, display the key strings of the
48             linked nodes (or the ids if the linked class has no key property)
49         '''
50         if not self.nodeid and self.form is None:
51             return '[Field: not called from item]'
52         propclass = self.properties[property]
53         if self.nodeid:
54             value = self.cl.get(self.nodeid, property)
55         else:
56             # TODO: pull the value from the form
57             if isinstance(propclass, hyperdb.Multilink): value = []
58             else: value = ''
59         if isinstance(propclass, hyperdb.String):
60             if value is None: value = ''
61             else: value = str(value)
62         elif isinstance(propclass, hyperdb.Password):
63             if value is None: value = ''
64             else: value = '*encrypted*'
65         elif isinstance(propclass, hyperdb.Date):
66             value = str(value)
67         elif isinstance(propclass, hyperdb.Interval):
68             value = str(value)
69         elif isinstance(propclass, hyperdb.Link):
70             linkcl = self.db.classes[propclass.classname]
71             k = linkcl.labelprop()
72             if value: value = str(linkcl.get(value, k))
73             else: value = '[unselected]'
74         elif isinstance(propclass, hyperdb.Multilink):
75             linkcl = self.db.classes[propclass.classname]
76             k = linkcl.labelprop()
77             value = ', '.join([linkcl.get(i, k) for i in value])
78         else:
79             s = 'Plain: bad propclass "%s"'%propclass
80         if escape:
81             value = cgi.escape(value)
82         return value
84     def do_stext(self, property, escape=0):
85         '''Render as structured text using the StructuredText module
86            (see above for details)
87         '''
88         s = self.do_plain(property, escape=escape)
89         if not StructuredText:
90             return s
91         return StructuredText(s,level=1,header=0)
93     def do_field(self, property, size=None, height=None, showid=0):
94         ''' display a property like the plain displayer, but in a text field
95             to be edited
96         '''
97         if not self.nodeid and self.form is None and self.filterspec is None:
98             return '[Field: not called from item]'
99         propclass = self.properties[property]
100         if self.nodeid:
101             value = self.cl.get(self.nodeid, property, None)
102             # TODO: remove this from the code ... it's only here for
103             # handling schema changes, and they should be handled outside
104             # of this code...
105             if isinstance(propclass, hyperdb.Multilink) and value is None:
106                 value = []
107         elif self.filterspec is not None:
108             if isinstance(propclass, hyperdb.Multilink):
109                 value = self.filterspec.get(property, [])
110             else:
111                 value = self.filterspec.get(property, '')
112         else:
113             # TODO: pull the value from the form
114             if isinstance(propclass, hyperdb.Multilink): value = []
115             else: value = ''
116         if (isinstance(propclass, hyperdb.String) or
117                 isinstance(propclass, hyperdb.Date) or
118                 isinstance(propclass, hyperdb.Interval)):
119             size = size or 30
120             if value is None:
121                 value = ''
122             else:
123                 value = cgi.escape(value)
124                 value = '"'.join(value.split('"'))
125             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
126         elif isinstance(propclass, hyperdb.Password):
127             size = size or 30
128             s = '<input type="password" name="%s" size="%s">'%(property, size)
129         elif isinstance(propclass, hyperdb.Link):
130             linkcl = self.db.classes[propclass.classname]
131             l = ['<select name="%s">'%property]
132             k = linkcl.labelprop()
133             if value is None:
134                 s = 'selected '
135             else:
136                 s = ''
137             l.append('<option %svalue="-1">- no selection -</option>'%s)
138             for optionid in linkcl.list():
139                 option = linkcl.get(optionid, k)
140                 s = ''
141                 if optionid == value:
142                     s = 'selected '
143                 if showid:
144                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
145                 else:
146                     lab = option
147                 if size is not None and len(lab) > size:
148                     lab = lab[:size-3] + '...'
149                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
150             l.append('</select>')
151             s = '\n'.join(l)
152         elif isinstance(propclass, hyperdb.Multilink):
153             linkcl = self.db.classes[propclass.classname]
154             list = linkcl.list()
155             height = height or min(len(list), 7)
156             l = ['<select multiple name="%s" size="%s">'%(property, height)]
157             k = linkcl.labelprop()
158             for optionid in list:
159                 option = linkcl.get(optionid, k)
160                 s = ''
161                 if optionid in value:
162                     s = 'selected '
163                 if showid:
164                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
165                 else:
166                     lab = option
167                 if size is not None and len(lab) > size:
168                     lab = lab[:size-3] + '...'
169                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
170             l.append('</select>')
171             s = '\n'.join(l)
172         else:
173             s = 'Plain: bad propclass "%s"'%propclass
174         return s
176     def do_menu(self, property, size=None, height=None, showid=0):
177         ''' for a Link property, display a menu of the available choices
178         '''
179         propclass = self.properties[property]
180         if self.nodeid:
181             value = self.cl.get(self.nodeid, property)
182         else:
183             # TODO: pull the value from the form
184             if isinstance(propclass, hyperdb.Multilink): value = []
185             else: value = None
186         if isinstance(propclass, hyperdb.Link):
187             linkcl = self.db.classes[propclass.classname]
188             l = ['<select name="%s">'%property]
189             k = linkcl.labelprop()
190             s = ''
191             if value is None:
192                 s = 'selected '
193             l.append('<option %svalue="-1">- no selection -</option>'%s)
194             for optionid in linkcl.list():
195                 option = linkcl.get(optionid, k)
196                 s = ''
197                 if optionid == value:
198                     s = 'selected '
199                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
200             l.append('</select>')
201             return '\n'.join(l)
202         if isinstance(propclass, hyperdb.Multilink):
203             linkcl = self.db.classes[propclass.classname]
204             list = linkcl.list()
205             height = height or min(len(list), 7)
206             l = ['<select multiple name="%s" size="%s">'%(property, height)]
207             k = linkcl.labelprop()
208             for optionid in list:
209                 option = linkcl.get(optionid, k)
210                 s = ''
211                 if optionid in value:
212                     s = 'selected '
213                 if showid:
214                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
215                 else:
216                     lab = option
217                 if size is not None and len(lab) > size:
218                     lab = lab[:size-3] + '...'
219                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
220             l.append('</select>')
221             return '\n'.join(l)
222         return '[Menu: not a link]'
224     #XXX deviates from spec
225     def do_link(self, property=None, is_download=0):
226         '''For a Link or Multilink property, display the names of the linked
227            nodes, hyperlinked to the item views on those nodes.
228            For other properties, link to this node with the property as the
229            text.
231            If is_download is true, append the property value to the generated
232            URL so that the link may be used as a download link and the
233            downloaded file name is correct.
234         '''
235         if not self.nodeid and self.form is None:
236             return '[Link: not called from item]'
237         propclass = self.properties[property]
238         if self.nodeid:
239             value = self.cl.get(self.nodeid, property)
240         else:
241             if isinstance(propclass, hyperdb.Multilink): value = []
242             elif isinstance(propclass, hyperdb.Link): value = None
243             else: value = ''
244         if isinstance(propclass, hyperdb.Link):
245             linkname = propclass.classname
246             if value is None: return '[no %s]'%property.capitalize()
247             linkcl = self.db.classes[linkname]
248             k = linkcl.labelprop()
249             linkvalue = linkcl.get(value, k)
250             if is_download:
251                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
252                     linkvalue, linkvalue)
253             else:
254                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
255         if isinstance(propclass, hyperdb.Multilink):
256             linkname = propclass.classname
257             linkcl = self.db.classes[linkname]
258             k = linkcl.labelprop()
259             if not value : return '[no %s]'%property.capitalize()
260             l = []
261             for value in value:
262                 linkvalue = linkcl.get(value, k)
263                 if is_download:
264                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
265                         linkvalue, linkvalue))
266                 else:
267                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
268                         linkvalue))
269             return ', '.join(l)
270         if isinstance(propclass, hyperdb.String):
271             if value == '': value = '[no %s]'%property.capitalize()
272         if is_download:
273             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
274                 value, value)
275         else:
276             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
278     def do_count(self, property, **args):
279         ''' for a Multilink property, display a count of the number of links in
280             the list
281         '''
282         if not self.nodeid:
283             return '[Count: not called from item]'
284         propclass = self.properties[property]
285         value = self.cl.get(self.nodeid, property)
286         if isinstance(propclass, hyperdb.Multilink):
287             return str(len(value))
288         return '[Count: not a Multilink]'
290     # XXX pretty is definitely new ;)
291     def do_reldate(self, property, pretty=0):
292         ''' display a Date property in terms of an interval relative to the
293             current date (e.g. "+ 3w", "- 2d").
295             with the 'pretty' flag, make it pretty
296         '''
297         if not self.nodeid and self.form is None:
298             return '[Reldate: not called from item]'
299         propclass = self.properties[property]
300         if isinstance(not propclass, hyperdb.Date):
301             return '[Reldate: not a Date]'
302         if self.nodeid:
303             value = self.cl.get(self.nodeid, property)
304         else:
305             value = date.Date('.')
306         interval = value - date.Date('.')
307         if pretty:
308             if not self.nodeid:
309                 return 'now'
310             pretty = interval.pretty()
311             if pretty is None:
312                 pretty = value.pretty()
313             return pretty
314         return str(interval)
316     def do_download(self, property, **args):
317         ''' show a Link("file") or Multilink("file") property using links that
318             allow you to download files
319         '''
320         if not self.nodeid:
321             return '[Download: not called from item]'
322         propclass = self.properties[property]
323         value = self.cl.get(self.nodeid, property)
324         if isinstance(propclass, hyperdb.Link):
325             linkcl = self.db.classes[propclass.classname]
326             linkvalue = linkcl.get(value, k)
327             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
328         if isinstance(propclass, hyperdb.Multilink):
329             linkcl = self.db.classes[propclass.classname]
330             l = []
331             for value in value:
332                 linkvalue = linkcl.get(value, k)
333                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
334             return ', '.join(l)
335         return '[Download: not a link]'
338     def do_checklist(self, property, **args):
339         ''' for a Link or Multilink property, display checkboxes for the
340             available choices to permit filtering
341         '''
342         propclass = self.properties[property]
343         if (not isinstance(propclass, hyperdb.Link) and not
344                 isinstance(propclass, hyperdb.Multilink)):
345             return '[Checklist: not a link]'
347         # get our current checkbox state
348         if self.nodeid:
349             # get the info from the node - make sure it's a list
350             if isinstance(propclass, hyperdb.Link):
351                 value = [self.cl.get(self.nodeid, property)]
352             else:
353                 value = self.cl.get(self.nodeid, property)
354         elif self.filterspec is not None:
355             # get the state from the filter specification (always a list)
356             value = self.filterspec.get(property, [])
357         else:
358             # it's a new node, so there's no state
359             value = []
361         # so we can map to the linked node's "lable" property
362         linkcl = self.db.classes[propclass.classname]
363         l = []
364         k = linkcl.labelprop()
365         for optionid in linkcl.list():
366             option = linkcl.get(optionid, k)
367             if optionid in value or option in value:
368                 checked = 'checked'
369             else:
370                 checked = ''
371             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
372                 option, checked, property, option))
374         # for Links, allow the "unselected" option too
375         if isinstance(propclass, hyperdb.Link):
376             if value is None or '-1' in value:
377                 checked = 'checked'
378             else:
379                 checked = ''
380             l.append('[unselected]:<input type="checkbox" %s name="%s" '
381                 'value="-1">'%(checked, property))
382         return '\n'.join(l)
384     def do_note(self, rows=5, cols=80):
385         ''' display a "note" field, which is a text area for entering a note to
386             go along with a change. 
387         '''
388         # TODO: pull the value from the form
389         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
390             '</textarea>'%(rows, cols)
392     # XXX new function
393     def do_list(self, property, reverse=0):
394         ''' list the items specified by property using the standard index for
395             the class
396         '''
397         propcl = self.properties[property]
398         if not isinstance(propcl, hyperdb.Multilink):
399             return '[List: not a Multilink]'
400         value = self.cl.get(self.nodeid, property)
401         if reverse:
402             value.reverse()
404         # render the sub-index into a string
405         fp = StringIO.StringIO()
406         try:
407             write_save = self.client.write
408             self.client.write = fp.write
409             index = IndexTemplate(self.client, self.templates, propcl.classname)
410             index.render(nodeids=value, show_display_form=0)
411         finally:
412             self.client.write = write_save
414         return fp.getvalue()
416     # XXX new function
417     def do_history(self, **args):
418         ''' list the history of the item
419         '''
420         if self.nodeid is None:
421             return "[History: node doesn't exist]"
423         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
424             '<tr class="list-header">',
425             '<td><span class="list-item"><strong>Date</strong></span></td>',
426             '<td><span class="list-item"><strong>User</strong></span></td>',
427             '<td><span class="list-item"><strong>Action</strong></span></td>',
428             '<td><span class="list-item"><strong>Args</strong></span></td>']
430         for id, date, user, action, args in self.cl.history(self.nodeid):
431             l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
432                date, user, action, args))
433         l.append('</table>')
434         return '\n'.join(l)
436     # XXX new function
437     def do_submit(self):
438         ''' add a submit button for the item
439         '''
440         if self.nodeid:
441             return '<input type="submit" value="Submit Changes">'
442         elif self.form is not None:
443             return '<input type="submit" value="Submit New Entry">'
444         else:
445             return '[Submit: not called from item]'
449 #   INDEX TEMPLATES
451 class IndexTemplateReplace:
452     def __init__(self, globals, locals, props):
453         self.globals = globals
454         self.locals = locals
455         self.props = props
457     replace=re.compile(
458         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
459         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
460     def go(self, text):
461         return self.replace.sub(self, text)
463     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
464         if m.group('name'):
465             if m.group('name') in self.props:
466                 text = m.group('text')
467                 replace = IndexTemplateReplace(self.globals, {}, self.props)
468                 return replace.go(m.group('text'))
469             else:
470                 return ''
471         if m.group('display'):
472             command = m.group('command')
473             return eval(command, self.globals, self.locals)
474         print '*** unhandled match', m.groupdict()
476 class IndexTemplate(TemplateFunctions):
477     def __init__(self, client, templates, classname):
478         self.client = client
479         self.templates = templates
480         self.classname = classname
482         # derived
483         self.db = self.client.db
484         self.cl = self.db.classes[self.classname]
485         self.properties = self.cl.getprops()
487         TemplateFunctions.__init__(self)
489     col_re=re.compile(r'<property\s+name="([^>]+)">')
490     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
491             show_display_form=1, nodeids=None, show_customization=1):
492         self.filterspec = filterspec
494         w = self.client.write
496         # get the filter template
497         try:
498             filter_template = open(os.path.join(self.templates,
499                 self.classname+'.filter')).read()
500             all_filters = self.col_re.findall(filter_template)
501         except IOError, error:
502             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
503             filter_template = None
504             all_filters = []
506         # XXX deviate from spec here ...
507         # load the index section template and figure the default columns from it
508         template = open(os.path.join(self.templates,
509             self.classname+'.index')).read()
510         all_columns = self.col_re.findall(template)
511         if not columns:
512             columns = []
513             for name in all_columns:
514                 columns.append(name)
515         else:
516             # re-sort columns to be the same order as all_columns
517             l = []
518             for name in all_columns:
519                 if name in columns:
520                     l.append(name)
521             columns = l
523         # display the filter section
524         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
525                 self.client.FILTER_POSITION in ('top and bottom', 'top')):
526             w('<form action="index">\n')
527             self.filter_section(filter_template, filter, columns, group,
528                 all_filters, all_columns, show_customization)
529             # make sure that the sorting doesn't get lost either
530             if sort:
531                 w('<input type="hidden" name=":sort" value="%s">'%
532                     ','.join(sort))
533             w('</form>\n')
536         # now display the index section
537         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
538         w('<tr class="list-header">\n')
539         for name in columns:
540             cname = name.capitalize()
541             if show_display_form:
542                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
543                 anchor = "%s?%s"%(self.classname, sb)
544                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
545                     anchor, cname))
546             else:
547                 w('<td><span class="list-header">%s</span></td>\n'%cname)
548         w('</tr>\n')
550         # this stuff is used for group headings - optimise the group names
551         old_group = None
552         group_names = []
553         if group:
554             for name in group:
555                 if name[0] == '-': group_names.append(name[1:])
556                 else: group_names.append(name)
558         # now actually loop through all the nodes we get from the filter and
559         # apply the template
560         if nodeids is None:
561             nodeids = self.cl.filter(filterspec, sort, group)
562         for nodeid in nodeids:
563             # check for a group heading
564             if group_names:
565                 this_group = [self.cl.get(nodeid, name) for name in group_names]
566                 if this_group != old_group:
567                     l = []
568                     for name in group_names:
569                         prop = self.properties[name]
570                         if isinstance(prop, hyperdb.Link):
571                             group_cl = self.db.classes[prop.classname]
572                             key = group_cl.getkey()
573                             value = self.cl.get(nodeid, name)
574                             if value is None:
575                                 l.append('[unselected %s]'%prop.classname)
576                             else:
577                                 l.append(group_cl.get(self.cl.get(nodeid,
578                                     name), key))
579                         elif isinstance(prop, hyperdb.Multilink):
580                             group_cl = self.db.classes[prop.classname]
581                             key = group_cl.getkey()
582                             for value in self.cl.get(nodeid, name):
583                                 l.append(group_cl.get(value, key))
584                         else:
585                             value = self.cl.get(nodeid, name)
586                             if value is None:
587                                 value = '[empty %s]'%name
588                             else:
589                                 value = str(value)
590                             l.append(value)
591                     w('<tr class="section-bar">'
592                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
593                         len(columns), ', '.join(l)))
594                     old_group = this_group
596             # display this node's row
597             replace = IndexTemplateReplace(self.globals, locals(), columns)
598             self.nodeid = nodeid
599             w(replace.go(template))
600             self.nodeid = None
602         w('</table>')
604         # display the filter section
605         if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
606                 self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
607             w('<form action="index">\n')
608             self.filter_section(filter_template, filter, columns, group,
609                 all_filters, all_columns, show_customization)
610             # make sure that the sorting doesn't get lost either
611             if sort:
612                 w('<input type="hidden" name=":sort" value="%s">'%
613                     ','.join(sort))
614             w('</form>\n')
617     def filter_section(self, template, filter, columns, group, all_filters,
618             all_columns, show_customization):
620         w = self.client.write
622         if template and filter:
623             # display the filter section
624             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
625             w('<tr class="location-bar">')
626             w(' <th align="left" colspan="2">Filter specification...</th>')
627             w('</tr>')
628             replace = IndexTemplateReplace(self.globals, locals(), filter)
629             w(replace.go(template))
630             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
631             w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
632             w('</table>')
634         # now add in the filter/columns/group/etc config table form
635         w('<input type="hidden" name="show_customization" value="%s">' %
636             show_customization )
637         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
638         names = []
639         for name in self.properties.keys():
640             if name in all_filters or name in all_columns:
641                 names.append(name)
642         if show_customization:
643             action = '-'
644         else:
645             action = '+'
646             # hide the values for filters, columns and grouping in the form
647             # if the customization widget is not visible
648             for name in names:
649                 if all_filters and name in filter:
650                     w('<input type="hidden" name=":filter" value="%s">' % name)
651                 if all_columns and name in columns:
652                     w('<input type="hidden" name=":columns" value="%s">' % name)
653                 if all_columns and name in group:
654                     w('<input type="hidden" name=":group" value="%s">' % name)
656         # TODO: The widget style can go into the stylesheet
657         w('<th align="left" colspan=%s>'
658           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
659           'customisation...</th></tr>\n'%(len(names)+1, action))
661         if not show_customization:
662             w('</table>\n')
664         w('<tr class="location-bar"><th>&nbsp;</th>')
665         for name in names:
666             w('<th>%s</th>'%name.capitalize())
667         w('</tr>\n')
669         # Filter
670         if all_filters:
671             w('<tr><th width="1%" align=right class="location-bar">'
672               'Filters</th>\n')
673             for name in names:
674                 if name not in all_filters:
675                     w('<td>&nbsp;</td>')
676                     continue
677                 if name in filter: checked=' checked'
678                 else: checked=''
679                 w('<td align=middle>\n')
680                 w(' <input type="checkbox" name=":filter" value="%s" '
681                   '%s></td>\n'%(name, checked))
682             w('</tr>\n')
684         # Columns
685         if all_columns:
686             w('<tr><th width="1%" align=right class="location-bar">'
687               'Columns</th>\n')
688             for name in names:
689                 if name not in all_columns:
690                     w('<td>&nbsp;</td>')
691                     continue
692                 if name in columns: checked=' checked'
693                 else: checked=''
694                 w('<td align=middle>\n')
695                 w(' <input type="checkbox" name=":columns" value="%s"'
696                   '%s></td>\n'%(name, checked))
697             w('</tr>\n')
699             # Grouping
700             w('<tr><th width="1%" align=right class="location-bar">'
701               'Grouping</th>\n')
702             for name in names:
703                 prop = self.properties[name]
704                 if name not in all_columns:
705                     w('<td>&nbsp;</td>')
706                     continue
707                 if name in group: checked=' checked'
708                 else: checked=''
709                 w('<td align=middle>\n')
710                 w(' <input type="checkbox" name=":group" value="%s"'
711                   '%s></td>\n'%(name, checked))
712             w('</tr>\n')
714         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
715         w('<td colspan="%s">'%len(names))
716         w('<input type="submit" name="action" value="Redisplay"></td>')
717         w('</tr>\n')
718         w('</table>\n')
721     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
722         l = []
723         w = l.append
724         for k, v in filterspec.items():
725             k = urllib.quote(k)
726             if type(v) == type([]):
727                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
728             else:
729                 w('%s=%s'%(k, urllib.quote(v)))
730         if columns:
731             w(':columns=%s'%','.join(map(urllib.quote, columns)))
732         if filter:
733             w(':filter=%s'%','.join(map(urllib.quote, filter)))
734         if group:
735             w(':group=%s'%','.join(map(urllib.quote, group)))
736         m = []
737         s_dir = ''
738         for name in sort:
739             dir = name[0]
740             if dir == '-':
741                 name = name[1:]
742             else:
743                 dir = ''
744             if sort_name == name:
745                 if dir == '-':
746                     s_dir = ''
747                 else:
748                     s_dir = '-'
749             else:
750                 m.append(dir+urllib.quote(name))
751         m.insert(0, s_dir+urllib.quote(sort_name))
752         # so things don't get completely out of hand, limit the sort to
753         # two columns
754         w(':sort=%s'%','.join(m[:2]))
755         return '&'.join(l)
758 #   ITEM TEMPLATES
760 class ItemTemplateReplace:
761     def __init__(self, globals, locals, cl, nodeid):
762         self.globals = globals
763         self.locals = locals
764         self.cl = cl
765         self.nodeid = nodeid
767     replace=re.compile(
768         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
769         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
770     def go(self, text):
771         return self.replace.sub(self, text)
773     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
774         if m.group('name'):
775             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
776                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
777                     self.nodeid)
778                 return replace.go(m.group('text'))
779             else:
780                 return ''
781         if m.group('display'):
782             command = m.group('command')
783             return eval(command, self.globals, self.locals)
784         print '*** unhandled match', m.groupdict()
787 class ItemTemplate(TemplateFunctions):
788     def __init__(self, client, templates, classname):
789         self.client = client
790         self.templates = templates
791         self.classname = classname
793         # derived
794         self.db = self.client.db
795         self.cl = self.db.classes[self.classname]
796         self.properties = self.cl.getprops()
798         TemplateFunctions.__init__(self)
800     def render(self, nodeid):
801         self.nodeid = nodeid
803         if (self.properties.has_key('type') and
804                 self.properties.has_key('content')):
805             pass
806             # XXX we really want to return this as a downloadable...
807             #  currently I handle this at a higher level by detecting 'file'
808             #  designators...
810         w = self.client.write
811         w('<form action="%s%s">'%(self.classname, nodeid))
812         s = open(os.path.join(self.templates, self.classname+'.item')).read()
813         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
814         w(replace.go(s))
815         w('</form>')
818 class NewItemTemplate(TemplateFunctions):
819     def __init__(self, client, templates, classname):
820         self.client = client
821         self.templates = templates
822         self.classname = classname
824         # derived
825         self.db = self.client.db
826         self.cl = self.db.classes[self.classname]
827         self.properties = self.cl.getprops()
829         TemplateFunctions.__init__(self)
831     def render(self, form):
832         self.form = form
833         w = self.client.write
834         c = self.classname
835         try:
836             s = open(os.path.join(self.templates, c+'.newitem')).read()
837         except:
838             s = open(os.path.join(self.templates, c+'.item')).read()
839         w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
840         for key in form.keys():
841             if key[0] == ':':
842                 value = form[key].value
843                 if type(value) != type([]): value = [value]
844                 for value in value:
845                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
846         replace = ItemTemplateReplace(self.globals, locals(), None, None)
847         w(replace.go(s))
848         w('</form>')
851 # $Log: not supported by cvs2svn $
852 # Revision 1.39  2001/11/03 01:43:47  richard
853 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
855 # Revision 1.38  2001/10/31 06:58:51  richard
856 # Added the wrap="hard" attribute to the textarea of the note field so the
857 # messages wrap sanely.
859 # Revision 1.37  2001/10/31 06:24:35  richard
860 # Added do_stext to htmltemplate, thanks Brad Clements.
862 # Revision 1.36  2001/10/28 22:51:38  richard
863 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
865 # Revision 1.35  2001/10/24 00:04:41  richard
866 # Removed the "infinite authentication loop", thanks Roch'e
868 # Revision 1.34  2001/10/23 22:56:36  richard
869 # Bugfix in filter "widget" placement, thanks Roch'e
871 # Revision 1.33  2001/10/23 01:00:18  richard
872 # Re-enabled login and registration access after lopping them off via
873 # disabling access for anonymous users.
874 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
875 # a couple of bugs while I was there. Probably introduced a couple, but
876 # things seem to work OK at the moment.
878 # Revision 1.32  2001/10/22 03:25:01  richard
879 # Added configuration for:
880 #  . anonymous user access and registration (deny/allow)
881 #  . filter "widget" location on index page (top, bottom, both)
882 # Updated some documentation.
884 # Revision 1.31  2001/10/21 07:26:35  richard
885 # feature #473127: Filenames. I modified the file.index and htmltemplate
886 #  source so that the filename is used in the link and the creation
887 #  information is displayed.
889 # Revision 1.30  2001/10/21 04:44:50  richard
890 # bug #473124: UI inconsistency with Link fields.
891 #    This also prompted me to fix a fairly long-standing usability issue -
892 #    that of being able to turn off certain filters.
894 # Revision 1.29  2001/10/21 00:17:56  richard
895 # CGI interface view customisation section may now be hidden (patch from
896 #  Roch'e Compaan.)
898 # Revision 1.28  2001/10/21 00:00:16  richard
899 # Fixed Checklist function - wasn't always working on a list.
901 # Revision 1.27  2001/10/20 12:13:44  richard
902 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
904 # Revision 1.26  2001/10/14 10:55:00  richard
905 # Handle empty strings in HTML template Link function
907 # Revision 1.25  2001/10/09 07:25:59  richard
908 # Added the Password property type. See "pydoc roundup.password" for
909 # implementation details. Have updated some of the documentation too.
911 # Revision 1.24  2001/09/27 06:45:58  richard
912 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
913 # on the plain() template function to escape the text for HTML.
915 # Revision 1.23  2001/09/10 09:47:18  richard
916 # Fixed bug in the generation of links to Link/Multilink in indexes.
917 #   (thanks Hubert Hoegl)
918 # Added AssignedTo to the "classic" schema's item page.
920 # Revision 1.22  2001/08/30 06:01:17  richard
921 # Fixed missing import in mailgw :(
923 # Revision 1.21  2001/08/16 07:34:59  richard
924 # better CGI text searching - but hidden filter fields are disappearing...
926 # Revision 1.20  2001/08/15 23:43:18  richard
927 # Fixed some isFooTypes that I missed.
928 # Refactored some code in the CGI code.
930 # Revision 1.19  2001/08/12 06:32:36  richard
931 # using isinstance(blah, Foo) now instead of isFooType
933 # Revision 1.18  2001/08/07 00:24:42  richard
934 # stupid typo
936 # Revision 1.17  2001/08/07 00:15:51  richard
937 # Added the copyright/license notice to (nearly) all files at request of
938 # Bizar Software.
940 # Revision 1.16  2001/08/01 03:52:23  richard
941 # Checklist was using wrong name.
943 # Revision 1.15  2001/07/30 08:12:17  richard
944 # Added time logging and file uploading to the templates.
946 # Revision 1.14  2001/07/30 06:17:45  richard
947 # Features:
948 #  . Added ability for cgi newblah forms to indicate that the new node
949 #    should be linked somewhere.
950 # Fixed:
951 #  . Fixed the agument handling for the roundup-admin find command.
952 #  . Fixed handling of summary when no note supplied for newblah. Again.
953 #  . Fixed detection of no form in htmltemplate Field display.
955 # Revision 1.13  2001/07/30 02:37:53  richard
956 # Temporary measure until we have decent schema migration.
958 # Revision 1.12  2001/07/30 01:24:33  richard
959 # Handles new node display now.
961 # Revision 1.11  2001/07/29 09:31:35  richard
962 # oops
964 # Revision 1.10  2001/07/29 09:28:23  richard
965 # Fixed sorting by clicking on column headings.
967 # Revision 1.9  2001/07/29 08:27:40  richard
968 # Fixed handling of passed-in values in form elements (ie. during a
969 # drill-down)
971 # Revision 1.8  2001/07/29 07:01:39  richard
972 # Added vim command to all source so that we don't get no steenkin' tabs :)
974 # Revision 1.7  2001/07/29 05:36:14  richard
975 # Cleanup of the link label generation.
977 # Revision 1.6  2001/07/29 04:06:42  richard
978 # Fixed problem in link display when Link value is None.
980 # Revision 1.5  2001/07/28 08:17:09  richard
981 # fixed use of stylesheet
983 # Revision 1.4  2001/07/28 07:59:53  richard
984 # Replaced errno integers with their module values.
985 # De-tabbed templatebuilder.py
987 # Revision 1.3  2001/07/25 03:39:47  richard
988 # Hrm - displaying links to classes that don't specify a key property. I've
989 # got it defaulting to 'name', then 'title' and then a "random" property (first
990 # one returned by getprops().keys().
991 # Needs to be moved onto the Class I think...
993 # Revision 1.2  2001/07/22 12:09:32  richard
994 # Final commit of Grande Splite
996 # Revision 1.1  2001/07/22 11:58:35  richard
997 # More Grande Splite
1000 # vim: set filetype=python ts=4 sw=4 et si