Code

forgot to fix the templating for last change
[roundup.git] / roundup / htmltemplate.py
index 75af618ca1c79673e5ef832554dce1b3f90d26c0..2f6557a1fdb0abba2b3e4d660048c9cfdc2e923b 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: htmltemplate.py,v 1.73 2002-02-15 07:08:44 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
@@ -33,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
@@ -43,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;
 
@@ -58,7 +75,7 @@ class TemplateFunctions:
         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:
@@ -93,9 +110,12 @@ class TemplateFunctions:
         elif isinstance(propclass, hyperdb.Multilink):
             linkcl = self.db.classes[propclass.classname]
             k = linkcl.labelprop()
-            value = ', '.join(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
@@ -203,9 +223,8 @@ class TemplateFunctions:
         elif isinstance(propclass, hyperdb.Multilink):
             sortfunc = self.make_sort_function(propclass.classname)
             linkcl = self.db.classes[propclass.classname]
-            list = linkcl.list()
-            list.sort(sortfunc)
-            l = []
+            if value:
+                value.sort(sortfunc)
             # map the id to the label property
             if not showid:
                 k = linkcl.labelprop()
@@ -257,20 +276,17 @@ class TemplateFunctions:
         value = self.determine_value(property)
 
         # display
-        if isinstance(propclass, hyperdb.Link):
+        if isinstance(propclass, hyperdb.Multilink):
             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)
             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 options:
                 option = linkcl.get(optionid, k)
                 s = ''
-                if optionid == value:
+                if optionid in value or option in value:
                     s = 'selected '
                 if showid:
                     lab = '%s%s: %s'%(propclass.classname, optionid, option)
@@ -279,20 +295,27 @@ class TemplateFunctions:
                 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('<option %svalue="%s">%s</option>'%(s, optionid,
+                    lab))
             l.append('</select>')
             return '\n'.join(l)
-        if isinstance(propclass, hyperdb.Multilink):
+        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)
             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 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)
@@ -301,14 +324,13 @@ class TemplateFunctions:
                 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('<option %svalue="%s">%s</option>'%(s, optionid, lab))
             l.append('</select>')
             return '\n'.join(l)
         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
@@ -331,25 +353,39 @@ class TemplateFunctions:
             linkname = propclass.classname
             linkcl = self.db.classes[linkname]
             k = linkcl.labelprop()
-            linkvalue = cgi.escape(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()
             l = []
             for value in value:
-                linkvalue = cgi.escape(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 is_download:
             return '<a href="%s%s/%s">%s</a>'%(self.classname, self.nodeid,
@@ -394,14 +430,11 @@ class TemplateFunctions:
             return ''
 
         # figure the interval
-        interval = value - date.Date('.')
+        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 interval.pretty()
         return str(interval)
 
     def do_download(self, property, **args):
@@ -441,7 +474,7 @@ class TemplateFunctions:
         l = []
         k = linkcl.labelprop()
         for optionid in linkcl.list():
-            option = cgi.escape(linkcl.get(optionid, k))
+            option = cgi.escape(str(linkcl.get(optionid, k)))
             if optionid in value or option in value:
                 checked = 'checked'
             else:
@@ -532,7 +565,7 @@ class TemplateFunctions:
                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
                         linkcl, linkid, key)
                 else:
-                    arg_s = str(arg)
+                    arg_s = str(args)
 
             elif action == 'unlink' and type(args) == type(()):
                 if len(args) == 3:
@@ -540,7 +573,7 @@ class TemplateFunctions:
                     arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
                         linkcl, linkid, key)
                 else:
-                    arg_s = str(arg)
+                    arg_s = str(args)
 
             elif type(args) == type({}):
                 cell = []
@@ -558,7 +591,7 @@ class TemplateFunctions:
                             classname = prop.classname
                             try:
                                 linkcl = self.db.classes[classname]
-                            except KeyError, message:
+                            except KeyError:
                                 labelprop = None
                                 comments[classname] = _('''The linked class
                                     %(classname)s no longer exists''')%locals()
@@ -649,11 +682,27 @@ class TemplateFunctions:
         else:
             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
@@ -670,16 +719,19 @@ 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
@@ -690,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):
@@ -711,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 = []
@@ -768,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:
@@ -847,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:
@@ -909,7 +966,6 @@ class IndexTemplate(TemplateFunctions):
             # Grouping
             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
@@ -970,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
@@ -993,11 +1051,14 @@ 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
@@ -1008,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
 
@@ -1030,7 +1089,10 @@ 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
@@ -1041,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
@@ -1064,6 +1124,77 @@ 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.