Code

Sorting was applied to all nodes of the MultiLink class instead of
[roundup.git] / roundup / htmltemplate.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17
18 # $Id: htmltemplate.py,v 1.88 2002-04-24 08:34:35 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             pretty = interval.pretty()
438             if pretty is None:
439                 pretty = value.pretty()
440             return pretty
441         return str(interval)
443     def do_download(self, property, **args):
444         ''' show a Link("file") or Multilink("file") property using links that
445             allow you to download files
446         '''
447         if not self.nodeid:
448             return _('[Download: not called from item]')
449         return self.do_link(property, is_download=1)
452     def do_checklist(self, property, **args):
453         ''' for a Link or Multilink property, display checkboxes for the
454             available choices to permit filtering
455         '''
456         propclass = self.properties[property]
457         if (not isinstance(propclass, hyperdb.Link) and not
458                 isinstance(propclass, hyperdb.Multilink)):
459             return _('[Checklist: not a link]')
461         # get our current checkbox state
462         if self.nodeid:
463             # get the info from the node - make sure it's a list
464             if isinstance(propclass, hyperdb.Link):
465                 value = [self.cl.get(self.nodeid, property)]
466             else:
467                 value = self.cl.get(self.nodeid, property)
468         elif self.filterspec is not None:
469             # get the state from the filter specification (always a list)
470             value = self.filterspec.get(property, [])
471         else:
472             # it's a new node, so there's no state
473             value = []
475         # so we can map to the linked node's "lable" property
476         linkcl = self.db.classes[propclass.classname]
477         l = []
478         k = linkcl.labelprop()
479         for optionid in linkcl.list():
480             option = cgi.escape(str(linkcl.get(optionid, k)))
481             if optionid in value or option in value:
482                 checked = 'checked'
483             else:
484                 checked = ''
485             l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
486                 option, checked, property, option))
488         # for Links, allow the "unselected" option too
489         if isinstance(propclass, hyperdb.Link):
490             if value is None or '-1' in value:
491                 checked = 'checked'
492             else:
493                 checked = ''
494             l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
495                 'value="-1">')%(checked, property))
496         return '\n'.join(l)
498     def do_note(self, rows=5, cols=80):
499         ''' display a "note" field, which is a text area for entering a note to
500             go along with a change. 
501         '''
502         # TODO: pull the value from the form
503         return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
504             '</textarea>'%(rows, cols)
506     # XXX new function
507     def do_list(self, property, reverse=0):
508         ''' list the items specified by property using the standard index for
509             the class
510         '''
511         propcl = self.properties[property]
512         if not isinstance(propcl, hyperdb.Multilink):
513             return _('[List: not a Multilink]')
515         value = self.determine_value(property)
516         if not value:
517             return ''
519         # sort, possibly revers and then re-stringify
520         value = map(int, value)
521         value.sort()
522         if reverse:
523             value.reverse()
524         value = map(str, value)
526         # render the sub-index into a string
527         fp = StringIO.StringIO()
528         try:
529             write_save = self.client.write
530             self.client.write = fp.write
531             index = IndexTemplate(self.client, self.templates, propcl.classname)
532             index.render(nodeids=value, show_display_form=0)
533         finally:
534             self.client.write = write_save
536         return fp.getvalue()
538     # XXX new function
539     def do_history(self, direction='descending'):
540         ''' list the history of the item
542             If "direction" is 'descending' then the most recent event will
543             be displayed first. If it is 'ascending' then the oldest event
544             will be displayed first.
545         '''
546         if self.nodeid is None:
547             return _("[History: node doesn't exist]")
549         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
550             '<tr class="list-header">',
551             _('<th align=left><span class="list-item">Date</span></th>'),
552             _('<th align=left><span class="list-item">User</span></th>'),
553             _('<th align=left><span class="list-item">Action</span></th>'),
554             _('<th align=left><span class="list-item">Args</span></th>'),
555             '</tr>']
557         comments = {}
558         history = self.cl.history(self.nodeid)
559         history.sort()
560         if direction == 'descending':
561             history.reverse()
562         for id, evt_date, user, action, args in history:
563             date_s = str(evt_date).replace("."," ")
564             arg_s = ''
565             if action == 'link' and type(args) == type(()):
566                 if len(args) == 3:
567                     linkcl, linkid, key = args
568                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
569                         linkcl, linkid, key)
570                 else:
571                     arg_s = str(args)
573             elif action == 'unlink' and type(args) == type(()):
574                 if len(args) == 3:
575                     linkcl, linkid, key = args
576                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
577                         linkcl, linkid, key)
578                 else:
579                     arg_s = str(args)
581             elif type(args) == type({}):
582                 cell = []
583                 for k in args.keys():
584                     # try to get the relevant property and treat it
585                     # specially
586                     try:
587                         prop = self.properties[k]
588                     except:
589                         prop = None
590                     if prop is not None:
591                         if args[k] and (isinstance(prop, hyperdb.Multilink) or
592                                 isinstance(prop, hyperdb.Link)):
593                             # figure what the link class is
594                             classname = prop.classname
595                             try:
596                                 linkcl = self.db.classes[classname]
597                             except KeyError:
598                                 labelprop = None
599                                 comments[classname] = _('''The linked class
600                                     %(classname)s no longer exists''')%locals()
601                             labelprop = linkcl.labelprop()
603                         if isinstance(prop, hyperdb.Multilink) and \
604                                 len(args[k]) > 0:
605                             ml = []
606                             for linkid in args[k]:
607                                 label = classname + linkid
608                                 # if we have a label property, try to use it
609                                 # TODO: test for node existence even when
610                                 # there's no labelprop!
611                                 try:
612                                     if labelprop is not None:
613                                         label = linkcl.get(linkid, labelprop)
614                                 except IndexError:
615                                     comments['no_link'] = _('''<strike>The
616                                         linked node no longer
617                                         exists</strike>''')
618                                     ml.append('<strike>%s</strike>'%label)
619                                 else:
620                                     ml.append('<a href="%s%s">%s</a>'%(
621                                         classname, linkid, label))
622                             cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
623                         elif isinstance(prop, hyperdb.Link) and args[k]:
624                             label = classname + args[k]
625                             # if we have a label property, try to use it
626                             # TODO: test for node existence even when
627                             # there's no labelprop!
628                             if labelprop is not None:
629                                 try:
630                                     label = linkcl.get(args[k], labelprop)
631                                 except IndexError:
632                                     comments['no_link'] = _('''<strike>The
633                                         linked node no longer
634                                         exists</strike>''')
635                                     cell.append(' <strike>%s</strike>,\n'%label)
636                                     # "flag" this is done .... euwww
637                                     label = None
638                             if label is not None:
639                                 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
640                                     classname, args[k], label))
642                         elif isinstance(prop, hyperdb.Date) and args[k]:
643                             d = date.Date(args[k])
644                             cell.append('%s: %s'%(k, str(d)))
646                         elif isinstance(prop, hyperdb.Interval) and args[k]:
647                             d = date.Interval(args[k])
648                             cell.append('%s: %s'%(k, str(d)))
650                         elif not args[k]:
651                             cell.append('%s: (no value)\n'%k)
653                         else:
654                             cell.append('%s: %s\n'%(k, str(args[k])))
655                     else:
656                         # property no longer exists
657                         comments['no_exist'] = _('''<em>The indicated property
658                             no longer exists</em>''')
659                         cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
660                 arg_s = '<br />'.join(cell)
661             else:
662                 # unkown event!!
663                 comments['unknown'] = _('''<strong><em>This event is not
664                     handled by the history display!</em></strong>''')
665                 arg_s = '<strong><em>' + str(args) + '</em></strong>'
666             date_s = date_s.replace(' ', '&nbsp;')
667             l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
668                 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
669                 user, action, arg_s))
670         if comments:
671             l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
672         for entry in comments.values():
673             l.append('<tr><td colspan=4>%s</td></tr>'%entry)
674         l.append('</table>')
675         return '\n'.join(l)
677     # XXX new function
678     def do_submit(self):
679         ''' add a submit button for the item
680         '''
681         if self.nodeid:
682             return _('<input type="submit" name="submit" value="Submit Changes">')
683         elif self.form is not None:
684             return _('<input type="submit" name="submit" value="Submit New Entry">')
685         else:
686             return _('[Submit: not called from item]')
688     def do_classhelp(self, classname, properties, label='?', width='400',
689             height='400'):
690         '''pop up a javascript window with class help
692            This generates a link to a popup window which displays the 
693            properties indicated by "properties" of the class named by
694            "classname". The "properties" should be a comma-separated list
695            (eg. 'id,name,description').
697            You may optionally override the label displayed, the width and
698            height. The popup window will be resizable and scrollable.
699         '''
700         return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
701             'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
702             properties, width, height, label)
704 #   INDEX TEMPLATES
706 class IndexTemplateReplace:
707     '''Regular-expression based parser that turns the template into HTML. 
708     '''
709     def __init__(self, globals, locals, props):
710         self.globals = globals
711         self.locals = locals
712         self.props = props
714     replace=re.compile(
715         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
716         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
717     def go(self, text):
718         return self.replace.sub(self, text)
720     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
721         if m.group('name'):
722             if m.group('name') in self.props:
723                 text = m.group('text')
724                 replace = IndexTemplateReplace(self.globals, {}, self.props)
725                 return replace.go(text)
726             else:
727                 return ''
728         if m.group('display'):
729             command = m.group('command')
730             return eval(command, self.globals, self.locals)
731         return '*** unhandled match: %s'%str(m.groupdict())
733 class IndexTemplate(TemplateFunctions):
734     '''Templating functionality specifically for index pages
735     '''
736     def __init__(self, client, templates, classname):
737         TemplateFunctions.__init__(self)
738         self.client = client
739         self.instance = client.instance
740         self.templates = templates
741         self.classname = classname
743         # derived
744         self.db = self.client.db
745         self.cl = self.db.classes[self.classname]
746         self.properties = self.cl.getprops()
748     col_re=re.compile(r'<property\s+name="([^>]+)">')
749     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
750             show_display_form=1, nodeids=None, show_customization=1):
751         self.filterspec = filterspec
753         w = self.client.write
755         # get the filter template
756         try:
757             filter_template = open(os.path.join(self.templates,
758                 self.classname+'.filter')).read()
759             all_filters = self.col_re.findall(filter_template)
760         except IOError, error:
761             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
762             filter_template = None
763             all_filters = []
765         # XXX deviate from spec here ...
766         # load the index section template and figure the default columns from it
767         try:
768             template = open(os.path.join(self.templates,
769                 self.classname+'.index')).read()
770         except IOError, error:
771             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
772             raise MissingTemplateError, self.classname+'.index'
773         all_columns = self.col_re.findall(template)
774         if not columns:
775             columns = []
776             for name in all_columns:
777                 columns.append(name)
778         else:
779             # re-sort columns to be the same order as all_columns
780             l = []
781             for name in all_columns:
782                 if name in columns:
783                     l.append(name)
784             columns = l
786         # display the filter section
787         if (show_display_form and 
788                 self.instance.FILTER_POSITION in ('top and bottom', 'top')):
789             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
790             self.filter_section(filter_template, filter, columns, group,
791                 all_filters, all_columns, show_customization)
792             # make sure that the sorting doesn't get lost either
793             if sort:
794                 w('<input type="hidden" name=":sort" value="%s">'%
795                     ','.join(sort))
796             w('</form>\n')
799         # now display the index section
800         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
801         w('<tr class="list-header">\n')
802         for name in columns:
803             cname = name.capitalize()
804             if show_display_form:
805                 sb = self.sortby(name, filterspec, columns, filter, group, sort)
806                 anchor = "%s?%s"%(self.classname, sb)
807                 w('<td><span class="list-header"><a href="%s">%s</a></span></td>\n'%(
808                     anchor, cname))
809             else:
810                 w('<td><span class="list-header">%s</span></td>\n'%cname)
811         w('</tr>\n')
813         # this stuff is used for group headings - optimise the group names
814         old_group = None
815         group_names = []
816         if group:
817             for name in group:
818                 if name[0] == '-': group_names.append(name[1:])
819                 else: group_names.append(name)
821         # now actually loop through all the nodes we get from the filter and
822         # apply the template
823         if nodeids is None:
824             nodeids = self.cl.filter(filterspec, sort, group)
825         for nodeid in nodeids:
826             # check for a group heading
827             if group_names:
828                 this_group = [self.cl.get(nodeid, name, _('[no value]'))
829                     for name in group_names]
830                 if this_group != old_group:
831                     l = []
832                     for name in group_names:
833                         prop = self.properties[name]
834                         if isinstance(prop, hyperdb.Link):
835                             group_cl = self.db.classes[prop.classname]
836                             key = group_cl.getkey()
837                             value = self.cl.get(nodeid, name)
838                             if value is None:
839                                 l.append(_('[unselected %(classname)s]')%{
840                                     'classname': prop.classname})
841                             else:
842                                 l.append(group_cl.get(self.cl.get(nodeid,
843                                     name), key))
844                         elif isinstance(prop, hyperdb.Multilink):
845                             group_cl = self.db.classes[prop.classname]
846                             key = group_cl.getkey()
847                             for value in self.cl.get(nodeid, name):
848                                 l.append(group_cl.get(value, key))
849                         else:
850                             value = self.cl.get(nodeid, name, _('[no value]'))
851                             if value is None:
852                                 value = _('[empty %(name)s]')%locals()
853                             else:
854                                 value = str(value)
855                             l.append(value)
856                     w('<tr class="section-bar">'
857                       '<td align=middle colspan=%s><strong>%s</strong></td></tr>'%(
858                         len(columns), ', '.join(l)))
859                     old_group = this_group
861             # display this node's row
862             replace = IndexTemplateReplace(self.globals, locals(), columns)
863             self.nodeid = nodeid
864             w(replace.go(template))
865             self.nodeid = None
867         w('</table>')
869         # display the filter section
870         if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
871                 self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
872             w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
873             self.filter_section(filter_template, filter, columns, group,
874                 all_filters, all_columns, show_customization)
875             # make sure that the sorting doesn't get lost either
876             if sort:
877                 w('<input type="hidden" name=":sort" value="%s">'%
878                     ','.join(sort))
879             w('</form>\n')
882     def filter_section(self, template, filter, columns, group, all_filters,
883             all_columns, show_customization):
885         w = self.client.write
887         # wrap the template in a single table to ensure the whole widget
888         # is displayed at once
889         w('<table><tr><td>')
891         if template and filter:
892             # display the filter section
893             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
894             w('<tr class="location-bar">')
895             w(_(' <th align="left" colspan="2">Filter specification...</th>'))
896             w('</tr>')
897             replace = IndexTemplateReplace(self.globals, locals(), filter)
898             w(replace.go(template))
899             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
900             w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
901             w('</table>')
903         # now add in the filter/columns/group/etc config table form
904         w('<input type="hidden" name="show_customization" value="%s">' %
905             show_customization )
906         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
907         names = []
908         seen = {}
909         for name in all_filters + all_columns:
910             if self.properties.has_key(name) and not seen.has_key(name):
911                 names.append(name)
912             seen[name] = 1
913         if show_customization:
914             action = '-'
915         else:
916             action = '+'
917             # hide the values for filters, columns and grouping in the form
918             # if the customization widget is not visible
919             for name in names:
920                 if all_filters and name in filter:
921                     w('<input type="hidden" name=":filter" value="%s">' % name)
922                 if all_columns and name in columns:
923                     w('<input type="hidden" name=":columns" value="%s">' % name)
924                 if all_columns and name in group:
925                     w('<input type="hidden" name=":group" value="%s">' % name)
927         # TODO: The widget style can go into the stylesheet
928         w(_('<th align="left" colspan=%s>'
929           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
930           'customisation...</th></tr>\n')%(len(names)+1, action))
932         if not show_customization:
933             w('</table>\n')
934             return
936         w('<tr class="location-bar"><th>&nbsp;</th>')
937         for name in names:
938             w('<th>%s</th>'%name.capitalize())
939         w('</tr>\n')
941         # Filter
942         if all_filters:
943             w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
944             for name in names:
945                 if name not in all_filters:
946                     w('<td>&nbsp;</td>')
947                     continue
948                 if name in filter: checked=' checked'
949                 else: checked=''
950                 w('<td align=middle>\n')
951                 w(' <input type="checkbox" name=":filter" value="%s" '
952                   '%s></td>\n'%(name, checked))
953             w('</tr>\n')
955         # Columns
956         if all_columns:
957             w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
958             for name in names:
959                 if name not in all_columns:
960                     w('<td>&nbsp;</td>')
961                     continue
962                 if name in columns: checked=' checked'
963                 else: checked=''
964                 w('<td align=middle>\n')
965                 w(' <input type="checkbox" name=":columns" value="%s"'
966                   '%s></td>\n'%(name, checked))
967             w('</tr>\n')
969             # Grouping
970             w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
971             for name in names:
972                 if name not in all_columns:
973                     w('<td>&nbsp;</td>')
974                     continue
975                 if name in group: checked=' checked'
976                 else: checked=''
977                 w('<td align=middle>\n')
978                 w(' <input type="checkbox" name=":group" value="%s"'
979                   '%s></td>\n'%(name, checked))
980             w('</tr>\n')
982         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
983         w('<td colspan="%s">'%len(names))
984         w(_('<input type="submit" name="action" value="Redisplay"></td>'))
985         w('</tr>\n')
986         w('</table>\n')
988         # and the outer table
989         w('</td></tr></table>')
992     def sortby(self, sort_name, filterspec, columns, filter, group, sort):
993         l = []
994         w = l.append
995         for k, v in filterspec.items():
996             k = urllib.quote(k)
997             if type(v) == type([]):
998                 w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
999             else:
1000                 w('%s=%s'%(k, urllib.quote(v)))
1001         if columns:
1002             w(':columns=%s'%','.join(map(urllib.quote, columns)))
1003         if filter:
1004             w(':filter=%s'%','.join(map(urllib.quote, filter)))
1005         if group:
1006             w(':group=%s'%','.join(map(urllib.quote, group)))
1007         m = []
1008         s_dir = ''
1009         for name in sort:
1010             dir = name[0]
1011             if dir == '-':
1012                 name = name[1:]
1013             else:
1014                 dir = ''
1015             if sort_name == name:
1016                 if dir == '-':
1017                     s_dir = ''
1018                 else:
1019                     s_dir = '-'
1020             else:
1021                 m.append(dir+urllib.quote(name))
1022         m.insert(0, s_dir+urllib.quote(sort_name))
1023         # so things don't get completely out of hand, limit the sort to
1024         # two columns
1025         w(':sort=%s'%','.join(m[:2]))
1026         return '&'.join(l)
1029 #   ITEM TEMPLATES
1031 class ItemTemplateReplace:
1032     '''Regular-expression based parser that turns the template into HTML. 
1033     '''
1034     def __init__(self, globals, locals, cl, nodeid):
1035         self.globals = globals
1036         self.locals = locals
1037         self.cl = cl
1038         self.nodeid = nodeid
1040     replace=re.compile(
1041         r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
1042         r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)
1043     def go(self, text):
1044         return self.replace.sub(self, text)
1046     def __call__(self, m, filter=None, columns=None, sort=None, group=None):
1047         if m.group('name'):
1048             if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
1049                 replace = ItemTemplateReplace(self.globals, {}, self.cl,
1050                     self.nodeid)
1051                 return replace.go(m.group('text'))
1052             else:
1053                 return ''
1054         if m.group('display'):
1055             command = m.group('command')
1056             return eval(command, self.globals, self.locals)
1057         return '*** unhandled match: %s'%str(m.groupdict())
1060 class ItemTemplate(TemplateFunctions):
1061     '''Templating functionality specifically for item (node) display
1062     '''
1063     def __init__(self, client, templates, classname):
1064         TemplateFunctions.__init__(self)
1065         self.client = client
1066         self.instance = client.instance
1067         self.templates = templates
1068         self.classname = classname
1070         # derived
1071         self.db = self.client.db
1072         self.cl = self.db.classes[self.classname]
1073         self.properties = self.cl.getprops()
1075     def render(self, nodeid):
1076         self.nodeid = nodeid
1078         if (self.properties.has_key('type') and
1079                 self.properties.has_key('content')):
1080             pass
1081             # XXX we really want to return this as a downloadable...
1082             #  currently I handle this at a higher level by detecting 'file'
1083             #  designators...
1085         w = self.client.write
1086         w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
1087             self.classname, nodeid))
1088         s = open(os.path.join(self.templates, self.classname+'.item')).read()
1089         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
1090         w(replace.go(s))
1091         w('</form>')
1094 class NewItemTemplate(TemplateFunctions):
1095     '''Templating functionality specifically for NEW item (node) display
1096     '''
1097     def __init__(self, client, templates, classname):
1098         TemplateFunctions.__init__(self)
1099         self.client = client
1100         self.instance = client.instance
1101         self.templates = templates
1102         self.classname = classname
1104         # derived
1105         self.db = self.client.db
1106         self.cl = self.db.classes[self.classname]
1107         self.properties = self.cl.getprops()
1109     def render(self, form):
1110         self.form = form
1111         w = self.client.write
1112         c = self.classname
1113         try:
1114             s = open(os.path.join(self.templates, c+'.newitem')).read()
1115         except IOError:
1116             s = open(os.path.join(self.templates, c+'.item')).read()
1117         w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
1118         for key in form.keys():
1119             if key[0] == ':':
1120                 value = form[key].value
1121                 if type(value) != type([]): value = [value]
1122                 for value in value:
1123                     w('<input type="hidden" name="%s" value="%s">'%(key, value))
1124         replace = ItemTemplateReplace(self.globals, locals(), None, None)
1125         w(replace.go(s))
1126         w('</form>')
1129 # $Log: not supported by cvs2svn $
1130 # Revision 1.87  2002/04/03 06:12:46  richard
1131 # Fix for date properties as labels.
1133 # Revision 1.86  2002/04/03 05:54:31  richard
1134 # Fixed serialisation problem by moving the serialisation step out of the
1135 # hyperdb.Class (get, set) into the hyperdb.Database.
1137 # Also fixed htmltemplate after the showid changes I made yesterday.
1139 # Unit tests for all of the above written.
1141 # Revision 1.85  2002/04/02 01:40:58  richard
1142 #  . link() htmltemplate function now has a "showid" option for links and
1143 #    multilinks. When true, it only displays the linked node id as the anchor
1144 #    text. The link value is displayed as a tooltip using the title anchor
1145 #    attribute.
1147 # Revision 1.84  2002/03/29 19:41:48  rochecompaan
1148 #  . Fixed display of mutlilink properties when using the template
1149 #    functions, menu and plain.
1151 # Revision 1.83  2002/02/27 04:14:31  richard
1152 # Ran it through pychecker, made fixes
1154 # Revision 1.82  2002/02/21 23:11:45  richard
1155 #  . fixed some problems in date calculations (calendar.py doesn't handle over-
1156 #    and under-flow). Also, hour/minute/second intervals may now be more than
1157 #    99 each.
1159 # Revision 1.81  2002/02/21 07:21:38  richard
1160 # docco
1162 # Revision 1.80  2002/02/21 07:19:08  richard
1163 # ... and label, width and height control for extra flavour!
1165 # Revision 1.79  2002/02/21 06:57:38  richard
1166 #  . Added popup help for classes using the classhelp html template function.
1167 #    - add <display call="classhelp('priority', 'id,name,description')">
1168 #      to an item page, and it generates a link to a popup window which displays
1169 #      the id, name and description for the priority class. The description
1170 #      field won't exist in most installations, but it will be added to the
1171 #      default templates.
1173 # Revision 1.78  2002/02/21 06:23:00  richard
1174 # *** empty log message ***
1176 # Revision 1.77  2002/02/20 05:05:29  richard
1177 #  . Added simple editing for classes that don't define a templated interface.
1178 #    - access using the admin "class list" interface
1179 #    - limited to admin-only
1180 #    - requires the csv module from object-craft (url given if it's missing)
1182 # Revision 1.76  2002/02/16 09:10:52  richard
1183 # oops
1185 # Revision 1.75  2002/02/16 08:43:23  richard
1186 #  . #517906 ] Attribute order in "View customisation"
1188 # Revision 1.74  2002/02/16 08:39:42  richard
1189 #  . #516854 ] "My Issues" and redisplay
1191 # Revision 1.73  2002/02/15 07:08:44  richard
1192 #  . Alternate email addresses are now available for users. See the MIGRATION
1193 #    file for info on how to activate the feature.
1195 # Revision 1.72  2002/02/14 23:39:18  richard
1196 # . All forms now have "double-submit" protection when Javascript is enabled
1197 #   on the client-side.
1199 # Revision 1.71  2002/01/23 06:15:24  richard
1200 # real (non-string, duh) sorting of lists by node id
1202 # Revision 1.70  2002/01/23 05:47:57  richard
1203 # more HTML template cleanup and unit tests
1205 # Revision 1.69  2002/01/23 05:10:27  richard
1206 # More HTML template cleanup and unit tests.
1207 #  - download() now implemented correctly, replacing link(is_download=1) [fixed in the
1208 #    templates, but link(is_download=1) will still work for existing templates]
1210 # Revision 1.68  2002/01/22 22:55:28  richard
1211 #  . htmltemplate list() wasn't sorting...
1213 # Revision 1.67  2002/01/22 22:46:22  richard
1214 # more htmltemplate cleanups and unit tests
1216 # Revision 1.66  2002/01/22 06:35:40  richard
1217 # more htmltemplate tests and cleanup
1219 # Revision 1.65  2002/01/22 00:12:06  richard
1220 # Wrote more unit tests for htmltemplate, and while I was at it, I polished
1221 # off the implementation of some of the functions so they behave sanely.
1223 # Revision 1.64  2002/01/21 03:25:59  richard
1224 # oops
1226 # Revision 1.63  2002/01/21 02:59:10  richard
1227 # Fixed up the HTML display of history so valid links are actually displayed.
1228 # Oh for some unit tests! :(
1230 # Revision 1.62  2002/01/18 08:36:12  grubert
1231 #  . add nowrap to history table date cell i.e. <td nowrap ...
1233 # Revision 1.61  2002/01/17 23:04:53  richard
1234 #  . much nicer history display (actualy real handling of property types etc)
1236 # Revision 1.60  2002/01/17 08:48:19  grubert
1237 #  . display superseder as html link in history.
1239 # Revision 1.59  2002/01/17 07:58:24  grubert
1240 #  . display links a html link in history.
1242 # Revision 1.58  2002/01/15 00:50:03  richard
1243 # #502949 ] index view for non-issues and redisplay
1245 # Revision 1.57  2002/01/14 23:31:21  richard
1246 # reverted the change that had plain() hyperlinking the link displays -
1247 # that's what link() is for!
1249 # Revision 1.56  2002/01/14 07:04:36  richard
1250 #  . plain rendering of links in the htmltemplate now generate a hyperlink to
1251 #    the linked node's page.
1252 #    ... this allows a display very similar to bugzilla's where you can actually
1253 #    find out information about the linked node.
1255 # Revision 1.55  2002/01/14 06:45:03  richard
1256 #  . #502953 ] nosy-like treatment of other multilinks
1257 #    ... had to revert most of the previous change to the multilink field
1258 #    display... not good.
1260 # Revision 1.54  2002/01/14 05:16:51  richard
1261 # The submit buttons need a name attribute or mozilla won't submit without a
1262 # file upload. Yeah, that's bloody obscure. Grr.
1264 # Revision 1.53  2002/01/14 04:03:32  richard
1265 # How about that ... date fields have never worked ...
1267 # Revision 1.52  2002/01/14 02:20:14  richard
1268 #  . changed all config accesses so they access either the instance or the
1269 #    config attriubute on the db. This means that all config is obtained from
1270 #    instance_config instead of the mish-mash of classes. This will make
1271 #    switching to a ConfigParser setup easier too, I hope.
1273 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1274 # 0.5.0 switch, I hope!)
1276 # Revision 1.51  2002/01/10 10:02:15  grubert
1277 # In do_history: replace "." in date by " " so html wraps more sensible.
1278 # Should this be done in date's string converter ?
1280 # Revision 1.50  2002/01/05 02:35:10  richard
1281 # I18N'ification
1283 # Revision 1.49  2001/12/20 15:43:01  rochecompaan
1284 # Features added:
1285 #  .  Multilink properties are now displayed as comma separated values in
1286 #     a textbox
1287 #  .  The add user link is now only visible to the admin user
1288 #  .  Modified the mail gateway to reject submissions from unknown
1289 #     addresses if ANONYMOUS_ACCESS is denied
1291 # Revision 1.48  2001/12/20 06:13:24  rochecompaan
1292 # Bugs fixed:
1293 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1294 #     lost somewhere
1295 #   . Internet Explorer submits full path for filename - we now strip away
1296 #     the path
1297 # Features added:
1298 #   . Link and multilink properties are now displayed sorted in the cgi
1299 #     interface
1301 # Revision 1.47  2001/11/26 22:55:56  richard
1302 # Feature:
1303 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
1304 #    the instance.
1305 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1306 #    signature info in e-mails.
1307 #  . Some more flexibility in the mail gateway and more error handling.
1308 #  . Login now takes you to the page you back to the were denied access to.
1310 # Fixed:
1311 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
1313 # Revision 1.46  2001/11/24 00:53:12  jhermann
1314 # "except:" is bad, bad , bad!
1316 # Revision 1.45  2001/11/22 15:46:42  jhermann
1317 # Added module docstrings to all modules.
1319 # Revision 1.44  2001/11/21 23:35:45  jhermann
1320 # Added globbing for win32, and sample marking in a 2nd file to test it
1322 # Revision 1.43  2001/11/21 04:04:43  richard
1323 # *sigh* more missing value handling
1325 # Revision 1.42  2001/11/21 03:40:54  richard
1326 # more new property handling
1328 # Revision 1.41  2001/11/15 10:26:01  richard
1329 #  . missing "return" in filter_section (thanks Roch'e Compaan)
1331 # Revision 1.40  2001/11/03 01:56:51  richard
1332 # More HTML compliance fixes. This will probably fix the Netscape problem
1333 # too.
1335 # Revision 1.39  2001/11/03 01:43:47  richard
1336 # Ahah! Fixed the lynx problem - there was a hidden input field misplaced.
1338 # Revision 1.38  2001/10/31 06:58:51  richard
1339 # Added the wrap="hard" attribute to the textarea of the note field so the
1340 # messages wrap sanely.
1342 # Revision 1.37  2001/10/31 06:24:35  richard
1343 # Added do_stext to htmltemplate, thanks Brad Clements.
1345 # Revision 1.36  2001/10/28 22:51:38  richard
1346 # Fixed ENOENT/WindowsError thing, thanks Juergen Hermann
1348 # Revision 1.35  2001/10/24 00:04:41  richard
1349 # Removed the "infinite authentication loop", thanks Roch'e
1351 # Revision 1.34  2001/10/23 22:56:36  richard
1352 # Bugfix in filter "widget" placement, thanks Roch'e
1354 # Revision 1.33  2001/10/23 01:00:18  richard
1355 # Re-enabled login and registration access after lopping them off via
1356 # disabling access for anonymous users.
1357 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1358 # a couple of bugs while I was there. Probably introduced a couple, but
1359 # things seem to work OK at the moment.
1361 # Revision 1.32  2001/10/22 03:25:01  richard
1362 # Added configuration for:
1363 #  . anonymous user access and registration (deny/allow)
1364 #  . filter "widget" location on index page (top, bottom, both)
1365 # Updated some documentation.
1367 # Revision 1.31  2001/10/21 07:26:35  richard
1368 # feature #473127: Filenames. I modified the file.index and htmltemplate
1369 #  source so that the filename is used in the link and the creation
1370 #  information is displayed.
1372 # Revision 1.30  2001/10/21 04:44:50  richard
1373 # bug #473124: UI inconsistency with Link fields.
1374 #    This also prompted me to fix a fairly long-standing usability issue -
1375 #    that of being able to turn off certain filters.
1377 # Revision 1.29  2001/10/21 00:17:56  richard
1378 # CGI interface view customisation section may now be hidden (patch from
1379 #  Roch'e Compaan.)
1381 # Revision 1.28  2001/10/21 00:00:16  richard
1382 # Fixed Checklist function - wasn't always working on a list.
1384 # Revision 1.27  2001/10/20 12:13:44  richard
1385 # Fixed grouping of non-str properties (thanks Roch'e Compaan)
1387 # Revision 1.26  2001/10/14 10:55:00  richard
1388 # Handle empty strings in HTML template Link function
1390 # Revision 1.25  2001/10/09 07:25:59  richard
1391 # Added the Password property type. See "pydoc roundup.password" for
1392 # implementation details. Have updated some of the documentation too.
1394 # Revision 1.24  2001/09/27 06:45:58  richard
1395 # *gak* ... xmp is Old Skool apparently. Am using pre again by have the option
1396 # on the plain() template function to escape the text for HTML.
1398 # Revision 1.23  2001/09/10 09:47:18  richard
1399 # Fixed bug in the generation of links to Link/Multilink in indexes.
1400 #   (thanks Hubert Hoegl)
1401 # Added AssignedTo to the "classic" schema's item page.
1403 # Revision 1.22  2001/08/30 06:01:17  richard
1404 # Fixed missing import in mailgw :(
1406 # Revision 1.21  2001/08/16 07:34:59  richard
1407 # better CGI text searching - but hidden filter fields are disappearing...
1409 # Revision 1.20  2001/08/15 23:43:18  richard
1410 # Fixed some isFooTypes that I missed.
1411 # Refactored some code in the CGI code.
1413 # Revision 1.19  2001/08/12 06:32:36  richard
1414 # using isinstance(blah, Foo) now instead of isFooType
1416 # Revision 1.18  2001/08/07 00:24:42  richard
1417 # stupid typo
1419 # Revision 1.17  2001/08/07 00:15:51  richard
1420 # Added the copyright/license notice to (nearly) all files at request of
1421 # Bizar Software.
1423 # Revision 1.16  2001/08/01 03:52:23  richard
1424 # Checklist was using wrong name.
1426 # Revision 1.15  2001/07/30 08:12:17  richard
1427 # Added time logging and file uploading to the templates.
1429 # Revision 1.14  2001/07/30 06:17:45  richard
1430 # Features:
1431 #  . Added ability for cgi newblah forms to indicate that the new node
1432 #    should be linked somewhere.
1433 # Fixed:
1434 #  . Fixed the agument handling for the roundup-admin find command.
1435 #  . Fixed handling of summary when no note supplied for newblah. Again.
1436 #  . Fixed detection of no form in htmltemplate Field display.
1438 # Revision 1.13  2001/07/30 02:37:53  richard
1439 # Temporary measure until we have decent schema migration.
1441 # Revision 1.12  2001/07/30 01:24:33  richard
1442 # Handles new node display now.
1444 # Revision 1.11  2001/07/29 09:31:35  richard
1445 # oops
1447 # Revision 1.10  2001/07/29 09:28:23  richard
1448 # Fixed sorting by clicking on column headings.
1450 # Revision 1.9  2001/07/29 08:27:40  richard
1451 # Fixed handling of passed-in values in form elements (ie. during a
1452 # drill-down)
1454 # Revision 1.8  2001/07/29 07:01:39  richard
1455 # Added vim command to all source so that we don't get no steenkin' tabs :)
1457 # Revision 1.7  2001/07/29 05:36:14  richard
1458 # Cleanup of the link label generation.
1460 # Revision 1.6  2001/07/29 04:06:42  richard
1461 # Fixed problem in link display when Link value is None.
1463 # Revision 1.5  2001/07/28 08:17:09  richard
1464 # fixed use of stylesheet
1466 # Revision 1.4  2001/07/28 07:59:53  richard
1467 # Replaced errno integers with their module values.
1468 # De-tabbed templatebuilder.py
1470 # Revision 1.3  2001/07/25 03:39:47  richard
1471 # Hrm - displaying links to classes that don't specify a key property. I've
1472 # got it defaulting to 'name', then 'title' and then a "random" property (first
1473 # one returned by getprops().keys().
1474 # Needs to be moved onto the Class I think...
1476 # Revision 1.2  2001/07/22 12:09:32  richard
1477 # Final commit of Grande Splite
1479 # Revision 1.1  2001/07/22 11:58:35  richard
1480 # More Grande Splite
1483 # vim: set filetype=python ts=4 sw=4 et si