Code

Added commentage to the dbinit files to help people with their
[roundup.git] / roundup / htmltemplate.py
index 042e7e53ea63da840887cb9f7ba906c71390e1d6..2f6557a1fdb0abba2b3e4d660048c9cfdc2e923b 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: htmltemplate.py,v 1.47 2001-11-26 22:55:56 richard Exp $
+# $Id: htmltemplate.py,v 1.89 2002-05-15 06:34:47 richard Exp $
 
 __doc__ = """
 Template engine.
 """
 
-import os, re, StringIO, urllib, cgi, errno
+import os, re, StringIO, urllib, cgi, errno, types, urllib
 
-import hyperdb, date, password
+import hyperdb, date
+from i18n import _
 
 # This imports the StructureText functionality for the do_stext function
 # get it from http://dev.zope.org/Members/jim/StructuredTextWiki/NGReleases
@@ -32,7 +33,15 @@ try:
 except ImportError:
     StructuredText = None
 
+class MissingTemplateError(ValueError):
+    '''Error raised when a template file is missing
+    '''
+    pass
+
 class TemplateFunctions:
+    '''Defines the templating functions that are used in the HTML templates
+       of the roundup web interface.
+    '''
     def __init__(self):
         self.form = None
         self.nodeid = None
@@ -42,6 +51,15 @@ class TemplateFunctions:
             if key[:3] == 'do_':
                 self.globals[key[3:]] = getattr(self, key)
 
+        # These are added by the subclass where appropriate
+        self.client = None
+        self.instance = None
+        self.templates = None
+        self.classname = None
+        self.db = None
+        self.cl = None
+        self.properties = None
+
     def do_plain(self, property, escape=0):
         ''' display a String property directly;
 
@@ -52,12 +70,12 @@ class TemplateFunctions:
             linked nodes (or the ids if the linked class has no key property)
         '''
         if not self.nodeid and self.form is None:
-            return '[Field: not called from item]'
+            return _('[Field: not called from item]')
         propclass = self.properties[property]
         if self.nodeid:
             # make sure the property is a valid one
             # TODO: this tests, but we should handle the exception
-            prop_test = self.cl.getprops()[property]
+            dummy = self.cl.getprops()[property]
 
             # get the value for this property
             try:
@@ -76,22 +94,28 @@ class TemplateFunctions:
             else: value = str(value)
         elif isinstance(propclass, hyperdb.Password):
             if value is None: value = ''
-            else: value = '*encrypted*'
+            else: value = _('*encrypted*')
         elif isinstance(propclass, hyperdb.Date):
+            # this gives "2002-01-17.06:54:39", maybe replace the "." by a " ".
             value = str(value)
         elif isinstance(propclass, hyperdb.Interval):
             value = str(value)
         elif isinstance(propclass, hyperdb.Link):
             linkcl = self.db.classes[propclass.classname]
             k = linkcl.labelprop()
-            if value: value = str(linkcl.get(value, k))
-            else: value = '[unselected]'
+            if value:
+                value = linkcl.get(value, k)
+            else:
+                value = _('[unselected]')
         elif isinstance(propclass, hyperdb.Multilink):
             linkcl = self.db.classes[propclass.classname]
             k = linkcl.labelprop()
-            value = ', '.join([linkcl.get(i, k) for i in value])
+            labels = []
+            for v in value:
+                labels.append(linkcl.get(v, k))
+            value = ', '.join(labels)
         else:
-            s = _('Plain: bad propclass "%(propclass)s"')%locals()
+            value = _('Plain: bad propclass "%(propclass)s"')%locals()
         if escape:
             value = cgi.escape(value)
         return value
@@ -105,52 +129,83 @@ class TemplateFunctions:
             return s
         return StructuredText(s,level=1,header=0)
 
