Code

c60ddfd0726789625245969ab1b5f78545bcdffd
[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.62 2002-01-18 08:36:12 grubert Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno
26 import hyperdb, date, password
27 from i18n import _
29 # This imports the StructureText functionality for the do_stext function
30 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
31 try:
32     from StructuredText.StructuredText import HTML as StructuredText
33 except ImportError:
34     StructuredText = None
36 class TemplateFunctions:
37     def __init__(self):
38         self.form = None
39         self.nodeid = None
40         self.filterspec = None
41         self.globals = {}
42         for key in TemplateFunctions.__dict__.keys():
43             if key[:3] == 'do_':
44                 self.globals[key[3:]] = getattr(self, key)
46     def do_plain(self, property, escape=0):
47         ''' display a String property directly;
49             display a Date property in a specified time zone with an option to
50             omit the time from the date stamp;
52             for a Link or Multilink property, display the key strings of the
53             linked nodes (or the ids if the linked class has no key property)
54         '''
55         if not self.nodeid and self.form is None:
56             return _('[Field: not called from item]')
57         propclass = self.properties[property]
58         if self.nodeid:
59             # make sure the property is a valid one
60             # TODO: this tests, but we should handle the exception
61             prop_test = self.cl.getprops()[property]
63             # get the value for this property
64             try:
65                 value = self.cl.get(self.nodeid, property)
66             except KeyError:
67                 # a KeyError here means that the node doesn't have a value
68                 # for the specified property
69                 if isinstance(propclass, hyperdb.Multilink): value = []
70                 else: value = ''
71         else:
72             # TODO: pull the value from the form
73             if isinstance(propclass, hyperdb.Multilink): value = []
74             else: value = ''
75         if isinstance(propclass, hyperdb.String):
76             if value is None: value = ''
77             else: value = str(value)
78         elif isinstance(propclass, hyperdb.Password):
79             if value is None: value = ''
80             else: value = _('*encrypted*')
81         elif isinstance(propclass, hyperdb.Date):
82             # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
83             value = str(value)
84         elif isinstance(propclass, hyperdb.Interval):
85             value = str(value)
86         elif isinstance(propclass, hyperdb.Link):
87             linkcl = self.db.classes[propclass.classname]
88             k = linkcl.labelprop()
89             if value:
90                 value = linkcl.get(value, k)
91             else:
92                 value = _('[unselected]')
93         elif isinstance(propclass, hyperdb.Multilink):
94             linkcl = self.db.classes[propclass.classname]
95             k = linkcl.labelprop()
96             value = ', '.join(value)
97         else:
98             s = _('Plain: bad propclass "%(propclass)s"')%locals()
99         if escape:
100             value = cgi.escape(value)
101         return value
103     def do_stext(self, property, escape=0):
104         '''Render as structured text using the StructuredText module
105            (see above for details)
106         '''
107         s = self.do_plain(property, escape=escape)
108         if not StructuredText:
109             return s
110         return StructuredText(s,level=1,header=0)
112     def do_field(self, property, size=None, height=None, showid=0):
113         ''' display a property like the plain displayer, but in a text field
114             to be edited
115         '''
116         if not self.nodeid and self.form is None and self.filterspec is None:
117             return _('[Field: not called from item]')
118         propclass = self.properties[property]
119         if (isinstance(propclass, hyperdb.Link) or
120             isinstance(propclass, hyperdb.Multilink)):
121             linkcl = self.db.classes[propclass.classname]
122             def sortfunc(a, b, cl=linkcl):
123                 if cl.getprops().has_key('order'):
124                     sort_on = 'order'
125                 else:
126                     sort_on = cl.labelprop()
127                 r = cmp(cl.get(a, sort_on), cl.get(b, sort_on))
128                 return r
129         if self.nodeid:
130             value = self.cl.get(self.nodeid, property, None)
131             # TODO: remove this from the code ... it's only here for
132             # handling schema changes, and they should be handled outside
133             # of this code...
134             if isinstance(propclass, hyperdb.Multilink) and value is None:
135                 value = []
136         elif self.filterspec is not None:
137             if isinstance(propclass, hyperdb.Multilink):
138                 value = self.filterspec.get(property, [])
139             else:
140                 value = self.filterspec.get(property, '')
141         else:
142             # TODO: pull the value from the form
143             if isinstance(propclass, hyperdb.Multilink): value = []
144             else: value = ''
145         if (isinstance(propclass, hyperdb.String) or
146                 isinstance(propclass, hyperdb.Date) or
147                 isinstance(propclass, hyperdb.Interval)):
148             size = size or 30
149             if value is None:
150                 value = ''
151             else:
152                 value = cgi.escape(str(value))
153                 value = '"'.join(value.split('"'))
154             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
155         elif isinstance(propclass, hyperdb.Password):
156             size = size or 30
157             s = '<input type="password" name="%s" size="%s">'%(property, size)
158         elif isinstance(propclass, hyperdb.Link):
159             l = ['<select name="%s">'%property]
160             k = linkcl.labelprop()
161             if value is None:
162                 s = 'selected '
163             else:
164                 s = ''
165             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
166             options = linkcl.list()
167             options.sort(sortfunc)
168             for optionid in options:
169                 option = linkcl.get(optionid, k)
170                 s = ''
171                 if optionid == value:
172                     s = 'selected '
173                 if showid:
174                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
175                 else:
176                     lab = option
177                 if size is not None and len(lab) > size:
178                     lab = lab[:size-3] + '...'
179                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
180             l.append('</select>')
181             s = '\n'.join(l)
182         elif isinstance(propclass, hyperdb.Multilink):
183             list = linkcl.list()
184             list.sort(sortfunc)
185             l = []
186             # map the id to the label property
187             # TODO: allow reversion to the older <select> box style display
188             if not showid:
189                 k = linkcl.labelprop()
190                 value = [linkcl.get(v, k) for v in value]
191             if size is None:
192                 size = '10'
193             l.insert(0,'<input name="%s" size="%s" value="%s">'%(property, 
194                 size, ','.join(value)))
195             s = "<br>\n".join(l)
196         else:
197             s = _('Plain: bad propclass "%(propclass)s"')%locals()
198         return s
200     def do_menu(self, property, size=None, height=None, showid=0):
201         ''' for a Link property, display a menu of the available choices
202         '''
203         propclass = self.properties[property]
204         if self.nodeid:
205             value = self.cl.get(self.nodeid, property)
206         else:
207             # TODO: pull the value from the form
208             if isinstance(propclass, hyperdb.Multilink): value = []
209             else: value = None
210         if isinstance(propclass, hyperdb.Link):
211             linkcl = self.db.classes[propclass.classname]
212             l = ['<select name="%s">'%property]
213             k = linkcl.labelprop()
214             s = ''
215             if value is None:
216                 s = 'selected '
217             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
218             for optionid in linkcl.list():
219                 option = linkcl.get(optionid, k)
220                 s = ''
221                 if optionid == value:
222                     s = 'selected '
223                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
224             l.append('</select>')
225             return '\n'.join(l)
226         if isinstance(propclass, hyperdb.Multilink):
227             linkcl = self.db.classes[propclass.classname]
228             list = linkcl.list()
229             height = height or min(len(list), 7)
230             l = ['<select multiple name="%s" size="%s">'%(property, height)]
231             k = linkcl.labelprop()
232             for optionid in list:
233                 option = linkcl.get(optionid, k)
234                 s = ''
235                 if optionid in value:
236                     s = 'selected '
237                 if showid:
238                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
239                 else:
240                     lab = option
241                 if size is not None and len(lab) > size:
242                     lab = lab[:size-3] + '...'
243                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
244             l.append('</select>')
245             return '\n'.join(l)
246         return _('[Menu: not a link]')
248     #XXX deviates from spec
249     def do_link(self, property=None, is_download=0):
250         '''For a Link or Multilink property, display the names of the linked
251            nodes, hyperlinked to the item views on those nodes.
252            For other properties, link to this node with the property as the
253            text.
255            If is_download is true, append the property value to the generated
256            URL so that the link may be used as a download link and the
257            downloaded file name is correct.
258         '''
259         if not self.nodeid and self.form is None:
260             return _('[Link: not called from item]')
261         propclass = self.properties[property]
262         if self.nodeid:
263             value = self.cl.get(self.nodeid, property)
264         else:
265             if isinstance(propclass, hyperdb.Multilink): value = []
266             elif isinstance(propclass, hyperdb.Link): value = None
267             else: value = ''
268         if isinstance(propclass, hyperdb.Link):
269             linkname = propclass.classname
270             if value is None: return '[no %s]'%property.capitalize()
271             linkcl = self.db.classes[linkname]
272             k = linkcl.labelprop()
273             linkvalue = linkcl.get(value, k)
274             if is_download:
275                 return '<a href="%s%s/%s">%s</a>'%(linkname, value,
276                     linkvalue, linkvalue)
277             else:
278                 return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
279         if isinstance(propclass, hyperdb.Multilink):
280             linkname = propclass.classname
281             linkcl = self.db.classes[linkname]
282             k = linkcl.labelprop()
283             if not value:
284                 return _('[no %(propname)s]')%{'propname': property.capitalize()}
285             l = []
286             for value in value:
287                 linkvalue = linkcl.get(value, k)
288                 if is_download:
289                     l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
290                         linkvalue, linkvalue))
291                 else:
292                     l.append('<a href="%s%s">%s</a>'%(linkname, value,
293                         linkvalue))
294             return ', '.join(l)
295         if isinstance(propclass, hyperdb.String) and value == '':
296             return _('[no %(propname)s]')%{'propname': property.capitalize()}
297         if is_download:
298             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
299                 value, value)
300         else:
301             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
303     def do_count(self, property, **args):
304         ''' for a Multilink property, display a count of the number of links in
305             the list
306         '''
307         if not self.nodeid:
308             return _('[Count: not called from item]')
309         propclass = self.properties[property]
310         value = self.cl.get(self.nodeid, property)
311         if isinstance(propclass, hyperdb.Multilink):
312             return str(len(value))
313         return _('[Count: not a Multilink]')
315     # XXX pretty is definitely new ;)
316     def do_reldate(self, property, pretty=0):
317         ''' display a Date property in terms of an interval relative to the
318             current date (e.g. "+ 3w", "- 2d").
320             with the 'pretty' flag, make it pretty
321         '''
322         if not self.nodeid and self.form is None:
323             return _('[Reldate: not called from item]')
324         propclass = self.properties[property]
325         if isinstance(not propclass, hyperdb.Date):
326             return _('[Reldate: not a Date]')
327         if self.nodeid:
328             value = self.cl.get(self.nodeid, property)
329         else:
330             value = date.Date('.')
331         interval = value - date.Date('.')
332         if pretty:
333             if not self.nodeid:
334                 return _('now')
335             pretty = interval.pretty()
336             if pretty is None:
337                 pretty = value.pretty()
338             return pretty
339         return str(interval)
341     def do_download(self, property, **args):
342         ''' show a Link("file") or Multilink("file") property using links that
343             allow you to download files
344         '''
345         if not self.nodeid:
346             return _('[Download: not called from item]')
347         propclass = self.properties[property]
348         value = self.cl.get(self.nodeid, property)
349         if isinstance(propclass, hyperdb.Link):
350             linkcl = self.db.classes[propclass.classname]
351             linkvalue = linkcl.get(value, k)
352             return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
353         if isinstance(propclass, hyperdb.Multilink):
354             linkcl = self.db.classes[propclass.classname]
355             l = []
356             for value in value:
357                 linkvalue = linkcl.get(value, k)
358                 l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
359             return ', '.join(l)
360         return _('[Download: not a link]')
363     def do_checklist(self, property, **args):
364         ''' for a Link or Multilink property, display checkboxes for the
365             available choices to permit filtering
366         '''
367         propclass = self.properties[property]
368         if (not isinstance(propclass, hyperdb.Link) and not
369                 isinstance(propclass, hyperdb.Multilink)):
370             return _('[Checklist: not a link]')
372         # get our current checkbox state
373         if self.nodeid:
374             # get the info from the node - make sure it's a list
375             if isinstance(propclass, hyperdb.Link):
376                 value = [self.cl.get(self.nodeid, property)]
377             else:
378                 value = self.cl.get(self.nodeid, property)
379         elif self.filterspec is not None:
380             # get the state from the filter specification (always a list)
381             value = self.filterspec.get(property, [])
382         else:
383             # it's a new node, so there's no state
384             value = []
386         # so we can map to the linked node's "lable" property
387         linkcl = self.db.classes[propclass.classname]
388         l = []
389         k = linkcl.labelprop()
390         for optionid in linkcl.list():
391             option = linkcl.get(optionid, k)
392             if optionid in value or option in value:
393                 checked = 'checked'
394             else:
395                 checked = ''
396             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
397                 option, checked, property, option))
399         # for Links, allow the "unselected" option too
400         if isinstance(propclass, hyperdb.Link):
401             if value is None or '-1' in value:
402                 checked = 'checked'
403             else:
404                 checked = ''
405             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
406                 'value="-1">')%(checked, property))
407         return '\n'.join(l)
409     def do_note(self, rows=5, cols=80):
410         ''' display a "note" field, which is a text area for entering a note to
411             go along with a change. 
412         '''
413         # TODO: pull the value from the form
414         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
415             '</textarea>'%(rows, cols)
417     # XXX new function
418     def do_list(self, property, reverse=0):
419         ''' list the items specified by property using the standard index for
420             the class
421         '''
422         propcl = self.properties[property]
423         if not isinstance(propcl, hyperdb.Multilink):
424             return _('[List: not a Multilink]')
425         value = self.cl.get(self.nodeid, property)
426         if reverse:
427             value.reverse()
429         # render the sub-index into a string
430         fp = StringIO.StringIO()
431         try:
432             write_save = self.client.write
433             self.client.write = fp.write
434             index = IndexTemplate(self.client, self.templates, propcl.classname)
435             index.render(nodeids=value, show_display_form=0)
436         finally:
437             self.client.write = write_save
439         return fp.getvalue()
441     # XXX new function
442     def do_history(self, direction='descending'):
443         ''' list the history of the item
445             If "direction" is 'descending' then the most recent event will
446             be displayed first. If it is 'ascending' then the oldest event
447             will be displayed first.
448         '''
449         if self.nodeid is None:
450             return _("[History: node doesn't exist]")
452         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
453             '<tr class="list-header">',
454             _('<th align=left><span class="list-item">Date</span></th>'),
455             _('<th align=left><span class="list-item">User</span></th>'),
456             _('<th align=left><span class="list-item">Action</span></th>'),
457             _('<th align=left><span class="list-item">Args</span></th>'),
458             '</tr>']
460         comments = {}
461         history = self.cl.history(self.nodeid)
462         if direction == 'descending':
463             history.reverse()
464         for id, evt_date, user, action, args in history:
465             date_s = str(evt_date).replace("."," ")
466             arg_s = ''
467             if action == 'link' and type(args) == type(()):
468                 if len(args) == 3:
469                     linkcl, linkid, key = args
470                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
471                         linkcl, linkid, key)
472                 else:
473                     arg_s = str(arg)
475             elif action == 'unlink' and type(args) == type(()):
476                 if len(args) == 3:
477                     linkcl, linkid, key = args
478                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
479                         linkcl, linkid, key)
480                 else:
481                     arg_s = str(arg)
483             elif type(args) == type({}):
484                 cell = []
485                 for k in args.keys():
486                     # try to get the relevant property and treat it
487                     # specially
488                     try:
489                         prop = self.properties[k]
490                     except:
491                         prop = None
492                     if prop is not None:
493                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
494                                 isinstance(prop, hyperdb.Link)):
495                             # figure what the link class is
496                             classname = prop.classname
497                             try:
498                                 linkcl = self.db.classes[classname]
499                             except KeyError, message:
500                                 labelprop = None
501                                 comments[classname] = _('''The linked class
502                                     %(classname)s no longer exists''')%locals()
503                             labelprop = linkcl.labelprop()
505                         if isinstance(prop, hyperdb.Multilink) and \
506                                 len(args[k]) > 0:
507                             ml = []
508                             for linkid in args[k]:
509                                 label = classname + linkid
510                                 # if we have a label property, try to use it
511                                 # TODO: test for node existence even when
512                                 # there's no labelprop!
513                                 try:
514                                     if labelprop is not None:
515                                         label = linkcl.get(linkid, labelprop)
516                                 except IndexError:
517                                     comments['no_link'] = _('''<strike>The
518                                         linked node no longer
519                                         exists</strike>''')
520                                     ml.append('<strike>%s</strike>'%label)
521                                 else:
522                                     ml.append('<a href="%s%s">%s</a>'%(
523                                         classname, linkid, label))
524                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
525                         elif isinstance(prop, hyperdb.Link) and args[k]:
526                             label = classname + args[k]
527                             # if we have a label property, try to use it
528                             # TODO: test for node existence even when
529                             # there's no labelprop!
530                             if labelprop is not None:
531                                 try:
532                                     label = linkcl.get(args[k], labelprop)
533                                 except IndexError:
534                                     comments['no_link'] = _('''<strike>The
535                                         linked node no longer
536                                         exists</strike>''')
537                                     cell.append(' <strike>%s</strike>,\n'%label)
538                             else:
539                                 cell.append('  <a href="%s%s">%s</a>,\n'%(
540                                     classname, linkid, label))
542                         elif isinstance(prop, hyperdb.Date) and args[k]:
543                             d = date.Date(args[k])
544                             cell.append('%s: %s'%(k, str(d)))
546                         elif isinstance(prop, hyperdb.Interval) and args[k]:
547                             d = date.Interval(args[k])
548                             cell.append('%s: %s'%(k, str(d)))
550                         elif not args[k]:
551                             cell.append('%s: (no value)\n'%k)
553                         else:
554                             cell.append('%s: %s\n'%(k, str(args[k])))
555                     else:
556                         # property no longer exists
557                         comments['no_exist'] = _('''<em>The indicated property
558                             no longer exists</em>''')
559                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
560                 arg_s = '<br />'.join(cell)
561             else:
562                 # unkown event!!
563                 comments['unknown'] = _('''<strong><em>This event is not
564                     handled by the history display!</em></strong>''')
565                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
566             date_s = date_s.replace(' ', '&nbsp;')
567             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
568                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
569                 user, action, arg_s))
570         if comments:
571             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
572         for entry in comments.values():
573             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
574         l.append('</table>')
575         return '\n'.join(l)
577     # XXX new function
578     def do_submit(self):
579         ''' add a submit button for the item
580         '''
581         if self.nodeid:
582             return _('<input type="submit" name="submit" value="Submit Changes">')
583         elif self.form is not None:
584             return _('<input type="submit" name="submit" value="Submit New Entry">')
585         else:
586             return _('[Submit: not called from item]')
590 #   INDEX TEMPLATES
592 class IndexTemplateReplace:
593     def __init__(self, globals, locals, props):
594         self.globals = globals
595         self.locals = locals
596         self.props = props
598     replace=re.compile(
599         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
600         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
601     def go(self, text):
602         return self.replace.sub(self, text)
604     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
605         if m.group('name'):
606             if m.group('name') in self.props:
607                 text = m.group('text')
608                 replace = IndexTemplateReplace(self.globals, {}, self.props)
609                 return replace.go(m.group('text'))
610             else:
611                 return ''
612         if m.group('display'):
613             command = m.group('command')
614             return eval(command, self.globals, self.locals)
615         print '*** unhandled match', m.groupdict()
617 class IndexTemplate(TemplateFunctions):
618     def __init__(self, client, templates, classname):
619         self.client = client
620         self.instance = client.instance
621         self.templates = templates
622         self.classname = classname
624         # derived
625         self.db = self.client.db
626         self.cl = self.db.classes[self.classname]
627         self.properties = self.cl.getprops()
629         TemplateFunctions.__init__(self)
631     col_re=re.compile(r'<property\s+name="([^>]+)">')
632     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
633             show_display_form=1, nodeids=None, show_customization=1):
634         self.filterspec = filterspec
636         w = self.client.write
638         # get the filter template
639         try:
640             filter_template = open(os.path.join(self.templates,
641                 self.classname+'.filter')).read()
642             all_filters = self.col_re.findall(filter_template)
643         except IOError, error:
644             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
645             filter_template = None
646             all_filters = []
648         # XXX deviate from spec here ...
649         # load the index section template and figure the default columns from it
650         template = open(os.path.join(self.templates,
651             self.classname+'.index')).read()
652         all_columns = self.col_re.findall(template)
653         if not columns:
654             columns = []
655             for name in all_columns:
656                 columns.append(name)
657         else:
658             # re-sort columns to be the same order as all_columns
659             l = []
660             for name in all_columns:
661                 if name in columns:
662                     l.append(name)
663             columns = l
665         # display the filter section
666         if (show_display_form and 
667                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
668             w('<form action="%s">\n'%self.classname)
669             self.filter_section(filter_template, filter, columns, group,
670                 all_filters, all_columns, show_customization)
671             # make sure that the sorting doesn't get lost either
672             if sort:
673                 w('<input type="hidden" name=":sort" value="%s">'%
674                     ','.join(sort))
675             w('</form>\n')
678         # now display the index section
679         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
680         w('<tr class="list-header">\n')
681         for name in columns:
682             cname = name.capitalize()
683             if show_display_form:
684                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
685                 anchor = "%s?%s"%(self.classname, sb)
686                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
687                     anchor, cname))
688             else:
689                 w('<td><span class="list-header">%s</span></td>\n'%cname)
690         w('</tr>\n')
692         # this stuff is used for group headings - optimise the group names
693         old_group = None
694         group_names = []
695         if group:
696             for name in group:
697                 if name[0] == '-': group_names.append(name[1:])
698                 else: group_names.append(name)
700         # now actually loop through all the nodes we get from the filter and
701         # apply the template
702         if nodeids is None:
703             nodeids = self.cl.filter(filterspec, sort, group)
704         for nodeid in nodeids:
705             # check for a group heading
706             if group_names:
707                 this_group = [self.cl.get(nodeid, name, _('[no value]')) for name in group_names]
708                 if this_group != old_group:
709                     l = []
710                     for name in group_names:
711                         prop = self.properties[name]
712                         if isinstance(prop, hyperdb.Link):
713                             group_cl = self.db.classes[prop.classname]
714                             key = group_cl.getkey()
715                             value = self.cl.get(nodeid, name)
716                             if value is None:
717                                 l.append(_('[unselected %(classname)s]')%{
718                                     'classname': prop.classname})
719                             else:
720                                 l.append(group_cl.get(self.cl.get(nodeid,
721                                     name), key))
722                         elif isinstance(prop, hyperdb.Multilink):
723                             group_cl = self.db.classes[prop.classname]
724                             key = group_cl.getkey()
725                             for value in self.cl.get(nodeid, name):
726                                 l.append(group_cl.get(value, key))
727                         else:
728                             value = self.cl.get(nodeid, name, _('[no value]'))
729                             if value is None:
730                                 value = _('[empty %(name)s]')%locals()
731                             else:
732                                 value = str(value)
733                             l.append(value)
734                     w('<tr class="section-bar">'
735                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
736                         len(columns), ', '.join(l)))
737                     old_group = this_group
739             # display this node's row
740             replace = IndexTemplateReplace(self.globals, locals(), columns)
741             self.nodeid = nodeid
742             w(replace.go(template))
743             self.nodeid = None
745         w('</table>')
747         # display the filter section
748         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
749                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
750             w('<form action="%s">\n'%self.classname)
751             self.filter_section(filter_template, filter, columns, group,
752                 all_filters, all_columns, show_customization)
753             # make sure that the sorting doesn't get lost either
754             if sort:
755                 w('<input type="hidden" name=":sort" value="%s">'%
756                     ','.join(sort))
757             w('</form>\n')
760     def filter_section(self, template, filter, columns, group, all_filters,
761             all_columns, show_customization):
763         w = self.client.write
765         # wrap the template in a single table to ensure the whole widget
766         # is displayed at once
767         w('<table><tr><td>')
769         if template and filter:
770             # display the filter section
771             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
772             w('<tr class="location-bar">')
773             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
774             w('</tr>')
775             replace = IndexTemplateReplace(self.globals, locals(), filter)
776             w(replace.go(template))
777             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
778             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
779             w('</table>')
781         # now add in the filter/columns/group/etc config table form
782         w('<input type="hidden" name="show_customization" value="%s">' %
783             show_customization )
784         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
785         names = []
786         for name in self.properties.keys():
787             if name in all_filters or name in all_columns:
788                 names.append(name)
789         if show_customization:
790             action = '-'
791         else:
792             action = '+'
793             # hide the values for filters, columns and grouping in the form
794             # if the customization widget is not visible
795             for name in names:
796                 if all_filters and name in filter:
797                     w('<input type="hidden" name=":filter" value="%s">' % name)
798                 if all_columns and name in columns:
799                     w('<input type="hidden" name=":columns" value="%s">' % name)
800                 if all_columns and name in group:
801                     w('<input type="hidden" name=":group" value="%s">' % name)
803         # TODO: The widget style can go into the stylesheet
804         w(_('<th align="left" colspan=%s>'
805           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
806           'customisation...</th></tr>\n')%(len(names)+1, action))
808         if not show_customization:
809             w('</table>\n')
810             return
812         w('<tr class="location-bar"><th>&nbsp;</th>')
813         for name in names:
814             w('<th>%s</th>'%name.capitalize())
815         w('</tr>\n')
817         # Filter
818         if all_filters:
819             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
820             for name in names:
821                 if name not in all_filters:
822                     w('<td>&nbsp;</td>')
823                     continue
824                 if name in filter: checked=' checked'
825                 else: checked=''
826                 w('<td align=middle>\n')
827                 w(' <input type="checkbox" name=":filter" value="%s" '
828                   '%s></td>\n'%(name, checked))
829             w('</tr>\n')
831         # Columns
832         if all_columns:
833             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
834             for name in names:
835                 if name not in all_columns:
836                     w('<td>&nbsp;</td>')
837                     continue
838                 if name in columns: checked=' checked'
839                 else: checked=''
840                 w('<td align=middle>\n')
841                 w(' <input type="checkbox" name=":columns" value="%s"'
842                   '%s></td>\n'%(name, checked))
843             w('</tr>\n')
845             # Grouping
846             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
847             for name in names:
848                 prop = self.properties[name]
849                 if name not in all_columns:
850                     w('<td>&nbsp;</td>')
851                     continue
852                 if name in group: checked=' checked'
853                 else: checked=''
854                 w('<td align=middle>\n')
855                 w(' <input type="checkbox" name=":group" value="%s"'
856                   '%s></td>\n'%(name, checked))
857             w('</tr>\n')
859         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
860         w('<td colspan="%s">'%len(names))
861         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
862         w('</tr>\n')
863         w('</table>\n')
865         # and the outer table
866         w('</td></tr></table>')
869     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
870         l = []
871         w = l.append
872         for k, v in filterspec.items():
873             k = urllib.quote(k)
874             if type(v) == type([]):
875                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
876             else:
877                 w('%s=%s'%(k, urllib.quote(v)))
878         if columns:
879             w(':columns=%s'%','.join(map(urllib.quote, columns)))
880         if filter:
881             w(':filter=%s'%','.join(map(urllib.quote, filter)))
882         if group:
883             w(':group=%s'%','.join(map(urllib.quote, group)))
884         m = []
885         s_dir = ''
886         for name in sort:
887             dir = name[0]
888             if dir == '-':
889                 name = name[1:]
890             else:
891                 dir = ''
892             if sort_name == name:
893                 if dir == '-':
894                     s_dir = ''
895                 else:
896                     s_dir = '-'
897             else:
898                 m.append(dir+urllib.quote(name))
899         m.insert(0, s_dir+urllib.quote(sort_name))
900         # so things don't get completely out of hand, limit the sort to
901         # two columns
902         w(':sort=%s'%','.join(m[:2]))
903         return '&'.join(l)
906 #   ITEM TEMPLATES
908 class ItemTemplateReplace:
909     def __init__(self, globals, locals, cl, nodeid):
910         self.globals = globals
911         self.locals = locals
912         self.cl = cl
913         self.nodeid = nodeid
915     replace=re.compile(
916         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
917         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
918     def go(self, text):
919         return self.replace.sub(self, text)
921     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
922         if m.group('name'):
923             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
924                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
925                     self.nodeid)
926                 return replace.go(m.group('text'))
927             else:
928                 return ''
929         if m.group('display'):
930             command = m.group('command')
931             return eval(command, self.globals, self.locals)
932         print '*** unhandled match', m.groupdict()
935 class ItemTemplate(TemplateFunctions):
936     def __init__(self, client, templates, classname):
937         self.client = client
938         self.instance = client.instance
939         self.templates = templates
940         self.classname = classname
942         # derived
943         self.db = self.client.db
944         self.cl = self.db.classes[self.classname]
945         self.properties = self.cl.getprops()
947         TemplateFunctions.__init__(self)
949     def render(self, nodeid):
950         self.nodeid = nodeid
952         if (self.properties.has_key('type') and
953                 self.properties.has_key('content')):
954             pass
955             # XXX we really want to return this as a downloadable...
956             #  currently I handle this at a higher level by detecting 'file'
957             #  designators...
959         w = self.client.write
960         w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
961             self.classname, nodeid))
962         s = open(os.path.join(self.templates, self.classname+'.item')).read()
963         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
964         w(replace.go(s))
965         w('</form>')
968 class NewItemTemplate(TemplateFunctions):
969     def __init__(self, client, templates, classname):
970         self.client = client
971         self.instance = client.instance
972         self.templates = templates
973         self.classname = classname
975         # derived
976         self.db = self.client.db
977         self.cl = self.db.classes[self.classname]
978         self.properties = self.cl.getprops()
980         TemplateFunctions.__init__(self)
982     def render(self, form):
983         self.form = form
984         w = self.client.write
985         c = self.classname
986         try:
987             s = open(os.path.join(self.templates, c+'.newitem')).read()
988         except IOError:
989             s = open(os.path.join(self.templates, c+'.item')).read()
990         w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
991         for key in form.keys():
992             if key[0] == ':':
993                 value = form[key].value
994                 if type(value) != type([]): value = [value]
995                 for value in value:
996                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
997         replace = ItemTemplateReplace(self.globals, locals(), None, None)
998         w(replace.go(s))
999         w('</form>')
1002 # $Log: not supported by cvs2svn $
1003 # Revision 1.61  2002/01/17 23:04:53  richard
1004 #  . much nicer history display (actualy real handling of property types etc)
1006 # Revision 1.60  2002/01/17 08:48:19  grubert
1007 #  . display superseder as html link in history.
1009 # Revision 1.59  2002/01/17 07:58:24  grubert
1010 #  . display links a html link in history.
1012 # Revision 1.58  2002/01/15 00:50:03  richard
1013 # #502949 ] index view for non-issues and redisplay
1015 # Revision 1.57  2002/01/14 23:31:21  richard
1016 # reverted the change that had plain() hyperlinking the link displays -
1017 # that's what link() is for!
1019 # Revision 1.56  2002/01/14 07:04:36  richard
1020 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1021 #    the linked node's page.
1022 #    ... this allows a display very similar to bugzilla's where you can actually
1023 #    find out information about the linked node.
1025 # Revision 1.55  2002/01/14 06:45:03  richard
1026 #  . #502953 ] nosy-like treatment of other multilinks
1027 #    ... had to revert most of the previous change to the multilink field
1028 #    display... not good.
1030 # Revision 1.54  2002/01/14 05:16:51  richard
1031 # The submit buttons need a name attribute or mozilla won't submit without a
1032 # file upload. Yeah, that's bloody obscure. Grr.
1034 # Revision 1.53  2002/01/14 04:03:32  richard
1035 # How about that ... date fields have never worked ...
1037 # Revision 1.52  2002/01/14 02:20:14  richard
1038 #  . changed all config accesses so they access either the instance or the
1039 #    config attriubute on the db. This means that all config is obtained from
1040 #    instance_config instead of the mish-mash of classes. This will make
1041 #    switching to a ConfigParser setup easier too, I hope.
1043 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1044 # 0.5.0 switch, I hope!)
1046 # Revision 1.51  2002/01/10 10:02:15  grubert
1047 # In do_history: replace "." in date by " " so html wraps more sensible.
1048 # Should this be done in date's string converter ?
1050 # Revision 1.50  2002/01/05 02:35:10  richard
1051 # I18N'ification
1053 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1054 # Features added:
1055 #  .  Multilink properties are now displayed as comma separated values in
1056 #     a textbox
1057 #  .  The add user link is now only visible to the admin user
1058 #  .  Modified the mail gateway to reject submissions from unknown
1059 #     addresses if ANONYMOUS_ACCESS is denied
1061 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1062 # Bugs fixed:
1063 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1064 #     lost somewhere
1065 #   . Internet Explorer submits full path for filename - we now strip away
1066 #     the path
1067 # Features added:
1068 #   . Link and multilink properties are now displayed sorted in the cgi
1069 #     interface
1071 # Revision 1.47  2001/11/26 22:55:56  richard
1072 # Feature:
1073 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1074 #    the instance.
1075 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1076 #    signature info in e-mails.
1077 #  . Some more flexibility in the mail gateway and more error handling.
1078 #  . Login now takes you to the page you back to the were denied access to.
1080 # Fixed:
1081 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1083 # Revision 1.46  2001/11/24 00:53:12  jhermann
1084 # "except:" is bad, bad , bad!
1086 # Revision 1.45  2001/11/22 15:46:42  jhermann
1087 # Added module docstrings to all modules.
1089 # Revision 1.44  2001/11/21 23:35:45  jhermann
1090 # Added globbing for win32, and sample marking in a 2nd file to test it
1092 # Revision 1.43  2001/11/21 04:04:43  richard
1093 # *sigh* more missing value handling
1095 # Revision 1.42  2001/11/21 03:40:54  richard
1096 # more new property handling
1098 # Revision 1.41  2001/11/15 10:26:01  richard
1099 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1101 # Revision 1.40  2001/11/03 01:56:51  richard
1102 # More HTML compliance fixes. This will probably fix the Netscape problem
1103 # too.
1105 # Revision 1.39  2001/11/03 01:43:47  richard
1106 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1108 # Revision 1.38  2001/10/31 06:58:51  richard
1109 # Added the wrap="hard" attribute to the textarea of the note field so the
1110 # messages wrap sanely.
1112 # Revision 1.37  2001/10/31 06:24:35  richard
1113 # Added do_stext to htmltemplate, thanks Brad Clements.
1115 # Revision 1.36  2001/10/28 22:51:38  richard
1116 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1118 # Revision 1.35  2001/10/24 00:04:41  richard
1119 # Removed the "infinite authentication loop", thanks Roch'e
1121 # Revision 1.34  2001/10/23 22:56:36  richard
1122 # Bugfix in filter "widget" placement, thanks Roch'e
1124 # Revision 1.33  2001/10/23 01:00:18  richard
1125 # Re-enabled login and registration access after lopping them off via
1126 # disabling access for anonymous users.
1127 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1128 # a couple of bugs while I was there. Probably introduced a couple, but
1129 # things seem to work OK at the moment.
1131 # Revision 1.32  2001/10/22 03:25:01  richard
1132 # Added configuration for:
1133 #  . anonymous user access and registration (deny/allow)
1134 #  . filter "widget" location on index page (top, bottom, both)
1135 # Updated some documentation.
1137 # Revision 1.31  2001/10/21 07:26:35  richard
1138 # feature #473127: Filenames. I modified the file.index and htmltemplate
1139 #  source so that the filename is used in the link and the creation
1140 #  information is displayed.
1142 # Revision 1.30  2001/10/21 04:44:50  richard
1143 # bug #473124: UI inconsistency with Link fields.
1144 #    This also prompted me to fix a fairly long-standing usability issue -
1145 #    that of being able to turn off certain filters.
1147 # Revision 1.29  2001/10/21 00:17:56  richard
1148 # CGI interface view customisation section may now be hidden (patch from
1149 #  Roch'e Compaan.)
1151 # Revision 1.28  2001/10/21 00:00:16  richard
1152 # Fixed Checklist function - wasn't always working on a list.
1154 # Revision 1.27  2001/10/20 12:13:44  richard
1155 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1157 # Revision 1.26  2001/10/14 10:55:00  richard
1158 # Handle empty strings in HTML template Link function
1160 # Revision 1.25  2001/10/09 07:25:59  richard
1161 # Added the Password property type. See "pydoc roundup.password" for
1162 # implementation details. Have updated some of the documentation too.
1164 # Revision 1.24  2001/09/27 06:45:58  richard
1165 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1166 # on the plain() template function to escape the text for HTML.
1168 # Revision 1.23  2001/09/10 09:47:18  richard
1169 # Fixed bug in the generation of links to Link/Multilink in indexes.
1170 #   (thanks Hubert Hoegl)
1171 # Added AssignedTo to the "classic" schema's item page.
1173 # Revision 1.22  2001/08/30 06:01:17  richard
1174 # Fixed missing import in mailgw :(
1176 # Revision 1.21  2001/08/16 07:34:59  richard
1177 # better CGI text searching - but hidden filter fields are disappearing...
1179 # Revision 1.20  2001/08/15 23:43:18  richard
1180 # Fixed some isFooTypes that I missed.
1181 # Refactored some code in the CGI code.
1183 # Revision 1.19  2001/08/12 06:32:36  richard
1184 # using isinstance(blah, Foo) now instead of isFooType
1186 # Revision 1.18  2001/08/07 00:24:42  richard
1187 # stupid typo
1189 # Revision 1.17  2001/08/07 00:15:51  richard
1190 # Added the copyright/license notice to (nearly) all files at request of
1191 # Bizar Software.
1193 # Revision 1.16  2001/08/01 03:52:23  richard
1194 # Checklist was using wrong name.
1196 # Revision 1.15  2001/07/30 08:12:17  richard
1197 # Added time logging and file uploading to the templates.
1199 # Revision 1.14  2001/07/30 06:17:45  richard
1200 # Features:
1201 #  . Added ability for cgi newblah forms to indicate that the new node
1202 #    should be linked somewhere.
1203 # Fixed:
1204 #  . Fixed the agument handling for the roundup-admin find command.
1205 #  . Fixed handling of summary when no note supplied for newblah. Again.
1206 #  . Fixed detection of no form in htmltemplate Field display.
1208 # Revision 1.13  2001/07/30 02:37:53  richard
1209 # Temporary measure until we have decent schema migration.
1211 # Revision 1.12  2001/07/30 01:24:33  richard
1212 # Handles new node display now.
1214 # Revision 1.11  2001/07/29 09:31:35  richard
1215 # oops
1217 # Revision 1.10  2001/07/29 09:28:23  richard
1218 # Fixed sorting by clicking on column headings.
1220 # Revision 1.9  2001/07/29 08:27:40  richard
1221 # Fixed handling of passed-in values in form elements (ie. during a
1222 # drill-down)
1224 # Revision 1.8  2001/07/29 07:01:39  richard
1225 # Added vim command to all source so that we don't get no steenkin' tabs :)
1227 # Revision 1.7  2001/07/29 05:36:14  richard
1228 # Cleanup of the link label generation.
1230 # Revision 1.6  2001/07/29 04:06:42  richard
1231 # Fixed problem in link display when Link value is None.
1233 # Revision 1.5  2001/07/28 08:17:09  richard
1234 # fixed use of stylesheet
1236 # Revision 1.4  2001/07/28 07:59:53  richard
1237 # Replaced errno integers with their module values.
1238 # De-tabbed templatebuilder.py
1240 # Revision 1.3  2001/07/25 03:39:47  richard
1241 # Hrm - displaying links to classes that don't specify a key property. I've
1242 # got it defaulting to 'name', then 'title' and then a "random" property (first
1243 # one returned by getprops().keys().
1244 # Needs to be moved onto the Class I think...
1246 # Revision 1.2  2001/07/22 12:09:32  richard
1247 # Final commit of Grande Splite
1249 # Revision 1.1  2001/07/22 11:58:35  richard
1250 # More Grande Splite
1253 # vim: set filetype=python ts=4 sw=4 et si