Code

- replaced the content() callback ickiness with Page Template macro usage
[roundup.git] / roundup / cgi / templating.py
index 2f2a478f14e570dbe49dc0cae1c0b21f67dfb6a5..a9757fa35bba8716ded4b9cc4b0f9420b3151911 100644 (file)
@@ -22,100 +22,88 @@ from roundup.cgi.PageTemplates.Expressions import getEngine
 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
 from roundup.cgi import ZTUtils
 
-# XXX WAH pagetemplates aren't pickleable :(
-#def getTemplate(dir, name, classname=None, request=None):
-#    ''' Interface to get a template, possibly loading a compiled template.
-#    '''
-#    # source
-#    src = os.path.join(dir, name)
-#
-#    # see if we can get a compile from the template"c" directory (most
-#    # likely is "htmlc"
-#    split = list(os.path.split(dir))
-#    split[-1] = split[-1] + 'c'
-#    cdir = os.path.join(*split)
-#    split.append(name)
-#    cpl = os.path.join(*split)
-#
-#    # ok, now see if the source is newer than the compiled (or if the
-#    # compiled even exists)
-#    MTIME = os.path.stat.ST_MTIME
-#    if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
-#        # nope, we need to compile
-#        pt = RoundupPageTemplate()
-#        pt.write(open(src).read())
-#        pt.id = name
-#
-#        # save off the compiled template
-#        if not os.path.exists(cdir):
-#            os.makedirs(cdir)
-#        f = open(cpl, 'wb')
-#        pickle.dump(pt, f)
-#        f.close()
-#    else:
-#        # yay, use the compiled template
-#        f = open(cpl, 'rb')
-#        pt = pickle.load(f)
-#    return pt
-
-templates = {}
-
 class NoTemplate(Exception):
     pass
 
-def getTemplate(dir, name, extension, classname=None, request=None):
-    ''' Interface to get a template, possibly loading a compiled template.
+class Templates:
+    templates = {}
 
-        "name" and "extension" indicate the template we're after, which in
-        most cases will be "name.extension". If "extension" is None, then
-        we look for a template just called "name" with no extension.
+    def __init__(self, dir):
+        self.dir = dir
 
-        If the file "name.extension" doesn't exist, we look for
-        "_generic.extension" as a fallback.
-    '''
-    # default the name to "home"
-    if name is None:
-        name = 'home'
+    def precompileTemplates(self):
+        ''' Go through a directory and precompile all the templates therein
+        '''
+        for filename in os.listdir(self.dir):
+            if os.path.isdir(filename): continue
+            if '.' in filename:
+                name, extension = filename.split('.')
+                self.getTemplate(name, extension)
+            else:
+                self.getTemplate(filename, None)
 
-    # find the source, figure the time it was last modified
-    if extension:
-        filename = '%s.%s'%(name, extension)
-    else:
-        filename = name
-    src = os.path.join(dir, filename)
-    try:
-        stime = os.stat(src)[os.path.stat.ST_MTIME]
-    except os.error, error:
-        if error.errno != errno.ENOENT:
-            raise
-        if not extension:
-            raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
-
-        # try for a generic template
-        generic = '_generic.%s'%extension
-        src = os.path.join(dir, generic)
+    def get(self, name, extension):
+        ''' Interface to get a template, possibly loading a compiled template.
+
+            "name" and "extension" indicate the template we're after, which in
+            most cases will be "name.extension". If "extension" is None, then
+            we look for a template just called "name" with no extension.
+
+            If the file "name.extension" doesn't exist, we look for
+            "_generic.extension" as a fallback.
+        '''
+        # default the name to "home"
+        if name is None:
+            name = 'home'
+
+        # find the source, figure the time it was last modified
+        if extension:
+            filename = '%s.%s'%(name, extension)
+        else:
+            filename = name
+        src = os.path.join(self.dir, filename)
         try:
             stime = os.stat(src)[os.path.stat.ST_MTIME]
         except os.error, error:
             if error.errno != errno.ENOENT:
                 raise