-    def do_field(self, property, size=None, height=None, showid=0):
-        ''' display a property like the plain displayer, but in a text field
-            to be edited
+    def determine_value(self, property):
+        '''determine the value of a property using the node, form or
+           filterspec
         '''
-        if not self.nodeid and self.form is None and self.filterspec is None:
-            return _('[Field: not called from item]')
         propclass = self.properties[property]
         if self.nodeid:
             value = self.cl.get(self.nodeid, property, None)
-            # TODO: remove this from the code ... it's only here for
-            # handling schema changes, and they should be handled outside
-            # of this code...
             if isinstance(propclass, hyperdb.Multilink) and value is None:
-                value = []
+                return []
+            return value
         elif self.filterspec is not None:
             if isinstance(propclass, hyperdb.Multilink):
-                value = self.filterspec.get(property, [])
+                return self.filterspec.get(property, [])
             else:
-                value = self.filterspec.get(property, '')
+                return self.filterspec.get(property, '')
+        # TODO: pull the value from the form
+        if isinstance(propclass, hyperdb.Multilink):
+            return []
         else:
-            # TODO: pull the value from the form
-            if isinstance(propclass, hyperdb.Multilink): value = []
-            else: value = ''
+            return ''
+
+    def make_sort_function(self, classname):
+        '''Make a sort function for a given class
+        '''
+        linkcl = self.db.classes[classname]
+        if linkcl.getprops().has_key('order'):
+            sort_on = 'order'
+        else:
+            sort_on = linkcl.labelprop()
+        def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
+            return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
+        return sortfunc
+
+    def do_field(self, property, size=None, showid=0):
+        ''' display a property like the plain displayer, but in a text field
+            to be edited
+
+            Note: if you would prefer an option list style display for
+            link or multilink editing, use menu().
+        '''
+        if not self.nodeid and self.form is None and self.filterspec is None:
+            return _('[Field: not called from item]')
+
+        if size is None:
+            size = 30
+
+        propclass = self.properties[property]
+
+        # get the value
+        value = self.determine_value(property)
+
+        # now display
         if (isinstance(propclass, hyperdb.String) or
                 isinstance(propclass, hyperdb.Date) or
                 isinstance(propclass, hyperdb.Interval)):
-            size = size or 30
             if value is None:
                 value = ''
             else:
-                value = cgi.escape(value)
+                value = cgi.escape(str(value))
                 value = '"'.join(value.split('"'))
             s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
         elif isinstance(propclass, hyperdb.Password):
-            size = size or 30
             s = '<input type="password" name="%s" size="%s">'%(property, size)
         elif isinstance(propclass, hyperdb.Link):
+            sortfunc = self.make_sort_function(propclass.classname)
             linkcl = self.db.classes[propclass.classname]
+            options = linkcl.list()
+            options.sort(sortfunc)
+            # TODO: make this a field display, not a menu one!
             l = ['<select name="%s">'%property]
             k = linkcl.labelprop()
             if value is None:
                 s = 'selected '
             else:
                 s = ''
-            l.append('<option %svalue="-1">- no selection -</option>'%s)
-            for optionid in linkcl.list():
+            l.append(_('<option %svalue="-1">- no selection -</option>')%s)
+            for optionid in options:
                 option = linkcl.get(optionid, k)
                 s = ''
                 if optionid == value:
@@ -161,19 +216,77 @@ class TemplateFunctions:
                     lab = option
                 if size is not None and len(lab) > size:
                     lab = lab[:size-3] + '...'
+                lab = cgi.escape(lab)
                 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
             l.append('</select>')
             s = '\n'.join(l)
         elif isinstance(propclass, hyperdb.Multilink):
+            sortfunc = self.make_sort_function(propclass.classname)
             linkcl = self.db.classes[propclass.classname]
-            list = linkcl.list()
-            height = height or min(len(list), 7)
+            if value:
+                value.sort(sortfunc)
+            # map the id to the label property
+            if not showid:
+                k = linkcl.labelprop()
+                value = [linkcl.get(v, k) for v in value]
+            value = cgi.escape(','.join(value))
+            s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
+        else:
+            s = _('Plain: bad propclass "%(propclass)s"')%locals()
+        return s
+
+    def do_multiline(self, property, rows=5, cols=40):
+        ''' display a string property in a multiline text edit field
+        '''
+        if not self.nodeid and self.form is None and self.filterspec is None:
+            return _('[Multiline: not called from item]')
+
+        propclass = self.properties[property]
+
+        # make sure this is a link property
+        if not isinstance(propclass, hyperdb.String):
+            return _('[Multiline: not a string]')
+
+        # get the value
+        value = self.determine_value(property)
+        if value is None:
+            value = ''
+
+        # display
+        return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
+            property, rows, cols, value)
+
+    def do_menu(self, property, size=None, height=None, showid=0):
+        ''' for a Link property, display a menu of the available choices
+        '''
+        if not self.nodeid and self.form is None and self.filterspec is None:
+            return _('[Field: not called from item]')
+
+        propclass = self.properties[property]
+
+        # make sure this is a link property
+        if not (isinstance(propclass, hyperdb.Link) or
+                isinstance(propclass, hyperdb.Multilink)):
+            return _('[Menu: not a link]')
+
+        # sort function
+        sortfunc = self.make_sort_function(propclass.classname)
+
+        # get the value
+        value = self.determine_value(property)
+
+        # display
+        if isinstance(propclass, hyperdb.Multilink):
+            linkcl = self.db.classes[propclass.classname]
+            options = linkcl.list()
+            options.sort(sortfunc)
+            height = height or min(len(options), 7)
             l = ['<select multiple name="%s" size="%s">'%(property, height)]
             k = linkcl.labelprop()
-            for optionid in list:
+            for optionid in options:
                 option = linkcl.get(optionid, k)
                 s = ''
-                if optionid in value:
+                if optionid in value or option in value:
                     s = 'selected '
                 if showid:
                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
@@ -181,49 +294,28 @@ class TemplateFunctions:
                     lab = option
                 if size is not None and len(lab) > size:
                     lab = lab[:size-3] + '...'
-                l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
+                lab = cgi.escape(lab)
+                l.append('<option %svalue="%s">%s</option>'%(s, optionid,
+                    lab))
             l.append('</select>')
-            s = '\n'.join(l)
-        else:
-            s = 'Plain: bad propclass "%s"'%propclass
-        return s
-
-    def do_menu(self, property, size=None, height=None, showid=0):
-        ''' for a Link property, display a menu of the available choices
-        '''
-        propclass = self.properties[property]
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            # TODO: pull the value from the form
-            if isinstance(propclass, hyperdb.Multilink): value = []
-            else: value = None
+            return '\n'.join(l)
         if isinstance(propclass, hyperdb.Link):
+            # force the value to be a single choice
+            if type(value) is types.ListType:
+                value = value[0]
             linkcl = self.db.classes[propclass.classname]
             l = ['<select name="%s">'%property]
             k = linkcl.labelprop()
             s = ''
             if value is None:
                 s = 'selected '
-            l.append('<option %svalue="-1">- no selection -</option>'%s)
-            for optionid in linkcl.list():
-                option = linkcl.get(optionid, k)
-                s = ''
-                if optionid == value:
-                    s = 'selected '
-                l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
-            l.append('</select>')
-            return '\n'.join(l)
-        if isinstance(propclass, hyperdb.Multilink):
-            linkcl = self.db.classes[propclass.classname]
-            list = linkcl.list()
-            height = height or min(len(list), 7)
-            l = ['<select multiple name="%s" size="%s">'%(property, height)]
-            k = linkcl.labelprop()
-            for optionid in list:
+            l.append(_('<option %svalue="-1">- no selection -</option>')%s)
+            options = linkcl.list()
+            options.sort(sortfunc)
+            for optionid in options:
                 option = linkcl.get(optionid, k)
                 s = ''
-                if optionid in value:
+                if value in [optionid, option]:
                     s = 'selected '
                 if showid:
                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
@@ -231,13 +323,14 @@ class TemplateFunctions:
                     lab = option
                 if size is not None and len(lab) > size:
                     lab = lab[:size-3] + '...'
-                l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
+                lab = cgi.escape(lab)
+                l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
             l.append('</select>')
             return '\n'.join(l)
-        return '[Menu: not a link]'
+        return _('[Menu: not a link]')
 
     #XXX deviates from spec
-    def do_link(self, property=None, is_download=0):
+    def do_link(self, property=None, is_download=0, showid=0):
         '''For a Link or Multilink property, display the names of the linked
            nodes, hyperlinked to the item views on those nodes.
            For other properties, link to this node with the property as the
