Code

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