-            # nicer error
-            raise NoTemplate, 'No template file exists for templating '\
-                '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
-                extension, filename, generic)
-        filename = generic
-
-    key = (dir, filename)
-    if templates.has_key(key) and stime < templates[key].mtime:
-        # compiled template is up to date
-        return templates[key]
-
-    # compile the template
-    templates[key] = pt = RoundupPageTemplate()
-    pt.write(open(src).read())
-    pt.id = filename
-    pt.mtime = time.time()
-    return pt
+            if not extension:
+                raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
+
+            # try for a generic template
+            generic = '_generic.%s'%extension
+            src = os.path.join(self.dir, generic)
+            try:
+                stime = os.stat(src)[os.path.stat.ST_MTIME]
+            except os.error, error:
+                if error.errno != errno.ENOENT:
+                    raise
+                # nicer error
+                raise NoTemplate, 'No template file exists for templating '\
+                    '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
+                    extension, filename, generic)
+            filename = generic
+
+        if self.templates.has_key(filename) and \
+                stime < self.templates[filename].mtime:
+            # compiled template is up to date
+            return self.templates[filename]
+
+        # compile the template
+        self.templates[filename] = pt = RoundupPageTemplate()
+        pt.write(open(src).read())
+        pt.id = filename
+        pt.mtime = time.time()
+        return pt
+
+    def __getitem__(self, name):
+        name, extension = os.path.splitext(name)
+        if extension:
+            extension = extension[1:]
+        try:
+            return self.get(name, extension)
+        except NoTemplate, message:
+            raise KeyError, message
 
 class RoundupPageTemplate(PageTemplate.PageTemplate):
     ''' A Roundup-specific PageTemplate.
@@ -138,8 +126,8 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
            - methods for easy filterspec link generation
            - *user*, the current user node as an HTMLItem instance
            - *form*, the current CGI form information as a FieldStorage
-        *instance*
-          The current instance
+        *tracker*
+          The current tracker
         *db*
           The current database, through which db.config may be reached.
     '''
@@ -148,10 +136,10 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
              'options': {},
              'nothing': None,
              'request': request,
-             'content': client.content,
              'db': HTMLDatabase(client),
-             'instance': client.instance,
+             'tracker': client.instance,
              'utils': TemplatingUtils(client),
+             'templates': Templates(client.instance.config.TEMPLATES),
         }
         # add in the item if there is one
         if client.nodeid:
@@ -159,7 +147,7 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
                 c['context'] = HTMLUser(client, classname, client.nodeid)
             else:
                 c['context'] = HTMLItem(client, classname, client.nodeid)
-        else:
+        elif client.db.classes.has_key(classname):
             c['context'] = HTMLClass(client, classname)
         return c
 
@@ -183,7 +171,7 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
 
         # and go
         output = StringIO.StringIO()