@@ -248,42 +341,52 @@ class TemplateFunctions:
            downloaded file name is correct.
         '''
         if not self.nodeid and self.form is None:
-            return '[Link: not called from item]'
+            return _('[Link: not called from item]')
+
+        # get the value
+        value = self.determine_value(property)
+        if not value:
+            return _('[no %(propname)s]')%{'propname':property.capitalize()}
+
         propclass = self.properties[property]
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            if isinstance(propclass, hyperdb.Multilink): value = []
-            elif isinstance(propclass, hyperdb.Link): value = None
-            else: value = ''
         if isinstance(propclass, hyperdb.Link):
             linkname = propclass.classname
-            if value is None: return '[no %s]'%property.capitalize()
             linkcl = self.db.classes[linkname]
             k = linkcl.labelprop()
-            linkvalue = linkcl.get(value, k)
+            linkvalue = cgi.escape(str(linkcl.get(value, k)))
+            if showid:
+                label = value
+                title = ' title="%s"'%linkvalue
+                # note ... this should be urllib.quote(linkcl.get(value, k))
+            else:
+                label = linkvalue
+                title = ''
             if is_download:
-                return '<a href="%s%s/%s">%s</a>'%(linkname, value,
-                    linkvalue, linkvalue)
+                return '<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
+                    linkvalue, title, label)
             else:
-                return '<a href="%s%s">%s</a>'%(linkname, value, linkvalue)
+                return '<a href="%s%s"%s>%s</a>'%(linkname, value, title, label)
         if isinstance(propclass, hyperdb.Multilink):
             linkname = propclass.classname
             linkcl = self.db.classes[linkname]
             k = linkcl.labelprop()
-            if not value : return '[no %s]'%property.capitalize()
             l = []
             for value in value:
-                linkvalue = linkcl.get(value, k)
+                linkvalue = cgi.escape(str(linkcl.get(value, k)))
+                if showid:
+                    label = value
+                    title = ' title="%s"'%linkvalue
+                    # note ... this should be urllib.quote(linkcl.get(value, k))
+                else:
+                    label = linkvalue
+                    title = ''
                 if is_download:
-                    l.append('<a href="%s%s/%s">%s</a>'%(linkname, value,
-                        linkvalue, linkvalue))
+                    l.append('<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
+                        linkvalue, title, label))
                 else:
-                    l.append('<a href="%s%s">%s</a>'%(linkname, value,
-                        linkvalue))
+                    l.append('<a href="%s%s"%s>%s</a>'%(linkname, value,
+                        title, label))
             return ', '.join(l)
-        if isinstance(propclass, hyperdb.String):
-            if value == '': value = '[no %s]'%property.capitalize()
         if is_download:
             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
                 value, value)
@@ -295,12 +398,15 @@ class TemplateFunctions:
             the list
         '''
         if not self.nodeid:
-            return '[Count: not called from item]'
+            return _('[Count: not called from item]')
+
         propclass = self.properties[property]
+        if not isinstance(propclass, hyperdb.Multilink):
+            return _('[Count: not a Multilink]')
+
+        # figure the length then...
         value = self.cl.get(self.nodeid, property)
-        if isinstance(propclass, hyperdb.Multilink):
-            return str(len(value))
-        return '[Count: not a Multilink]'
+        return str(len(value))
 
     # XXX pretty is definitely new ;)
     def do_reldate(self, property, pretty=0):
@@ -310,22 +416,25 @@ class TemplateFunctions:
             with the 'pretty' flag, make it pretty
         '''
         if not self.nodeid and self.form is None:
-            return '[Reldate: not called from item]'
+            return _('[Reldate: not called from item]')
+
         propclass = self.properties[property]
-        if isinstance(not propclass, hyperdb.Date):
-            return '[Reldate: not a Date]'
+        if not isinstance(propclass, hyperdb.Date):
+            return _('[Reldate: not a Date]')
+
         if self.nodeid:
             value = self.cl.get(self.nodeid, property)
         else:
-            value = date.Date('.')
-        interval = value - date.Date('.')
+            return ''
+        if not value:
+            return ''
+
+        # figure the interval
+        interval = date.Date('.') - value
         if pretty:
             if not self.nodeid:
-                return 'now'
-            pretty = interval.pretty()
-            if pretty is None:
-                pretty = value.pretty()
-            return pretty
+                return _('now')
+            return interval.pretty()
         return str(interval)
 
     def do_download(self, property, **args):
@@ -333,21 +442,8 @@ class TemplateFunctions:
             allow you to download files
         '''
         if not self.nodeid:
-            return '[Download: not called from item]'
-        propclass = self.properties[property]
-        value = self.cl.get(self.nodeid, property)
-        if isinstance(propclass, hyperdb.Link):
-            linkcl = self.db.classes[propclass.classname]
-            linkvalue = linkcl.get(value, k)
-            return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
-        if isinstance(propclass, hyperdb.Multilink):
-            linkcl = self.db.classes[propclass.classname]
-            l = []
-            for value in value:
-                linkvalue = linkcl.get(value, k)
-                l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
-            return ', '.join(l)
-        return '[Download: not a link]'
+            return _('[Download: not called from item]')
+        return self.do_link(property, is_download=1)
 
 
     def do_checklist(self, property, **args):
@@ -357,7 +453,7 @@ class TemplateFunctions:
         propclass = self.properties[property]
         if (not isinstance(propclass, hyperdb.Link) and not
                 isinstance(propclass, hyperdb.Multilink)):
-            return '[Checklist: not a link]'
+            return _('[Checklist: not a link]')
 
         # get our current checkbox state
         if self.nodeid:
@@ -378,7 +474,7 @@ class TemplateFunctions:
         l = []
         k = linkcl.labelprop()
         for optionid in linkcl.list():
-            option = linkcl.get(optionid, k)
+            option = cgi.escape(str(linkcl.get(optionid, k)))
             if optionid in value or option in value:
                 checked = 'checked'
             else:
@@ -392,8 +488,8 @@ class TemplateFunctions:
                 checked = 'checked'
             else:
                 checked = ''
-            l.append('[unselected]:<input type="checkbox" %s name="%s" '
-                'value="-1">'%(checked, property))
+            l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
+                'value="-1">')%(checked, property))
         return '\n'.join(l)
 
     def do_note(self, rows=5, cols=80):
@@ -411,10 +507,18 @@ class TemplateFunctions:
         '''
         propcl = self.properties[property]
         if not isinstance(propcl, hyperdb.Multilink):
-            return '[List: not a Multilink]'
-        value = self.cl.get(self.nodeid, property)
+            return _('[List: not a Multilink]')
+
+        value = self.determine_value(property)
+        if not value:
+            return ''
+
+        # sort, possibly revers and then re-stringify
+        value = map(int, value)
+        value.sort()
         if reverse:
             value.reverse()
+        value = map(str, value)
 
         # render the sub-index into a string
         fp = StringIO.StringIO()
@@ -429,22 +533,141 @@ class TemplateFunctions:
         return fp.getvalue()
 
     # XXX new function
-    def do_history(self, **args):
+    def do_history(self, direction='descending'):
         ''' list the history of the item
+
+            If "direction" is 'descending' then the most recent event will
+            be displayed first. If it is 'ascending' then the oldest event
+            will be displayed first.
         '''
         if self.nodeid is None:
-            return "[History: node doesn't exist]"
+            return _("[History: node doesn't exist]")
 
         l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
             '<tr class="list-header">',
-            '<td><span class="list-item"><strong>Date</strong></span></td>',
-            '<td><span class="list-item"><strong>User</strong></span></td>',
-            '<td><span class="list-item"><strong>Action</strong></span></td>',
-            '<td><span class="list-item"><strong>Args</strong></span></td>']
-
-        for id, date, user, action, args in self.cl.history(self.nodeid):
-            l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
-               date, user, action, args))
+            _('<th align=left><span class="list-item">Date</span></th>'),
+            _('<th align=left><span class="list-item">User</span></th>'),
+            _('<th align=left><span class="list-item">Action</span></th>'),
+            _('<th align=left><span class="list-item">Args</span></th>'),
+            '</tr>']
+
+        comments = {}
+        history = self.cl.history(self.nodeid)
+        history.sort()
+        if direction == 'descending':
+            history.reverse()
+        for id, evt_date, user, action, args in history:
+            date_s = str(evt_date).replace("."," ")
+            arg_s = ''
+            if action == 'link' and type(args) == type(()):
+                if len(args) == 3:
+                    linkcl, linkid, key = args
+                    arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+                        linkcl, linkid, key)
+                else:
+                    arg_s = str(args)
+
+            elif action == 'unlink' and type(args) == type(()):
+                if len(args) == 3:
+                    linkcl, linkid, key = args
+                    arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
+                        linkcl, linkid, key)
+                else:
+                    arg_s = str(args)
+
+            elif type(args) == type({}):
+                cell = []
+                for k in args.keys():
+                    # try to get the relevant property and treat it
+                    # specially
+                    try:
+                        prop = self.properties[k]
+                    except:
+                        prop = None
+                    if prop is not None:
+                        if args[k] and (isinstance(prop, hyperdb.Multilink) or
+                                isinstance(prop, hyperdb.Link)):
+                            # figure what the link class is
+                            classname = prop.classname
+                            try:
+                                linkcl = self.db.classes[classname]
+                            except KeyError:
+                                labelprop = None
+                                comments[classname] = _('''The linked class
+                                    %(classname)s no longer exists''')%locals()
+                            labelprop = linkcl.labelprop()
+
+                        if isinstance(prop, hyperdb.Multilink) and \
+                                len(args[k]) > 0:
+                            ml = []
+                            for linkid in args[k]:
+                                label = classname + linkid
+                                # if we have a label property, try to use it
+                                # TODO: test for node existence even when
+                                # there's no labelprop!
+                                try:
+                                    if labelprop is not None:
+                                        label = linkcl.get(linkid, labelprop)
+                                except IndexError:
+                                    comments['no_link'] = _('''<strike>The
+                                        linked node no longer
+                                        exists</strike>''')
+                                    ml.append('<strike>%s</strike>'%label)
+                                else:
+                                    ml.append('<a href="%s%s">%s</a>'%(
+                                        classname, linkid, label))
+                            cell.append('%s:\n  %s'%(k, ',\n  '.join(ml)))
+                        elif isinstance(prop, hyperdb.Link) and args[k]:
+                            label = classname + args[k]
+                            # if we have a label property, try to use it
+                            # TODO: test for node existence even when
+                            # there's no labelprop!
+                            if labelprop is not None:
+                                try:
+                                    label = linkcl.get(args[k], labelprop)
+                                except IndexError:
+                                    comments['no_link'] = _('''<strike>The
+                                        linked node no longer
+                                        exists</strike>''')
+                                    cell.append(' <strike>%s</strike>,\n'%label)
+                                    # "flag" this is done .... euwww
+                                    label = None
+                            if label is not None:
+                                cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
+                                    classname, args[k], label))
+
+                        elif isinstance(prop, hyperdb.Date) and args[k]:
+                            d = date.Date(args[k])
+                            cell.append('%s: %s'%(k, str(d)))
+
+                        elif isinstance(prop, hyperdb.Interval) and args[k]:
+                            d = date.Interval(args[k])
+                            cell.append('%s: %s'%(k, str(d)))
+
+                        elif not args[k]:
+                            cell.append('%s: (no value)\n'%k)
+
+                        else:
+                            cell.append('%s: %s\n'%(k, str(args[k])))
+                    else:
+                        # property no longer exists
+                        comments['no_exist'] = _('''<em>The indicated property
+                            no longer exists</em>''')
+                        cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
+                arg_s = '<br />'.join(cell)
+            else:
+                # unkown event!!
+                comments['unknown'] = _('''<strong><em>This event is not
+                    handled by the history display!</em></strong>''')
+                arg_s = '<strong><em>' + str(args) + '</em></strong>'
+            date_s = date_s.replace(' ', '&nbsp;')
+            l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
+                '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
+                user, action, arg_s))
+        if comments:
+            l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
+        for entry in comments.values():
+            l.append('<tr><td colspan=4>%s</td></tr>'%entry)
         l.append('</table>')
         return '\n'.join(l)
 
@@ -453,17 +676,33 @@ class TemplateFunctions:
         ''' add a submit button for the item
         '''
         if self.nodeid:
-            return '<input type="submit" value="Submit Changes">'
+            return _('<input type="submit" name="submit" value="Submit Changes">')
         elif self.form is not None:
-            return '<input type="submit" value="Submit New Entry">'
+            return _('<input type="submit" name="submit" value="Submit New Entry">')
         else:
-            return '[Submit: not called from item]'
+            return _('[Submit: not called from item]')
 
+    def do_classhelp(self, classname, properties, label='?', width='400',
+            height='400'):
+        '''pop up a javascript window with class help
 
