Code

Merged search_indexing-branch with HEAD
[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.90 2002-05-25 07:16:24 rochecompaan Exp $
20 __doc__ = """
21 Template engine.
22 """
24 import os, re, StringIO, urllib, cgi, errno, types, urllib
26 import hyperdb, date
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 MissingTemplateError(ValueError):
37     '''Error raised when a template file is missing
38     '''
39     pass
41 class TemplateFunctions:
42     '''Defines the templating functions that are used in the HTML templates
43        of the roundup web interface.
44     '''
45     def __init__(self):
46         self.form = None
47         self.nodeid = None
48         self.filterspec = None
49         self.globals = {}
50         for key in TemplateFunctions.__dict__.keys():
51             if key[:3] == 'do_':
52                 self.globals[key[3:]] = getattr(self, key)
54         # These are added by the subclass where appropriate
55         self.client = None
56         self.instance = None
57         self.templates = None
58         self.classname = None
59         self.db = None
60         self.cl = None
61         self.properties = None
63     def do_plain(self, property, escape=0):
64         ''' display a String property directly;
66             display a Date property in a specified time zone with an option to
67             omit the time from the date stamp;
69             for a Link or Multilink property, display the key strings of the
70             linked nodes (or the ids if the linked class has no key property)
71         '''
72         if not self.nodeid and self.form is None:
73             return _('[Field: not called from item]')
74         propclass = self.properties[property]
75         if self.nodeid:
76             # make sure the property is a valid one
77             # TODO: this tests, but we should handle the exception
78             dummy = self.cl.getprops()[property]
80             # get the value for this property
81             try:
82                 value = self.cl.get(self.nodeid, property)
83             except KeyError:
84                 # a KeyError here means that the node doesn't have a value
85                 # for the specified property
86                 if isinstance(propclass, hyperdb.Multilink): value = []
87                 else: value = ''
88         else:
89             # TODO: pull the value from the form
90             if isinstance(propclass, hyperdb.Multilink): value = []
91             else: value = ''
92         if isinstance(propclass, hyperdb.String):
93             if value is None: value = ''
94             else: value = str(value)
95         elif isinstance(propclass, hyperdb.Password):
96             if value is None: value = ''
97             else: value = _('*encrypted*')
98         elif isinstance(propclass, hyperdb.Date):
99             # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
100             value = str(value)
101         elif isinstance(propclass, hyperdb.Interval):
102             value = str(value)
103         elif isinstance(propclass, hyperdb.Link):
104             linkcl = self.db.classes[propclass.classname]
105             k = linkcl.labelprop()
106             if value:
107                 value = linkcl.get(value, k)
108             else:
109                 value = _('[unselected]')
110         elif isinstance(propclass, hyperdb.Multilink):
111             linkcl = self.db.classes[propclass.classname]
112             k = linkcl.labelprop()
113             labels = []
114             for v in value:
115                 labels.append(linkcl.get(v, k))
116             value = ', '.join(labels)
117         else:
118             value = _('Plain: bad propclass "%(propclass)s"')%locals()
119         if escape:
120             value = cgi.escape(value)
121         return value
123     def do_stext(self, property, escape=0):
124         '''Render as structured text using the StructuredText module
125            (see above for details)
126         '''
127         s = self.do_plain(property, escape=escape)
128         if not StructuredText:
129             return s
130         return StructuredText(s,level=1,header=0)
132     def determine_value(self, property):
133         '''determine the value of a property using the node, form or
134            filterspec
135         '''
136         propclass = self.properties[property]
137         if self.nodeid:
138             value = self.cl.get(self.nodeid, property, None)
139             if isinstance(propclass, hyperdb.Multilink) and value is None:
140                 return []
141             return value
142         elif self.filterspec is not None:
143             if isinstance(propclass, hyperdb.Multilink):
144                 return self.filterspec.get(property, [])
145             else:
146                 return self.filterspec.get(property, '')
147         # TODO: pull the value from the form
148         if isinstance(propclass, hyperdb.Multilink):
149             return []
150         else:
151             return ''
153     def make_sort_function(self, classname):
154         '''Make a sort function for a given class
155         '''
156         linkcl = self.db.classes[classname]
157         if linkcl.getprops().has_key('order'):
158             sort_on = 'order'
159         else:
160             sort_on = linkcl.labelprop()
161         def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
162             return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
163         return sortfunc
165     def do_field(self, property, size=None, showid=0):
166         ''' display a property like the plain displayer, but in a text field
167             to be edited
169             Note: if you would prefer an option list style display for
170             link or multilink editing, use menu().
171         '''
172         if not self.nodeid and self.form is None and self.filterspec is None:
173             return _('[Field: not called from item]')
175         if size is None:
176             size = 30
178         propclass = self.properties[property]
180         # get the value
181         value = self.determine_value(property)
183         # now display
184         if (isinstance(propclass, hyperdb.String) or
185                 isinstance(propclass, hyperdb.Date) or
186                 isinstance(propclass, hyperdb.Interval)):
187             if value is None:
188                 value = ''
189             else:
190                 value = cgi.escape(str(value))
191                 value = '"'.join(value.split('"'))
192             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
193         elif isinstance(propclass, hyperdb.Password):
194             s = '<input type="password" name="%s" size="%s">'%(property, size)
195         elif isinstance(propclass, hyperdb.Link):
196             sortfunc = self.make_sort_function(propclass.classname)
197             linkcl = self.db.classes[propclass.classname]
198             options = linkcl.list()
199             options.sort(sortfunc)
200             # TODO: make this a field display, not a menu one!
201             l = ['<select name="%s">'%property]
202             k = linkcl.labelprop()
203             if value is None:
204                 s = 'selected '
205             else:
206                 s = ''
207             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
208             for optionid in options:
209                 option = linkcl.get(optionid, k)
210                 s = ''
211                 if optionid == value:
212                     s = 'selected '
213                 if showid:
214                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
215                 else:
216                     lab = option
217                 if size is not None and len(lab) > size:
218                     lab = lab[:size-3] + '...'
219                 lab = cgi.escape(lab)
220                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
221             l.append('</select>')
222             s = '\n'.join(l)
223         elif isinstance(propclass, hyperdb.Multilink):
224             sortfunc = self.make_sort_function(propclass.classname)
225             linkcl = self.db.classes[propclass.classname]
226             if value:
227                 value.sort(sortfunc)
228             # map the id to the label property
229             if not showid:
230                 k = linkcl.labelprop()
231                 value = [linkcl.get(v, k) for v in value]
232             value = cgi.escape(','.join(value))
233             s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
234         else:
235             s = _('Plain: bad propclass "%(propclass)s"')%locals()
236         return s
238     def do_multiline(self, property, rows=5, cols=40):
239         ''' display a string property in a multiline text edit field
240         '''
241         if not self.nodeid and self.form is None and self.filterspec is None:
242             return _('[Multiline: not called from item]')
244         propclass = self.properties[property]
246         # make sure this is a link property
247         if not isinstance(propclass, hyperdb.String):
248             return _('[Multiline: not a string]')
250         # get the value
251         value = self.determine_value(property)
252         if value is None:
253             value = ''
255         # display
256         return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
257             property, rows, cols, value)
259     def do_menu(self, property, size=None, height=None, showid=0):
260         ''' for a Link property, display a menu of the available choices
261         '''
262         if not self.nodeid and self.form is None and self.filterspec is None:
263             return _('[Field: not called from item]')
265         propclass = self.properties[property]
267         # make sure this is a link property
268         if not (isinstance(propclass, hyperdb.Link) or
269                 isinstance(propclass, hyperdb.Multilink)):
270             return _('[Menu: not a link]')
272         # sort function
273         sortfunc = self.make_sort_function(propclass.classname)
275         # get the value
276         value = self.determine_value(property)
278         # display
279         if isinstance(propclass, hyperdb.Multilink):
280             linkcl = self.db.classes[propclass.classname]
281             options = linkcl.list()
282             options.sort(sortfunc)
283             height = height or min(len(options), 7)
284             l = ['<select multiple name="%s" size="%s">'%(property, height)]
285             k = linkcl.labelprop()
286             for optionid in options:
287                 option = linkcl.get(optionid, k)
288                 s = ''
289                 if optionid in value or option in value:
290                     s = 'selected '
291                 if showid:
292                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
293                 else:
294                     lab = option
295                 if size is not None and len(lab) > size:
296                     lab = lab[:size-3] + '...'
297                 lab = cgi.escape(lab)
298                 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
299                     lab))
300             l.append('</select>')
301             return '\n'.join(l)
302         if isinstance(propclass, hyperdb.Link):
303             # force the value to be a single choice
304             if type(value) is types.ListType:
305                 value = value[0]
306             linkcl = self.db.classes[propclass.classname]
307             l = ['<select name="%s">'%property]
308             k = linkcl.labelprop()
309             s = ''
310             if value is None:
311                 s = 'selected '
312             l.append(_('<option %svalue="-1">- no selection -</option>')%s)
313             options = linkcl.list()
314             options.sort(sortfunc)
315             for optionid in options:
316                 option = linkcl.get(optionid, k)
317                 s = ''
318                 if value in [optionid, option]:
319                     s = 'selected '
320                 if showid:
321                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
322                 else:
323                     lab = option
324                 if size is not None and len(lab) > size:
325                     lab = lab[:size-3] + '...'
326                 lab = cgi.escape(lab)
327                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
328             l.append('</select>')
329             return '\n'.join(l)
330         return _('[Menu: not a link]')
332     #XXX deviates from spec
333     def do_link(self, property=None, is_download=0, showid=0):
334         '''For a Link or Multilink property, display the names of the linked
335            nodes, hyperlinked to the item views on those nodes.
336            For other properties, link to this node with the property as the
337            text.
339            If is_download is true, append the property value to the generated
340            URL so that the link may be used as a download link and the
341            downloaded file name is correct.
342         '''
343         if not self.nodeid and self.form is None:
344             return _('[Link: not called from item]')
346         # get the value
347         value = self.determine_value(property)
348         if not value:
349             return _('[no %(propname)s]')%{'propname':property.capitalize()}
351         propclass = self.properties[property]
352         if isinstance(propclass, hyperdb.Link):
353             linkname = propclass.classname
354             linkcl = self.db.classes[linkname]
355             k = linkcl.labelprop()
356             linkvalue = cgi.escape(str(linkcl.get(value, k)))
357             if showid:
358                 label = value
359                 title = ' title="%s"'%linkvalue
360                 # note ... this should be urllib.quote(linkcl.get(value, k))
361             else:
362                 label = linkvalue
363                 title = ''
364             if is_download:
365                 return '<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
366                     linkvalue, title, label)
367             else:
368                 return '<a href="%s%s"%s>%s</a>'%(linkname, value, title, label)
369         if isinstance(propclass, hyperdb.Multilink):
370             linkname = propclass.classname
371             linkcl = self.db.classes[linkname]
372             k = linkcl.labelprop()
373             l = []
374             for value in value:
375                 linkvalue = cgi.escape(str(linkcl.get(value, k)))
376                 if showid:
377                     label = value
378                     title = ' title="%s"'%linkvalue
379                     # note ... this should be urllib.quote(linkcl.get(value, k))
380                 else:
381                     label = linkvalue
382                     title = ''
383                 if is_download:
384                     l.append('<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
385                         linkvalue, title, label))
386                 else:
387                     l.append('<a href="%s%s"%s>%s</a>'%(linkname, value,
388                         title, label))
389             return ', '.join(l)
390         if is_download:
391             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
392                 value, value)
393         else:
394             return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
396     def do_count(self, property, **args):
397         ''' for a Multilink property, display a count of the number of links in
398             the list
399         '''
400         if not self.nodeid:
401             return _('[Count: not called from item]')
403         propclass = self.properties[property]
404         if not isinstance(propclass, hyperdb.Multilink):
405             return _('[Count: not a Multilink]')
407         # figure the length then...
408         value = self.cl.get(self.nodeid, property)
409         return str(len(value))
411     # XXX pretty is definitely new ;)
412     def do_reldate(self, property, pretty=0):
413         ''' display a Date property in terms of an interval relative to the
414             current date (e.g. "+ 3w", "- 2d").
416             with the 'pretty' flag, make it pretty
417         '''
418         if not self.nodeid and self.form is None:
419             return _('[Reldate: not called from item]')
421         propclass = self.properties[property]
422         if not isinstance(propclass, hyperdb.Date):
423             return _('[Reldate: not a Date]')
425         if self.nodeid:
426             value = self.cl.get(self.nodeid, property)
427         else:
428             return ''
429         if not value:
430             return ''
432         # figure the interval
433         interval = date.Date('.') - value
434         if pretty:
435             if not self.nodeid:
436                 return _('now')
437             return interval.pretty()
438         return str(interval)
440     def do_download(self, property, **args):
441         ''' show a Link("file") or Multilink("file") property using links that
442             allow you to download files
443         '''
444         if not self.nodeid:
445             return _('[Download: not called from item]')
446         return self.do_link(property, is_download=1)
449     def do_checklist(self, property, **args):
450         ''' for a Link or Multilink property, display checkboxes for the
451             available choices to permit filtering
452         '''
453         propclass = self.properties[property]
454         if (not isinstance(propclass, hyperdb.Link) and not
455                 isinstance(propclass, hyperdb.Multilink)):
456             return _('[Checklist: not a link]')
458         # get our current checkbox state
459         if self.nodeid:
460             # get the info from the node - make sure it's a list
461             if isinstance(propclass, hyperdb.Link):
462                 value = [self.cl.get(self.nodeid, property)]
463             else:
464                 value = self.cl.get(self.nodeid, property)
465         elif self.filterspec is not None:
466             # get the state from the filter specification (always a list)
467             value = self.filterspec.get(property, [])
468         else:
469             # it's a new node, so there's no state
470             value = []
472         # so we can map to the linked node's "lable" property
473         linkcl = self.db.classes[propclass.classname]
474         l = []
475         k = linkcl.labelprop()
476         for optionid in linkcl.list():
477             option = cgi.escape(str(linkcl.get(optionid, k)))
478             if optionid in value or option in value:
479                 checked = 'checked'
480             else:
481                 checked = ''
482             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
483                 option, checked, property, option))
485         # for Links, allow the "unselected" option too
486         if isinstance(propclass, hyperdb.Link):
487             if value is None or '-1' in value:
488                 checked = 'checked'
489             else:
490                 checked = ''
491             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
492                 'value="-1">')%(checked, property))
493         return '\n'.join(l)
495     def do_note(self, rows=5, cols=80):
496         ''' display a "note" field, which is a text area for entering a note to
497             go along with a change. 
498         '''
499         # TODO: pull the value from the form
500         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
501             '</textarea>'%(rows, cols)
503     # XXX new function
504     def do_list(self, property, reverse=0):
505         ''' list the items specified by property using the standard index for
506             the class
507         '''
508         propcl = self.properties[property]
509         if not isinstance(propcl, hyperdb.Multilink):
510             return _('[List: not a Multilink]')
512         value = self.determine_value(property)
513         if not value:
514             return ''
516         # sort, possibly revers and then re-stringify
517         value = map(int, value)
518         value.sort()
519         if reverse:
520             value.reverse()
521         value = map(str, value)
523         # render the sub-index into a string
524         fp = StringIO.StringIO()
525         try:
526             write_save = self.client.write
527             self.client.write = fp.write
528             index = IndexTemplate(self.client, self.templates, propcl.classname)
529             index.render(nodeids=value, show_display_form=0)
530         finally:
531             self.client.write = write_save
533         return fp.getvalue()
535     # XXX new function
536     def do_history(self, direction='descending'):
537         ''' list the history of the item
539             If "direction" is 'descending' then the most recent event will
540             be displayed first. If it is 'ascending' then the oldest event
541             will be displayed first.
542         '''
543         if self.nodeid is None:
544             return _("[History: node doesn't exist]")
546         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
547             '<tr class="list-header">',
548             _('<th align=left><span class="list-item">Date</span></th>'),
549             _('<th align=left><span class="list-item">User</span></th>'),
550             _('<th align=left><span class="list-item">Action</span></th>'),
551             _('<th align=left><span class="list-item">Args</span></th>'),
552             '</tr>']
554         comments = {}
555         history = self.cl.history(self.nodeid)
556         history.sort()
557         if direction == 'descending':
558             history.reverse()
559         for id, evt_date, user, action, args in history:
560             date_s = str(evt_date).replace("."," ")
561             arg_s = ''
562             if action == 'link' and type(args) == type(()):
563                 if len(args) == 3:
564                     linkcl, linkid, key = args
565                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
566                         linkcl, linkid, key)
567                 else:
568                     arg_s = str(args)
570             elif action == 'unlink' and type(args) == type(()):
571                 if len(args) == 3:
572                     linkcl, linkid, key = args
573                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
574                         linkcl, linkid, key)
575                 else:
576                     arg_s = str(args)
578             elif type(args) == type({}):
579                 cell = []
580                 for k in args.keys():
581                     # try to get the relevant property and treat it
582                     # specially
583                     try:
584                         prop = self.properties[k]
585                     except:
586                         prop = None
587                     if prop is not None:
588                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
589                                 isinstance(prop, hyperdb.Link)):
590                             # figure what the link class is
591                             classname = prop.classname
592                             try:
593                                 linkcl = self.db.classes[classname]
594                             except KeyError:
595                                 labelprop = None
596                                 comments[classname] = _('''The linked class
597                                     %(classname)s no longer exists''')%locals()
598                             labelprop = linkcl.labelprop()
600                         if isinstance(prop, hyperdb.Multilink) and \
601                                 len(args[k]) > 0:
602                             ml = []
603                             for linkid in args[k]:
604                                 label = classname + linkid
605                                 # if we have a label property, try to use it
606                                 # TODO: test for node existence even when
607                                 # there's no labelprop!
608                                 try:
609                                     if labelprop is not None:
610                                         label = linkcl.get(linkid, labelprop)
611                                 except IndexError:
612                                     comments['no_link'] = _('''<strike>The
613                                         linked node no longer
614                                         exists</strike>''')
615                                     ml.append('<strike>%s</strike>'%label)
616                                 else:
617                                     ml.append('<a href="%s%s">%s</a>'%(
618                                         classname, linkid, label))
619                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
620                         elif isinstance(prop, hyperdb.Link) and args[k]:
621                             label = classname + args[k]
622                             # if we have a label property, try to use it
623                             # TODO: test for node existence even when
624                             # there's no labelprop!
625                             if labelprop is not None:
626                                 try:
627                                     label = linkcl.get(args[k], labelprop)
628                                 except IndexError:
629                                     comments['no_link'] = _('''<strike>The
630                                         linked node no longer
631                                         exists</strike>''')
632                                     cell.append(' <strike>%s</strike>,\n'%label)
633                                     # "flag" this is done .... euwww
634                                     label = None
635                             if label is not None:
636                                 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
637                                     classname, args[k], label))
639                         elif isinstance(prop, hyperdb.Date) and args[k]:
640                             d = date.Date(args[k])
641                             cell.append('%s: %s'%(k, str(d)))
643                         elif isinstance(prop, hyperdb.Interval) and args[k]:
644                             d = date.Interval(args[k])
645                             cell.append('%s: %s'%(k, str(d)))
647                         elif not args[k]:
648                             cell.append('%s: (no value)\n'%k)
650                         else:
651                             cell.append('%s: %s\n'%(k, str(args[k])))
652                     else:
653                         # property no longer exists
654                         comments['no_exist'] = _('''<em>The indicated property
655                             no longer exists</em>''')
656                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
657                 arg_s = '<br />'.join(cell)
658             else:
659                 # unkown event!!
660                 comments['unknown'] = _('''<strong><em>This event is not
661                     handled by the history display!</em></strong>''')
662                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
663             date_s = date_s.replace(' ', '&nbsp;')
664             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
665                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
666                 user, action, arg_s))
667         if comments:
668             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
669         for entry in comments.values():
670             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
671         l.append('</table>')
672         return '\n'.join(l)
674     # XXX new function
675     def do_submit(self):
676         ''' add a submit button for the item
677         '''
678         if self.nodeid:
679             return _('<input type="submit" name="submit" value="Submit Changes">')
680         elif self.form is not None:
681             return _('<input type="submit" name="submit" value="Submit New Entry">')
682         else:
683             return _('[Submit: not called from item]')
685     def do_classhelp(self, classname, properties, label='?', width='400',
686             height='400'):
687         '''pop up a javascript window with class help
689            This generates a link to a popup window which displays the 
690            properties indicated by "properties" of the class named by
691            "classname". The "properties" should be a comma-separated list
692            (eg. 'id,name,description').
694            You may optionally override the label displayed, the width and
695            height. The popup window will be resizable and scrollable.
696         '''
697         return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
698             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
699             properties, width, height, label)
701 #   INDEX TEMPLATES
703 class IndexTemplateReplace:
704     '''Regular-expression based parser that turns the template into HTML. 
705     '''
706     def __init__(self, globals, locals, props):
707         self.globals = globals
708         self.locals = locals
709         self.props = props
711     replace=re.compile(
712         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
713         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
714     def go(self, text):
715         return self.replace.sub(self, text)
717     def __call__(self, m, search_text=None, filter=None, columns=None,
718             sort=None, group=None):
719         if m.group('name'):
720             if m.group('name') in self.props:
721                 text = m.group('text')
722                 replace = IndexTemplateReplace(self.globals, {}, self.props)
723                 return replace.go(text)
724             else:
725                 return ''
726         if m.group('display'):
727             command = m.group('command')
728             return eval(command, self.globals, self.locals)
729         return '*** unhandled match: %s'%str(m.groupdict())
731 class IndexTemplate(TemplateFunctions):
732     '''Templating functionality specifically for index pages
733     '''
734     def __init__(self, client, templates, classname):
735         TemplateFunctions.__init__(self)
736         self.client = client
737         self.instance = client.instance
738         self.templates = templates
739         self.classname = classname
741         # derived
742         self.db = self.client.db
743         self.cl = self.db.classes[self.classname]
744         self.properties = self.cl.getprops()
746     col_re=re.compile(r'<property\s+name="([^>]+)">')
747     def render(self, filterspec={}, search_text='', filter=[], columns=[], 
748             sort=[], group=[], show_display_form=1, nodeids=None,
749             show_customization=1, show_nodes=1):
750         self.filterspec = filterspec
752         w = self.client.write
754         # get the filter template
755         try:
756             filter_template = open(os.path.join(self.templates,
757                 self.classname+'.filter')).read()
758             all_filters = self.col_re.findall(filter_template)
759         except IOError, error:
760             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
761             filter_template = None
762             all_filters = []
764         # XXX deviate from spec here ...
765         # load the index section template and figure the default columns from it
766         try:
767             template = open(os.path.join(self.templates,
768                 self.classname+'.index')).read()
769         except IOError, error:
770             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
771             raise MissingTemplateError, self.classname+'.index'
772         all_columns = self.col_re.findall(template)
773         if not columns:
774             columns = []
775             for name in all_columns:
776                 columns.append(name)
777         else:
778             # re-sort columns to be the same order as all_columns
779             l = []
780             for name in all_columns:
781                 if name in columns:
782                     l.append(name)
783             columns = l
785         # display the filter section
786         if (show_display_form and 
787                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
788             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
789             self.filter_section(filter_template, search_text, filter, 
790                 columns, group, all_filters, all_columns, show_customization)
791             # make sure that the sorting doesn't get lost either
792             if sort:
793                 w('<input type="hidden" name=":sort" value="%s">'%
794                     ','.join(sort))
795             w('</form>\n')
798         # now display the index section
799         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
800         w('<tr class="list-header">\n')
801         for name in columns:
802             cname = name.capitalize()
803             if show_display_form:
804                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
805                 anchor = "%s?%s"%(self.classname, sb)
806                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
807                     anchor, cname))
808             else:
809                 w('<td><span class="list-header">%s</span></td>\n'%cname)
810         w('</tr>\n')
812         # this stuff is used for group headings - optimise the group names
813         old_group = None
814         group_names = []
815         if group:
816             for name in group:
817                 if name[0] == '-': group_names.append(name[1:])
818                 else: group_names.append(name)
820         # now actually loop through all the nodes we get from the filter and
821         # apply the template
822         if show_nodes:
823             matches = None
824             if nodeids is None:
825                 if search_text != '':
826                     matches = self.client.indexer.search(
827                         search_text.split(' '), self.cl)
828                 nodeids = self.cl.filter(matches, filterspec, sort, group)
829             for nodeid in nodeids:
830                 # check for a group heading
831                 if group_names:
832                     this_group = [self.cl.get(nodeid, name, _('[no value]'))
833                         for name in group_names]
834                     if this_group != old_group:
835                         l = []
836                         for name in group_names:
837                             prop = self.properties[name]
838                             if isinstance(prop, hyperdb.Link):
839                                 group_cl = self.db.classes[prop.classname]
840                                 key = group_cl.getkey()
841                                 value = self.cl.get(nodeid, name)
842                                 if value is None:
843                                     l.append(_('[unselected %(classname)s]')%{
844                                         'classname': prop.classname})
845                                 else:
846                                     l.append(group_cl.get(self.cl.get(nodeid,
847                                         name), key))
848                             elif isinstance(prop, hyperdb.Multilink):
849                                 group_cl = self.db.classes[prop.classname]
850                                 key = group_cl.getkey()
851                                 for value in self.cl.get(nodeid, name):
852                                     l.append(group_cl.get(value, key))
853                             else:
854                                 value = self.cl.get(nodeid, name, 
855                                     _('[no value]'))
856                                 if value is None:
857                                     value = _('[empty %(name)s]')%locals()
858                                 else:
859                                     value = str(value)
860                                 l.append(value)
861                         w('<tr class="section-bar">'
862                         '<td align=middle colspan=%s>'
863                         '<strong>%s</strong></td></tr>'%(
864                             len(columns), ', '.join(l)))
865                         old_group = this_group
867                 # display this node's row
868                 replace = IndexTemplateReplace(self.globals, locals(), columns)
869                 self.nodeid = nodeid
870                 w(replace.go(template))
871                 if matches:
872                     self.node_matches(matches[nodeid], len(columns))
873                 self.nodeid = None
875         w('</table>')
877         # display the filter section
878         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
879                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
880             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
881             self.filter_section(filter_template, search_text, filter, 
882                 columns, group, all_filters, all_columns, show_customization)
883             # make sure that the sorting doesn't get lost either
884             if sort:
885                 w('<input type="hidden" name=":sort" value="%s">'%
886                     ','.join(sort))
887             w('</form>\n')
889     def node_matches(self, match, colspan):
890         ''' display the files and messages for a node that matched a
891             full text search
892         '''
893         w = self.client.write
895         message_links = []
896         file_links = []
897         if match.has_key('messages'):
898             for msgid in match['messages']:
899                 k = self.db.msg.labelprop()
900                 lab = self.db.msg.get(msgid, k)
901                 msgpath = 'msg%s'%msgid
902                 message_links.append('<a href="%(msgpath)s">%(lab)s</a>'
903                     %locals())
904             w(_('<tr class="row-hilite"><td colspan="%s">'
905                 '&nbsp;&nbsp;Matched messages: %s</td></tr>')%(
906                     colspan, ', '.join(message_links)))
908         if match.has_key('files'):
909             for fileid in match['files']:
910                 filename = self.db.file.get(fileid, 'name')
911                 filepath = 'file%s/%s'%(fileid, filename)
912                 file_links.append('<a href="%(filepath)s">%(filename)s</a>'
913                     %locals())
914             w(_('<tr class="row-hilite"><td colspan="%s">'
915                 '&nbsp;&nbsp;Matched files: %s</td></tr>')%(
916                     colspan, ', '.join(file_links)))
919     def filter_section(self, template, search_text, filter, columns, group, 
920             all_filters, all_columns, show_customization):
922         w = self.client.write
924         # wrap the template in a single table to ensure the whole widget
925         # is displayed at once
926         w('<table><tr><td>')
928         if template and filter:
929             # display the filter section
930             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
931             w('<tr class="location-bar">')
932             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
933             w('</tr>')
934             w('<tr>')
935             w('<th class="location-bar">Search terms</th>')
936             w('<td><input name="search_text" value="%s" size="50"></td>'%(
937                 search_text))
938             w('</tr>')
939             replace = IndexTemplateReplace(self.globals, locals(), filter)
940             w(replace.go(template))
941             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
942             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
943             w('</table>')
945         # now add in the filter/columns/group/etc config table form
946         w('<input type="hidden" name="show_customization" value="%s">' %
947             show_customization )
948         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
949         names = []
950         seen = {}
951         for name in all_filters + all_columns:
952             if self.properties.has_key(name) and not seen.has_key(name):
953                 names.append(name)
954             seen[name] = 1
955         if show_customization:
956             action = '-'
957         else:
958             action = '+'
959             # hide the values for filters, columns and grouping in the form
960             # if the customization widget is not visible
961             for name in names:
962                 if all_filters and name in filter:
963                     w('<input type="hidden" name=":filter" value="%s">' % name)
964                 if all_columns and name in columns:
965                     w('<input type="hidden" name=":columns" value="%s">' % name)
966                 if all_columns and name in group:
967                     w('<input type="hidden" name=":group" value="%s">' % name)
969         # TODO: The widget style can go into the stylesheet
970         w(_('<th align="left" colspan=%s>'
971           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
972           'customisation...</th></tr>\n')%(len(names)+1, action))
974         if not show_customization:
975             w('</table>\n')
976             return
978         w('<tr class="location-bar"><th>&nbsp;</th>')
979         for name in names:
980             w('<th>%s</th>'%name.capitalize())
981         w('</tr>\n')
983         # Filter
984         if all_filters:
985             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
986             for name in names:
987                 if name not in all_filters:
988                     w('<td>&nbsp;</td>')
989                     continue
990                 if name in filter: checked=' checked'
991                 else: checked=''
992                 w('<td align=middle>\n')
993                 w(' <input type="checkbox" name=":filter" value="%s" '
994                   '%s></td>\n'%(name, checked))
995             w('</tr>\n')
997         # Columns
998         if all_columns:
999             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
1000             for name in names:
1001                 if name not in all_columns:
1002                     w('<td>&nbsp;</td>')
1003                     continue
1004                 if name in columns: checked=' checked'
1005                 else: checked=''
1006                 w('<td align=middle>\n')
1007                 w(' <input type="checkbox" name=":columns" value="%s"'
1008                   '%s></td>\n'%(name, checked))
1009             w('</tr>\n')
1011             # Grouping
1012             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
1013             for name in names:
1014                 if name not in all_columns:
1015                     w('<td>&nbsp;</td>')
1016                     continue
1017                 if name in group: checked=' checked'
1018                 else: checked=''
1019                 w('<td align=middle>\n')
1020                 w(' <input type="checkbox" name=":group" value="%s"'
1021                   '%s></td>\n'%(name, checked))
1022             w('</tr>\n')
1024         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
1025         w('<td colspan="%s">'%len(names))
1026         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
1027         w('</tr>\n')
1028         w('</table>\n')
1030         # and the outer table
1031         w('</td></tr></table>')
1034     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
1035         l = []
1036         w = l.append
1037         for k, v in filterspec.items():
1038             k = urllib.quote(k)
1039             if type(v) == type([]):
1040                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
1041             else:
1042                 w('%s=%s'%(k, urllib.quote(v)))
1043         if columns:
1044             w(':columns=%s'%','.join(map(urllib.quote, columns)))
1045         if filter:
1046             w(':filter=%s'%','.join(map(urllib.quote, filter)))
1047         if group:
1048             w(':group=%s'%','.join(map(urllib.quote, group)))
1049         m = []
1050         s_dir = ''
1051         for name in sort:
1052             dir = name[0]
1053             if dir == '-':
1054                 name = name[1:]
1055             else:
1056                 dir = ''
1057             if sort_name == name:
1058                 if dir == '-':
1059                     s_dir = ''
1060                 else:
1061                     s_dir = '-'
1062             else:
1063                 m.append(dir+urllib.quote(name))
1064         m.insert(0, s_dir+urllib.quote(sort_name))
1065         # so things don't get completely out of hand, limit the sort to
1066         # two columns
1067         w(':sort=%s'%','.join(m[:2]))
1068         return '&'.join(l)
1072 #   ITEM TEMPLATES
1074 class ItemTemplateReplace:
1075     '''Regular-expression based parser that turns the template into HTML. 
1076     '''
1077     def __init__(self, globals, locals, cl, nodeid):
1078         self.globals = globals
1079         self.locals = locals
1080         self.cl = cl
1081         self.nodeid = nodeid
1083     replace=re.compile(
1084         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1085         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1086     def go(self, text):
1087         return self.replace.sub(self, text)
1089     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1090         if m.group('name'):
1091             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1092                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1093                     self.nodeid)
1094                 return replace.go(m.group('text'))
1095             else:
1096                 return ''
1097         if m.group('display'):
1098             command = m.group('command')
1099             return eval(command, self.globals, self.locals)
1100         return '*** unhandled match: %s'%str(m.groupdict())
1103 class ItemTemplate(TemplateFunctions):
1104     '''Templating functionality specifically for item (node) display
1105     '''
1106     def __init__(self, client, templates, classname):
1107         TemplateFunctions.__init__(self)
1108         self.client = client
1109         self.instance = client.instance
1110         self.templates = templates
1111         self.classname = classname
1113         # derived
1114         self.db = self.client.db
1115         self.cl = self.db.classes[self.classname]
1116         self.properties = self.cl.getprops()
1118     def render(self, nodeid):
1119         self.nodeid = nodeid
1121         if (self.properties.has_key('type') and
1122                 self.properties.has_key('content')):
1123             pass
1124             # XXX we really want to return this as a downloadable...
1125             #  currently I handle this at a higher level by detecting 'file'
1126             #  designators...
1128         w = self.client.write
1129         w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1130             self.classname, nodeid))
1131         s = open(os.path.join(self.templates, self.classname+'.item')).read()
1132         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1133         w(replace.go(s))
1134         w('</form>')
1137 class NewItemTemplate(TemplateFunctions):
1138     '''Templating functionality specifically for NEW item (node) display
1139     '''
1140     def __init__(self, client, templates, classname):
1141         TemplateFunctions.__init__(self)
1142         self.client = client
1143         self.instance = client.instance
1144         self.templates = templates
1145         self.classname = classname
1147         # derived
1148         self.db = self.client.db
1149         self.cl = self.db.classes[self.classname]
1150         self.properties = self.cl.getprops()
1152     def render(self, form):
1153         self.form = form
1154         w = self.client.write
1155         c = self.classname
1156         try:
1157             s = open(os.path.join(self.templates, c+'.newitem')).read()
1158         except IOError:
1159             s = open(os.path.join(self.templates, c+'.item')).read()
1160         w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1161         for key in form.keys():
1162             if key[0] == ':':
1163                 value = form[key].value
1164                 if type(value) != type([]): value = [value]
1165                 for value in value:
1166                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
1167         replace = ItemTemplateReplace(self.globals, locals(), None, None)
1168         w(replace.go(s))
1169         w('</form>')
1172 # $Log: not supported by cvs2svn $
1173 # Revision 1.89  2002/05/15 06:34:47  richard
1174 # forgot to fix the templating for last change
1176 # Revision 1.88  2002/04/24 08:34:35  rochecompaan
1177 # Sorting was applied to all nodes of the MultiLink class instead of
1178 # the nodes that are actually linked to in the "field" template
1179 # function.  This adds about 20+ seconds in the display of an issue if
1180 # your database has a 1000 or more issue in it.
1182 # Revision 1.87  2002/04/03 06:12:46  richard
1183 # Fix for date properties as labels.
1185 # Revision 1.86  2002/04/03 05:54:31  richard
1186 # Fixed serialisation problem by moving the serialisation step out of the
1187 # hyperdb.Class (get, set) into the hyperdb.Database.
1189 # Also fixed htmltemplate after the showid changes I made yesterday.
1191 # Unit tests for all of the above written.
1193 # Revision 1.85  2002/04/02 01:40:58  richard
1194 #  . link() htmltemplate function now has a "showid" option for links and
1195 #    multilinks. When true, it only displays the linked node id as the anchor
1196 #    text. The link value is displayed as a tooltip using the title anchor
1197 #    attribute.
1199 # Revision 1.84.2.2  2002/04/20 13:23:32  rochecompaan
1200 # We now have a separate search page for nodes.  Search links for
1201 # different classes can be customized in instance_config similar to
1202 # index links.
1204 # Revision 1.84.2.1  2002/04/19 19:54:42  rochecompaan
1205 # cgi_client.py
1206 #     removed search link for the time being
1207 #     moved rendering of matches to htmltemplate
1208 # hyperdb.py
1209 #     filtering of nodes on full text search incorporated in filter method
1210 # roundupdb.py
1211 #     added paramater to call of filter method
1212 # roundup_indexer.py
1213 #     added search method to RoundupIndexer class
1215 # Revision 1.84  2002/03/29 19:41:48  rochecompaan
1216 #  . Fixed display of mutlilink properties when using the template
1217 #    functions, menu and plain.
1219 # Revision 1.83  2002/02/27 04:14:31  richard
1220 # Ran it through pychecker, made fixes
1222 # Revision 1.82  2002/02/21 23:11:45  richard
1223 #  . fixed some problems in date calculations (calendar.py doesn't handle over-
1224 #    and under-flow). Also, hour/minute/second intervals may now be more than
1225 #    99 each.
1227 # Revision 1.81  2002/02/21 07:21:38  richard
1228 # docco
1230 # Revision 1.80  2002/02/21 07:19:08  richard
1231 # ... and label, width and height control for extra flavour!
1233 # Revision 1.79  2002/02/21 06:57:38  richard
1234 #  . Added popup help for classes using the classhelp html template function.
1235 #    - add <display call="classhelp('priority', 'id,name,description')">
1236 #      to an item page, and it generates a link to a popup window which displays
1237 #      the id, name and description for the priority class. The description
1238 #      field won't exist in most installations, but it will be added to the
1239 #      default templates.
1241 # Revision 1.78  2002/02/21 06:23:00  richard
1242 # *** empty log message ***
1244 # Revision 1.77  2002/02/20 05:05:29  richard
1245 #  . Added simple editing for classes that don't define a templated interface.
1246 #    - access using the admin "class list" interface
1247 #    - limited to admin-only
1248 #    - requires the csv module from object-craft (url given if it's missing)
1250 # Revision 1.76  2002/02/16 09:10:52  richard
1251 # oops
1253 # Revision 1.75  2002/02/16 08:43:23  richard
1254 #  . #517906 ] Attribute order in "View customisation"
1256 # Revision 1.74  2002/02/16 08:39:42  richard
1257 #  . #516854 ] "My Issues" and redisplay
1259 # Revision 1.73  2002/02/15 07:08:44  richard
1260 #  . Alternate email addresses are now available for users. See the MIGRATION
1261 #    file for info on how to activate the feature.
1263 # Revision 1.72  2002/02/14 23:39:18  richard
1264 # . All forms now have "double-submit" protection when Javascript is enabled
1265 #   on the client-side.
1267 # Revision 1.71  2002/01/23 06:15:24  richard
1268 # real (non-string, duh) sorting of lists by node id
1270 # Revision 1.70  2002/01/23 05:47:57  richard
1271 # more HTML template cleanup and unit tests
1273 # Revision 1.69  2002/01/23 05:10:27  richard
1274 # More HTML template cleanup and unit tests.
1275 #  - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1276 #    templates, but link(is_download=1) will still work for existing templates]
1278 # Revision 1.68  2002/01/22 22:55:28  richard
1279 #  . htmltemplate list() wasn't sorting...
1281 # Revision 1.67  2002/01/22 22:46:22  richard
1282 # more htmltemplate cleanups and unit tests
1284 # Revision 1.66  2002/01/22 06:35:40  richard
1285 # more htmltemplate tests and cleanup
1287 # Revision 1.65  2002/01/22 00:12:06  richard
1288 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1289 # off the implementation of some of the functions so they behave sanely.
1291 # Revision 1.64  2002/01/21 03:25:59  richard
1292 # oops
1294 # Revision 1.63  2002/01/21 02:59:10  richard
1295 # Fixed up the HTML display of history so valid links are actually displayed.
1296 # Oh for some unit tests! :(
1298 # Revision 1.62  2002/01/18 08:36:12  grubert
1299 #  . add nowrap to history table date cell i.e. <td nowrap ...
1301 # Revision 1.61  2002/01/17 23:04:53  richard
1302 #  . much nicer history display (actualy real handling of property types etc)
1304 # Revision 1.60  2002/01/17 08:48:19  grubert
1305 #  . display superseder as html link in history.
1307 # Revision 1.59  2002/01/17 07:58:24  grubert
1308 #  . display links a html link in history.
1310 # Revision 1.58  2002/01/15 00:50:03  richard
1311 # #502949 ] index view for non-issues and redisplay
1313 # Revision 1.57  2002/01/14 23:31:21  richard
1314 # reverted the change that had plain() hyperlinking the link displays -
1315 # that's what link() is for!
1317 # Revision 1.56  2002/01/14 07:04:36  richard
1318 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1319 #    the linked node's page.
1320 #    ... this allows a display very similar to bugzilla's where you can actually
1321 #    find out information about the linked node.
1323 # Revision 1.55  2002/01/14 06:45:03  richard
1324 #  . #502953 ] nosy-like treatment of other multilinks
1325 #    ... had to revert most of the previous change to the multilink field
1326 #    display... not good.
1328 # Revision 1.54  2002/01/14 05:16:51  richard
1329 # The submit buttons need a name attribute or mozilla won't submit without a
1330 # file upload. Yeah, that's bloody obscure. Grr.
1332 # Revision 1.53  2002/01/14 04:03:32  richard
1333 # How about that ... date fields have never worked ...
1335 # Revision 1.52  2002/01/14 02:20:14  richard
1336 #  . changed all config accesses so they access either the instance or the
1337 #    config attriubute on the db. This means that all config is obtained from
1338 #    instance_config instead of the mish-mash of classes. This will make
1339 #    switching to a ConfigParser setup easier too, I hope.
1341 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1342 # 0.5.0 switch, I hope!)
1344 # Revision 1.51  2002/01/10 10:02:15  grubert
1345 # In do_history: replace "." in date by " " so html wraps more sensible.
1346 # Should this be done in date's string converter ?
1348 # Revision 1.50  2002/01/05 02:35:10  richard
1349 # I18N'ification
1351 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1352 # Features added:
1353 #  .  Multilink properties are now displayed as comma separated values in
1354 #     a textbox
1355 #  .  The add user link is now only visible to the admin user
1356 #  .  Modified the mail gateway to reject submissions from unknown
1357 #     addresses if ANONYMOUS_ACCESS is denied
1359 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1360 # Bugs fixed:
1361 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1362 #     lost somewhere
1363 #   . Internet Explorer submits full path for filename - we now strip away
1364 #     the path
1365 # Features added:
1366 #   . Link and multilink properties are now displayed sorted in the cgi
1367 #     interface
1369 # Revision 1.47  2001/11/26 22:55:56  richard
1370 # Feature:
1371 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1372 #    the instance.
1373 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1374 #    signature info in e-mails.
1375 #  . Some more flexibility in the mail gateway and more error handling.
1376 #  . Login now takes you to the page you back to the were denied access to.
1378 # Fixed:
1379 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1381 # Revision 1.46  2001/11/24 00:53:12  jhermann
1382 # "except:" is bad, bad , bad!
1384 # Revision 1.45  2001/11/22 15:46:42  jhermann
1385 # Added module docstrings to all modules.
1387 # Revision 1.44  2001/11/21 23:35:45  jhermann
1388 # Added globbing for win32, and sample marking in a 2nd file to test it
1390 # Revision 1.43  2001/11/21 04:04:43  richard
1391 # *sigh* more missing value handling
1393 # Revision 1.42  2001/11/21 03:40:54  richard
1394 # more new property handling
1396 # Revision 1.41  2001/11/15 10:26:01  richard
1397 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1399 # Revision 1.40  2001/11/03 01:56:51  richard
1400 # More HTML compliance fixes. This will probably fix the Netscape problem
1401 # too.
1403 # Revision 1.39  2001/11/03 01:43:47  richard
1404 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1406 # Revision 1.38  2001/10/31 06:58:51  richard
1407 # Added the wrap="hard" attribute to the textarea of the note field so the
1408 # messages wrap sanely.
1410 # Revision 1.37  2001/10/31 06:24:35  richard
1411 # Added do_stext to htmltemplate, thanks Brad Clements.
1413 # Revision 1.36  2001/10/28 22:51:38  richard
1414 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1416 # Revision 1.35  2001/10/24 00:04:41  richard
1417 # Removed the "infinite authentication loop", thanks Roch'e
1419 # Revision 1.34  2001/10/23 22:56:36  richard
1420 # Bugfix in filter "widget" placement, thanks Roch'e
1422 # Revision 1.33  2001/10/23 01:00:18  richard
1423 # Re-enabled login and registration access after lopping them off via
1424 # disabling access for anonymous users.
1425 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1426 # a couple of bugs while I was there. Probably introduced a couple, but
1427 # things seem to work OK at the moment.
1429 # Revision 1.32  2001/10/22 03:25:01  richard
1430 # Added configuration for:
1431 #  . anonymous user access and registration (deny/allow)
1432 #  . filter "widget" location on index page (top, bottom, both)
1433 # Updated some documentation.
1435 # Revision 1.31  2001/10/21 07:26:35  richard
1436 # feature #473127: Filenames. I modified the file.index and htmltemplate
1437 #  source so that the filename is used in the link and the creation
1438 #  information is displayed.
1440 # Revision 1.30  2001/10/21 04:44:50  richard
1441 # bug #473124: UI inconsistency with Link fields.
1442 #    This also prompted me to fix a fairly long-standing usability issue -
1443 #    that of being able to turn off certain filters.
1445 # Revision 1.29  2001/10/21 00:17:56  richard
1446 # CGI interface view customisation section may now be hidden (patch from
1447 #  Roch'e Compaan.)
1449 # Revision 1.28  2001/10/21 00:00:16  richard
1450 # Fixed Checklist function - wasn't always working on a list.
1452 # Revision 1.27  2001/10/20 12:13:44  richard
1453 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1455 # Revision 1.26  2001/10/14 10:55:00  richard
1456 # Handle empty strings in HTML template Link function
1458 # Revision 1.25  2001/10/09 07:25:59  richard
1459 # Added the Password property type. See "pydoc roundup.password" for
1460 # implementation details. Have updated some of the documentation too.
1462 # Revision 1.24  2001/09/27 06:45:58  richard
1463 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1464 # on the plain() template function to escape the text for HTML.
1466 # Revision 1.23  2001/09/10 09:47:18  richard
1467 # Fixed bug in the generation of links to Link/Multilink in indexes.
1468 #   (thanks Hubert Hoegl)
1469 # Added AssignedTo to the "classic" schema's item page.
1471 # Revision 1.22  2001/08/30 06:01:17  richard
1472 # Fixed missing import in mailgw :(
1474 # Revision 1.21  2001/08/16 07:34:59  richard
1475 # better CGI text searching - but hidden filter fields are disappearing...
1477 # Revision 1.20  2001/08/15 23:43:18  richard
1478 # Fixed some isFooTypes that I missed.
1479 # Refactored some code in the CGI code.
1481 # Revision 1.19  2001/08/12 06:32:36  richard
1482 # using isinstance(blah, Foo) now instead of isFooType
1484 # Revision 1.18  2001/08/07 00:24:42  richard
1485 # stupid typo
1487 # Revision 1.17  2001/08/07 00:15:51  richard
1488 # Added the copyright/license notice to (nearly) all files at request of
1489 # Bizar Software.
1491 # Revision 1.16  2001/08/01 03:52:23  richard
1492 # Checklist was using wrong name.
1494 # Revision 1.15  2001/07/30 08:12:17  richard
1495 # Added time logging and file uploading to the templates.
1497 # Revision 1.14  2001/07/30 06:17:45  richard
1498 # Features:
1499 #  . Added ability for cgi newblah forms to indicate that the new node
1500 #    should be linked somewhere.
1501 # Fixed:
1502 #  . Fixed the agument handling for the roundup-admin find command.
1503 #  . Fixed handling of summary when no note supplied for newblah. Again.
1504 #  . Fixed detection of no form in htmltemplate Field display.
1506 # Revision 1.13  2001/07/30 02:37:53  richard
1507 # Temporary measure until we have decent schema migration.
1509 # Revision 1.12  2001/07/30 01:24:33  richard
1510 # Handles new node display now.
1512 # Revision 1.11  2001/07/29 09:31:35  richard
1513 # oops
1515 # Revision 1.10  2001/07/29 09:28:23  richard
1516 # Fixed sorting by clicking on column headings.
1518 # Revision 1.9  2001/07/29 08:27:40  richard
1519 # Fixed handling of passed-in values in form elements (ie. during a
1520 # drill-down)
1522 # Revision 1.8  2001/07/29 07:01:39  richard
1523 # Added vim command to all source so that we don't get no steenkin' tabs :)
1525 # Revision 1.7  2001/07/29 05:36:14  richard
1526 # Cleanup of the link label generation.
1528 # Revision 1.6  2001/07/29 04:06:42  richard
1529 # Fixed problem in link display when Link value is None.
1531 # Revision 1.5  2001/07/28 08:17:09  richard
1532 # fixed use of stylesheet
1534 # Revision 1.4  2001/07/28 07:59:53  richard
1535 # Replaced errno integers with their module values.
1536 # De-tabbed templatebuilder.py
1538 # Revision 1.3  2001/07/25 03:39:47  richard
1539 # Hrm - displaying links to classes that don't specify a key property. I've
1540 # got it defaulting to 'name', then 'title' and then a "random" property (first
1541 # one returned by getprops().keys().
1542 # Needs to be moved onto the Class I think...
1544 # Revision 1.2  2001/07/22 12:09:32  richard
1545 # Final commit of Grande Splite
1547 # Revision 1.1  2001/07/22 11:58:35  richard
1548 # More Grande Splite
1551 # vim: set filetype=python ts=4 sw=4 et si