-        TALInterpreter(self._v_program, self._v_macros,
+        TALInterpreter(self._v_program, self.macros,
             getEngine().getContext(c), output, tal=1, strictinsert=0)()
         return output.getvalue()
 
@@ -249,9 +237,8 @@ class HTMLClass(HTMLPermissions):
         # we want classname to be exposed, but _classname gives a
         # consistent API for extending Class/Item
         self._classname = self.classname = classname
-        if classname is not None:
-            self._klass = self._db.getclass(self.classname)
-            self._props = self._klass.getprops()
+        self._klass = self._db.getclass(self.classname)
+        self._props = self._klass.getprops()
 
     def __repr__(self):
         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
@@ -302,6 +289,20 @@ class HTMLClass(HTMLPermissions):
         except KeyError:
             raise AttributeError, attr
 
+    def getItem(self, itemid, num_re=re.compile('\d+')):
+        ''' Get an item of this class by its item id.
+        '''
+        # make sure we're looking at an itemid
+        if not num_re.match(itemid):
+            itemid = self._klass.lookup(itemid)
+
+        if self.classname == 'user':
+            klass = HTMLUser
+        else:
+            klass = HTMLItem
+
+        return klass(self._client, self.classname, itemid)
+
     def properties(self):
         ''' Return HTMLProperty for all of this class' properties.
         '''
@@ -326,7 +327,7 @@ class HTMLClass(HTMLPermissions):
 
         # get the list and sort it nicely
         l = self._klass.list()
-        sortfunc = make_sort_function(self._db, self._prop.classname)
+        sortfunc = make_sort_function(self._db, self.classname)
         l.sort(sortfunc)
 
         l = [klass(self._client, self.classname, x) for x in l]
@@ -401,8 +402,8 @@ class HTMLClass(HTMLPermissions):
             properties.sort()
             properties = ','.join(properties)
         return '<a href="javascript:help_window(\'%s?:template=help&' \
-            ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
-            '(%s)</b></a>'%(self.classname, properties, width, height, label)
+            'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
+            self.classname, properties, width, height, label)
 
     def submit(self, label="Submit New Entry"):
         ''' Generate a submit button (and action hidden element)
@@ -422,7 +423,7 @@ class HTMLClass(HTMLPermissions):
         req.update(kwargs)
 
         # new template, using the specified classname and request
-        pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
+        pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
 
         # use our fabricated request
         return pt.render(self._client, self.classname, req)
@@ -445,7 +446,7 @@ class HTMLItem(HTMLPermissions):
     def __getitem__(self, item):
         ''' return an HTMLProperty instance
         '''
-       #print 'HTMLItem.getitem', (self, item)
+        #print 'HTMLItem.getitem', (self, item)
         if item == 'id':
             return self._nodeid
 
@@ -930,16 +931,25 @@ class LinkHTMLProperty(HTMLProperty):
             s = ''
         l.append(_('<option %svalue="-1">- no selection -</option>')%s)
         for optionid in options:
-            option = linkcl.get(optionid, k)
+            # get the option value, and if it's None use an empty string
+            option = linkcl.get(optionid, k) or ''
+
+            # figure if this option is selected
             s = ''
             if optionid == self._value:
                 s = 'selected '
+
+            # figure the label
             if showid:
                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
+
+            # truncate if it's too long
             if size is not None and len(lab) > size:
                 lab = lab[:size-3] + '...'
+
+            # and generate
             lab = cgi.escape(lab)
             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
         l.append('</select>')
@@ -967,14 +977,21 @@ class LinkHTMLProperty(HTMLProperty):
             sort_on = ('+', linkcl.labelprop())
         options = linkcl.filter(None, conditions, sort_on, (None, None))
         for optionid in options:
-            option = linkcl.get(optionid, k)
+            # get the option value, and if it's None use an empty string
+            option = linkcl.get(optionid, k) or ''
+
+            # figure if this option is selected
             s = ''
             if value in [optionid, option]:
                 s = 'selected '
+
+            # figure the label
             if showid:
                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
+
+            # truncate if it's too long
             if size is not None and len(lab) > size:
                 lab = lab[:size-3] + '...'
             if additional:
@@ -982,6 +999,8 @@ class LinkHTMLProperty(HTMLProperty):
                 for propname in additional:
                     m.append(linkcl.get(optionid, propname))
                 lab = lab + ' (%s)'%', '.join(map(str, m))
+
+            # and generate
             lab = cgi.escape(lab)
             l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
         l.append('</select>')
@@ -1078,14 +1097,20 @@ class MultilinkHTMLProperty(HTMLProperty):
         l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
         k = linkcl.labelprop(1)
         for optionid in options:
-            option = linkcl.get(optionid, k)
+            # get the option value, and if it's None use an empty string
+            option = linkcl.get(optionid, k) or ''
+
+            # figure if this option is selected
             s = ''
             if optionid in value or option in value:
                 s = 'selected '
+
+            # figure the label
             if showid:
                 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
             else:
                 lab = option
+            # truncate if it's too long
             if size is not None and len(lab) > size:
                 lab = lab[:size-3] + '...'
             if additional:
@@ -1093,6 +1118,8 @@ class MultilinkHTMLProperty(HTMLProperty):
                 for propname in additional:
                     m.append(linkcl.get(optionid, propname))
                 lab = lab + ' (%s)'%', '.join(m)
+
+            # and generate
             lab = cgi.escape(lab)
             l.append('<option %svalue="%s">%s</option>'%(s, optionid,
                 lab))
@@ -1217,15 +1244,17 @@ class HTMLRequest:
         if self.form.has_key(':filter'):
             self.filter = handleListCGIValue(self.form[':filter'])
         self.filterspec = {}
+        db = self.client.db
         if self.classname is not None:
-            props = self.client.db.getclass(self.classname).getprops()
+            props = db.getclass(self.classname).getprops()
             for name in self.filter:
                 if self.form.has_key(name):
                     prop = props[name]
                     fv = self.form[name]
                     if (isinstance(prop, hyperdb.Link) or
                             isinstance(prop, hyperdb.Multilink)):
-                        self.filterspec[name] = handleListCGIValue(fv)
+                        self.filterspec[name] = lookupIds(db, prop,
+                            handleListCGIValue(fv))
                     else:
                         self.filterspec[name] = fv.value
 
@@ -1299,7 +1328,6 @@ class HTMLRequest:
         d['env'] = e
         return '''
 form: %(form)s