+           This generates a link to a popup window which displays the 
+           properties indicated by "properties" of the class named by
+           "classname". The "properties" should be a comma-separated list
+           (eg. 'id,name,description').
+
+           You may optionally override the label displayed, the width and
+           height. The popup window will be resizable and scrollable.
+        '''
+        return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
+            'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(classname,
+            properties, width, height, label)
 #
 #   INDEX TEMPLATES
 #
 class IndexTemplateReplace:
+    '''Regular-expression based parser that turns the template into HTML. 
+    '''
     def __init__(self, globals, locals, props):
         self.globals = globals
         self.locals = locals
@@ -480,17 +719,21 @@ class IndexTemplateReplace:
             if m.group('name') in self.props:
                 text = m.group('text')
                 replace = IndexTemplateReplace(self.globals, {}, self.props)
-                return replace.go(m.group('text'))
+                return replace.go(text)
             else:
                 return ''
         if m.group('display'):
             command = m.group('command')
             return eval(command, self.globals, self.locals)
-        print '*** unhandled match', m.groupdict()
+        return '*** unhandled match: %s'%str(m.groupdict())
 
 class IndexTemplate(TemplateFunctions):
+    '''Templating functionality specifically for index pages
+    '''
     def __init__(self, client, templates, classname):
+        TemplateFunctions.__init__(self)
         self.client = client
+        self.instance = client.instance
         self.templates = templates
         self.classname = classname
 
@@ -499,8 +742,6 @@ class IndexTemplate(TemplateFunctions):
         self.cl = self.db.classes[self.classname]
         self.properties = self.cl.getprops()
 
-        TemplateFunctions.__init__(self)
-
     col_re=re.compile(r'<property\s+name="([^>]+)">')
     def render(self, filterspec={}, filter=[], columns=[], sort=[], group=[],
             show_display_form=1, nodeids=None, show_customization=1):
@@ -520,8 +761,12 @@ class IndexTemplate(TemplateFunctions):
 
         # XXX deviate from spec here ...
         # load the index section template and figure the default columns from it
-        template = open(os.path.join(self.templates,
-            self.classname+'.index')).read()
+        try:
+            template = open(os.path.join(self.templates,
+                self.classname+'.index')).read()
+        except IOError, error:
+            if error.errno not in (errno.ENOENT, errno.ESRCH): raise
+            raise MissingTemplateError, self.classname+'.index'
         all_columns = self.col_re.findall(template)
         if not columns:
             columns = []
@@ -536,9 +781,9 @@ class IndexTemplate(TemplateFunctions):
             columns = l
 
         # display the filter section
-        if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
-                self.client.FILTER_POSITION in ('top and bottom', 'top')):
-            w('<form action="index">\n')
+        if (show_display_form and 
+                self.instance.FILTER_POSITION in ('top and bottom', 'top')):
+            w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
             self.filter_section(filter_template, filter, columns, group,
                 all_filters, all_columns, show_customization)
             # make sure that the sorting doesn't get lost either
@@ -577,7 +822,8 @@ class IndexTemplate(TemplateFunctions):
         for nodeid in nodeids:
             # check for a group heading
             if group_names:
-                this_group = [self.cl.get(nodeid, name, '[no value]') for name in group_names]
+                this_group = [self.cl.get(nodeid, name, _('[no value]'))
+                    for name in group_names]
                 if this_group != old_group:
                     l = []
                     for name in group_names:
@@ -587,7 +833,8 @@ class IndexTemplate(TemplateFunctions):
                             key = group_cl.getkey()
                             value = self.cl.get(nodeid, name)
                             if value is None:
-                                l.append('[unselected %s]'%prop.classname)
+                                l.append(_('[unselected %(classname)s]')%{
+                                    'classname': prop.classname})
                             else:
                                 l.append(group_cl.get(self.cl.get(nodeid,
                                     name), key))
@@ -597,9 +844,9 @@ class IndexTemplate(TemplateFunctions):
                             for value in self.cl.get(nodeid, name):
                                 l.append(group_cl.get(value, key))
                         else:
-                            value = self.cl.get(nodeid, name, '[no value]')
+                            value = self.cl.get(nodeid, name, _('[no value]'))
                             if value is None:
-                                value = '[empty %s]'%name
+                                value = _('[empty %(name)s]')%locals()
                             else:
                                 value = str(value)
                             l.append(value)
@@ -617,9 +864,9 @@ class IndexTemplate(TemplateFunctions):
         w('</table>')
 
         # display the filter section
-        if (show_display_form and hasattr(self.client, 'FILTER_POSITION') and
-                self.client.FILTER_POSITION in ('top and bottom', 'bottom')):
-            w('<form action="index">\n')
+        if (show_display_form and hasattr(self.instance, 'FILTER_POSITION') and
+                self.instance.FILTER_POSITION in ('top and bottom', 'bottom')):
+            w('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
             self.filter_section(filter_template, filter, columns, group,
                 all_filters, all_columns, show_customization)
             # make sure that the sorting doesn't get lost either
@@ -642,12 +889,12 @@ class IndexTemplate(TemplateFunctions):
             # display the filter section
             w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
             w('<tr class="location-bar">')
-            w(' <th align="left" colspan="2">Filter specification...</th>')
+            w(_(' <th align="left" colspan="2">Filter specification...</th>'))
             w('</tr>')
             replace = IndexTemplateReplace(self.globals, locals(), filter)
             w(replace.go(template))
             w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
-            w('<td><input type="submit" name="action" value="Redisplay"></td></tr>')
+            w(_('<td><input type="submit" name="action" value="Redisplay"></td></tr>'))
             w('</table>')
 
         # now add in the filter/columns/group/etc config table form
@@ -655,9 +902,11 @@ class IndexTemplate(TemplateFunctions):
             show_customization )
         w('<table width=100% border=0 cellspacing=0 cellpadding=2>\n')
         names = []
-        for name in self.properties.keys():
-            if name in all_filters or name in all_columns:
+        seen = {}
+        for name in all_filters + all_columns:
+            if self.properties.has_key(name) and not seen.has_key(name):
                 names.append(name)
+            seen[name] = 1
         if show_customization:
             action = '-'
         else:
@@ -673,9 +922,9 @@ class IndexTemplate(TemplateFunctions):
                     w('<input type="hidden" name=":group" value="%s">' % name)
 
         # TODO: The widget style can go into the stylesheet
-        w('<th align="left" colspan=%s>'
+        w(_('<th align="left" colspan=%s>'
           '<input style="height : 1em; width : 1em; font-size: 12pt" type="submit" name="action" value="%s">&nbsp;View '
-          'customisation...</th></tr>\n'%(len(names)+1, action))
+          'customisation...</th></tr>\n')%(len(names)+1, action))
 
         if not show_customization:
             w('</table>\n')
@@ -688,8 +937,7 @@ class IndexTemplate(TemplateFunctions):
 
         # Filter
         if all_filters:
-            w('<tr><th width="1%" align=right class="location-bar">'
-              'Filters</th>\n')
+            w(_('<tr><th width="1%" align=right class="location-bar">Filters</th>\n'))
             for name in names:
                 if name not in all_filters:
                     w('<td>&nbsp;</td>')
@@ -703,8 +951,7 @@ class IndexTemplate(TemplateFunctions):
 
         # Columns
         if all_columns:
-            w('<tr><th width="1%" align=right class="location-bar">'
-              'Columns</th>\n')
+            w(_('<tr><th width="1%" align=right class="location-bar">Columns</th>\n'))
             for name in names:
                 if name not in all_columns:
                     w('<td>&nbsp;</td>')
@@ -717,10 +964,8 @@ class IndexTemplate(TemplateFunctions):
             w('</tr>\n')
 
             # Grouping
-            w('<tr><th width="1%" align=right class="location-bar">'
-              'Grouping</th>\n')
+            w(_('<tr><th width="1%" align=right class="location-bar">Grouping</th>\n'))
             for name in names:
-                prop = self.properties[name]
                 if name not in all_columns:
                     w('<td>&nbsp;</td>')
                     continue
@@ -733,7 +978,7 @@ class IndexTemplate(TemplateFunctions):
 
         w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
         w('<td colspan="%s">'%len(names))
-        w('<input type="submit" name="action" value="Redisplay"></td>')
+        w(_('<input type="submit" name="action" value="Redisplay"></td>'))
         w('</tr>\n')
         w('</table>\n')
 
@@ -781,6 +1026,8 @@ class IndexTemplate(TemplateFunctions):
 #   ITEM TEMPLATES
 #
 class ItemTemplateReplace:
+    '''Regular-expression based parser that turns the template into HTML. 
+    '''
     def __init__(self, globals, locals, cl, nodeid):
         self.globals = globals
         self.locals = locals
@@ -804,12 +1051,16 @@ class ItemTemplateReplace:
         if m.group('display'):
             command = m.group('command')
             return eval(command, self.globals, self.locals)
-        print '*** unhandled match', m.groupdict()
+        return '*** unhandled match: %s'%str(m.groupdict())
 
 
 class ItemTemplate(TemplateFunctions):
+    '''Templating functionality specifically for item (node) display
+    '''
     def __init__(self, client, templates, classname):
+        TemplateFunctions.__init__(self)
         self.client = client
+        self.instance = client.instance
         self.templates = templates
         self.classname = classname
 
@@ -818,8 +1069,6 @@ class ItemTemplate(TemplateFunctions):
         self.cl = self.db.classes[self.classname]
         self.properties = self.cl.getprops()
 
-        TemplateFunctions.__init__(self)
-
     def render(self, nodeid):
         self.nodeid = nodeid
 
@@ -831,7 +1080,7 @@ class ItemTemplate(TemplateFunctions):
             #  designators...
 
         w = self.client.write
-        w('<form action="%s%s" method="POST" enctype="multipart/form-data">'%(
+        w('<form onSubmit="return submit_once()" action="%s%s" method="POST" enctype="multipart/form-data">'%(
             self.classname, nodeid))
         s = open(os.path.join(self.templates, self.classname+'.item')).read()
         replace = ItemTemplateReplace(self.globals, locals(), self.cl, nodeid)
@@ -840,8 +1089,12 @@ class ItemTemplate(TemplateFunctions):
 
 
 class NewItemTemplate(TemplateFunctions):
+    '''Templating functionality specifically for NEW item (node) display
+    '''
     def __init__(self, client, templates, classname):
+        TemplateFunctions.__init__(self)
         self.client = client
+        self.instance = client.instance
         self.templates = templates
         self.classname = classname
 
@@ -850,8 +1103,6 @@ class NewItemTemplate(TemplateFunctions):
         self.cl = self.db.classes[self.classname]
         self.properties = self.cl.getprops()
 
-        TemplateFunctions.__init__(self)
-
     def render(self, form):
         self.form = form
         w = self.client.write
@@ -860,7 +1111,7 @@ class NewItemTemplate(TemplateFunctions):
             s = open(os.path.join(self.templates, c+'.newitem')).read()
         except IOError:
             s = open(os.path.join(self.templates, c+'.item')).read()
-        w('<form action="new%s" method="POST" enctype="multipart/form-data">'%c)
+        w('<form onSubmit="return submit_once()" action="new%s" method="POST" enctype="multipart/form-data">'%c)
         for key in form.keys():
             if key[0] == ':':
                 value = form[key].value
@@ -873,6 +1124,195 @@ class NewItemTemplate(TemplateFunctions):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.88  2002/04/24 08:34:35  rochecompaan
+# Sorting was applied to all nodes of the MultiLink class instead of
+# the nodes that are actually linked to in the "field" template
+# function.  This adds about 20+ seconds in the display of an issue if
+# your database has a 1000 or more issue in it.
+#
+# Revision 1.87  2002/04/03 06:12:46  richard
+# Fix for date properties as labels.
+#
+# Revision 1.86  2002/04/03 05:54:31  richard
+# Fixed serialisation problem by moving the serialisation step out of the
+# hyperdb.Class (get, set) into the hyperdb.Database.
+#
+# Also fixed htmltemplate after the showid changes I made yesterday.
+#
+# Unit tests for all of the above written.
+#
+# Revision 1.85  2002/04/02 01:40:58  richard
+#  . link() htmltemplate function now has a "showid" option for links and
+#    multilinks. When true, it only displays the linked node id as the anchor
+#    text. The link value is displayed as a tooltip using the title anchor
+#    attribute.
+#
+# Revision 1.84  2002/03/29 19:41:48  rochecompaan
+#  . Fixed display of mutlilink properties when using the template
+#    functions, menu and plain.
+#
+# Revision 1.83  2002/02/27 04:14:31  richard
+# Ran it through pychecker, made fixes
+#
+# Revision 1.82  2002/02/21 23:11:45  richard
+#  . fixed some problems in date calculations (calendar.py doesn't handle over-
+#    and under-flow). Also, hour/minute/second intervals may now be more than
+#    99 each.
+#
+# Revision 1.81  2002/02/21 07:21:38  richard
+# docco
+#
+# Revision 1.80  2002/02/21 07:19:08  richard
+# ... and label, width and height control for extra flavour!
+#
+# Revision 1.79  2002/02/21 06:57:38  richard
+#  . Added popup help for classes using the classhelp html template function.
+#    - add <display call="classhelp('priority', 'id,name,description')">
+#      to an item page, and it generates a link to a popup window which displays
+#      the id, name and description for the priority class. The description
+#      field won't exist in most installations, but it will be added to the
+#      default templates.
+#
+# Revision 1.78  2002/02/21 06:23:00  richard
+# *** empty log message ***
+#
+# Revision 1.77  2002/02/20 05:05:29  richard
+#  . Added simple editing for classes that don't define a templated interface.
+#    - access using the admin "class list" interface
+#    - limited to admin-only
+#    - requires the csv module from object-craft (url given if it's missing)
+#
+# Revision 1.76  2002/02/16 09:10:52  richard
+# oops
+#
+# Revision 1.75  2002/02/16 08:43:23  richard
+#  . #517906 ] Attribute order in "View customisation"
+#
+# Revision 1.74  2002/02/16 08:39:42  richard
+#  . #516854 ] "My Issues" and redisplay
+#
+# Revision 1.73  2002/02/15 07:08:44  richard
+#  . Alternate email addresses are now available for users. See the MIGRATION
+#    file for info on how to activate the feature.
+#
+# Revision 1.72  2002/02/14 23:39:18  richard
+# . All forms now have "double-submit" protection when Javascript is enabled
+#   on the client-side.
+#
+# Revision 1.71  2002/01/23 06:15:24  richard
+# real (non-string, duh) sorting of lists by node id
+#
+# Revision 1.70  2002/01/23 05:47:57  richard
+# more HTML template cleanup and unit tests
+#
+# Revision 1.69  2002/01/23 05:10:27  richard
+# More HTML template cleanup and unit tests.
+#  - download() now implemented correctly, replacing link(is_download=1) [fixed in the
+#    templates, but link(is_download=1) will still work for existing templates]
+#
+# Revision 1.68  2002/01/22 22:55:28  richard
+#  . htmltemplate list() wasn't sorting...
+#
+# Revision 1.67  2002/01/22 22:46:22  richard
+# more htmltemplate cleanups and unit tests
+#
+# Revision 1.66  2002/01/22 06:35:40  richard
+# more htmltemplate tests and cleanup
+#
+# Revision 1.65  2002/01/22 00:12:06  richard
+# Wrote more unit tests for htmltemplate, and while I was at it, I polished
+# off the implementation of some of the functions so they behave sanely.
+#
+# Revision 1.64  2002/01/21 03:25:59  richard
+# oops
+#
+# Revision 1.63  2002/01/21 02:59:10  richard
+# Fixed up the HTML display of history so valid links are actually displayed.
+# Oh for some unit tests! :(
+#
+# Revision 1.62  2002/01/18 08:36:12  grubert
+#  . add nowrap to history table date cell i.e. <td nowrap ...
+#
+# Revision 1.61  2002/01/17 23:04:53  richard
+#  . much nicer history display (actualy real handling of property types etc)
+#
+# Revision 1.60  2002/01/17 08:48:19  grubert
+#  . display superseder as html link in history.
+#
+# Revision 1.59  2002/01/17 07:58:24  grubert
+#  . display links a html link in history.
+#
+# Revision 1.58  2002/01/15 00:50:03  richard
+# #502949 ] index view for non-issues and redisplay
+#
+# Revision 1.57  2002/01/14 23:31:21  richard
+# reverted the change that had plain() hyperlinking the link displays -
+# that's what link() is for!
+#
+# Revision 1.56  2002/01/14 07:04:36  richard
+#  . plain rendering of links in the htmltemplate now generate a hyperlink to
+#    the linked node's page.
+#    ... this allows a display very similar to bugzilla's where you can actually
+#    find out information about the linked node.
+#
+# Revision 1.55  2002/01/14 06:45:03  richard
+#  . #502953 ] nosy-like treatment of other multilinks
+#    ... had to revert most of the previous change to the multilink field
+#    display... not good.
+#
+# Revision 1.54  2002/01/14 05:16:51  richard
+# The submit buttons need a name attribute or mozilla won't submit without a
+# file upload. Yeah, that's bloody obscure. Grr.
+#
+# Revision 1.53  2002/01/14 04:03:32  richard
+# How about that ... date fields have never worked ...
+#
+# Revision 1.52  2002/01/14 02:20:14  richard
+#  . changed all config accesses so they access either the instance or the
+#    config attriubute on the db. This means that all config is obtained from
+#    instance_config instead of the mish-mash of classes. This will make
+#    switching to a ConfigParser setup easier too, I hope.
+#
+# At a minimum, this makes migration a _little_ easier (a lot easier in the
+# 0.5.0 switch, I hope!)
+#
+# Revision 1.51  2002/01/10 10:02:15  grubert
+# In do_history: replace "." in date by " " so html wraps more sensible.
+# Should this be done in date's string converter ?
+#
+# Revision 1.50  2002/01/05 02:35:10  richard
+# I18N'ification
+#
+# Revision 1.49  2001/12/20 15:43:01  rochecompaan
+# Features added:
+#  .  Multilink properties are now displayed as comma separated values in
+#     a textbox
+#  .  The add user link is now only visible to the admin user
+#  .  Modified the mail gateway to reject submissions from unknown
+#     addresses if ANONYMOUS_ACCESS is denied
+#
+# Revision 1.48  2001/12/20 06:13:24  rochecompaan
+# Bugs fixed:
+#   . Exception handling in hyperdb for strings-that-look-like numbers got
+#     lost somewhere
+#   . Internet Explorer submits full path for filename - we now strip away
+#     the path
+# Features added:
+#   . Link and multilink properties are now displayed sorted in the cgi
+#     interface
+#
+# Revision 1.47  2001/11/26 22:55:56  richard
+# Feature:
+#  . Added INSTANCE_NAME to configuration - used in web and email to identify
+#    the instance.
+#  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
+#    signature info in e-mails.
+#  . Some more flexibility in the mail gateway and more error handling.
+#  . Login now takes you to the page you back to the were denied access to.
+#
+# Fixed:
+#  . Lots of bugs, thanks Roché and others on the devel mailing list!
+#
 # Revision 1.46  2001/11/24 00:53:12  jhermann
 # "except:" is bad, bad , bad!
 #