Code

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