-url: %(url)r
 base: %(base)r
 classname: %(classname)r
 template: %(template)r
@@ -1388,7 +1416,7 @@ function submit_once() {
 }
 
 function help_window(helpurl, width, height) {
-    HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
+    HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
 }
 </script>
 '''%self.base
@@ -1409,15 +1437,9 @@ function help_window(helpurl, width, height) {
             matches = None
         l = klass.filter(matches, filterspec, sort, group)
 
-        # map the item ids to instances
-        if self.classname == 'user':
-            klass = HTMLUser
-        else:
-            klass = HTMLItem
-        l = [klass(self.client, self.classname, item) for item in l]
-
-        # return the batch object
-        return Batch(self.client, l, self.pagesize, self.startwith)
+        # return the batch object, using IDs only
+        return Batch(self.client, l, self.pagesize, self.startwith,
+            classname=self.classname)
 
 # extend the standard ZTUtils Batch object to remove dependency on
 # Acquisition and add a couple of useful methods
@@ -1428,7 +1450,8 @@ class Batch(ZTUtils.Batch):
         ========= ========================================================
         Parameter  Usage
         ========= ========================================================
-        sequence  a list of HTMLItems
+        sequence  a list of HTMLItems or item ids
+        classname if sequence is a list of ids, this is the class of item
         size      how big to make the sequence.
         start     where to start (0-indexed) in the sequence.
         end       where to end (0-indexed) in the sequence.
@@ -1445,10 +1468,11 @@ class Batch(ZTUtils.Batch):
         "sequence_length" is the length of the original, unbatched, sequence.
     '''
     def __init__(self, client, sequence, size, start, end=0, orphan=0,
-            overlap=0):
+            overlap=0, classname=None):
         self.client = client
         self.last_index = self.last_item = None
         self.current_item = None
+        self.classname = classname
         self.sequence_length = len(sequence)
         ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
             overlap)
@@ -1468,8 +1492,15 @@ class Batch(ZTUtils.Batch):
             self.last_item = self.current_item
             self.last_index = index
 
-        self.current_item = self._sequence[index + self.first]
-        return self.current_item
+        item = self._sequence[index + self.first]
+        if self.classname:
+            # map the item ids to instances
+            if self.classname == 'user':
+                item = HTMLUser(self.client, self.classname, item)
+            else:
+                item = HTMLItem(self.client, self.classname, item)
+        self.current_item = item
+        return item
 
     def propchanged(self, property):
         ''' Detect if the property marked as being the group property