Code

- replaced the content() callback ickiness with Page Template macro usage
[roundup.git] / roundup / cgi / templating.py
index 8f63d3a8576c24897cb600d89e3cd194f82144f8..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,15 +136,18 @@ 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:
-            c['context'] = HTMLItem(client, classname, client.nodeid)
-        else:
+            if classname == 'user':
+                c['context'] = HTMLUser(client, classname, client.nodeid)
+            else:
+                c['context'] = HTMLItem(client, classname, client.nodeid)
+        elif client.db.classes.has_key(classname):
             c['context'] = HTMLClass(client, classname)
         return c
 
@@ -170,7 +161,7 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
 
         if self._v_errors:
             raise PageTemplate.PTRuntimeError, \
-                'Page Template %s has errors.' % self.id
+                'Page Template %s has errors.'%self.id
 
         # figure the context
         classname = classname or client.classname
@@ -180,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()
 
@@ -218,18 +209,36 @@ def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
             l.append(cl.lookup(entry))
     return l
 
-class HTMLClass:
+class HTMLPermissions:
+    ''' Helpers that provide answers to commonly asked Permission questions.
+    '''
+    def is_edit_ok(self):
+        ''' Is the user allowed to Edit the current class?
+        '''
+        return self._db.security.hasPermission('Edit', self._client.userid,
+            self._classname)
+    def is_view_ok(self):
+        ''' Is the user allowed to View the current class?
+        '''
+        return self._db.security.hasPermission('View', self._client.userid,
+            self._classname)
+    def is_only_view_ok(self):
+        ''' Is the user only allowed to View (ie. not Edit) the current class?
+        '''
+        return self.is_view_ok() and not self.is_edit_ok()
+
+class HTMLClass(HTMLPermissions):
     ''' Accesses through a class (either through *class* or *db.<classname>*)
     '''
     def __init__(self, client, classname):
         self._client = client
         self._db = client.db
 
-        # we want classname to be exposed
-        self.classname = classname
-        if classname is not None:
-            self._klass = self._db.getclass(self.classname)
-            self._props = self._klass.getprops()
+        # we want classname to be exposed, but _classname gives a
+        # consistent API for extending Class/Item
+        self._classname = self.classname = classname
+        self._klass = self._db.getclass(self.classname)
+        self._props = self._klass.getprops()
 
     def __repr__(self):
         return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
@@ -280,6 +289,20 @@ class HTMLClass:
         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.
         '''
@@ -301,7 +324,13 @@ class HTMLClass:
             klass = HTMLUser
         else:
             klass = HTMLItem
-        l = [klass(self._client, self.classname, x) for x in self._klass.list()]
+
+        # get the list and sort it nicely
+        l = self._klass.list()
+        sortfunc = make_sort_function(self._db, self.classname)
+        l.sort(sortfunc)
+
+        l = [klass(self._client, self.classname, x) for x in l]
         return l
 
     def csv(self):
@@ -373,8 +402,8 @@ class HTMLClass:
             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)
@@ -394,12 +423,12 @@ class HTMLClass:
         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)
 
-class HTMLItem:
+class HTMLItem(HTMLPermissions):
     ''' Accesses through an *item*
     '''
     def __init__(self, client, classname, nodeid):
@@ -417,7 +446,7 @@ class HTMLItem:
     def __getitem__(self, item):
         ''' return an HTMLProperty instance
         '''
-       #print 'HTMLItem.getitem', (self, item)
+        #print 'HTMLItem.getitem', (self, item)
         if item == 'id':
             return self._nodeid
 
@@ -627,6 +656,7 @@ class HTMLUser(HTMLItem):
 
         # used for security checks
         self._security = client.db.security
+
     _marker = []
     def hasPermission(self, role, classname=_marker):
         ''' Determine if the user has the Role.
@@ -638,6 +668,20 @@ class HTMLUser(HTMLItem):
             classname = self._default_classname
         return self._security.hasPermission(role, self._nodeid, classname)
 
+    def is_edit_ok(self):
+        ''' Is the user allowed to Edit the current class?
+            Also check whether this is the current user's info.
+        '''
+        return self._db.security.hasPermission('Edit', self._client.userid,
+            self._classname) or self._nodeid == self._client.userid
+
+    def is_view_ok(self):
+        ''' Is the user allowed to View the current class?
+            Also check whether this is the current user's info.
+        '''
+        return self._db.security.hasPermission('Edit', self._client.userid,
+            self._classname) or self._nodeid == self._client.userid
+
 class HTMLProperty:
     ''' String, Number, Date, Interval HTMLProperty
 
@@ -730,10 +774,18 @@ class PasswordHTMLProperty(HTMLProperty):
         return _('*encrypted*')
 
     def field(self, size = 30):
-        ''' Render a form edit field for the property
+        ''' Render a form edit field for the property.
         '''
         return '<input type="password" name="%s" size="%s">'%(self._name, size)
 
+    def confirm(self, size = 30):
+        ''' Render a second form edit field for the property, used for 
+            confirmation that the user typed the password correctly. Generates
+            a field with name "name:confirm".
+        '''
+        return '<input type="password" name="%s:confirm" size="%s">'%(
+            self._name, size)
+
 class NumberHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
@@ -879,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>')
@@ -903,9 +964,6 @@ class LinkHTMLProperty(HTMLProperty):
         # sort function
         sortfunc = make_sort_function(self._db, self._prop.classname)
 
-        # force the value to be a single choice
-        if isinstance(value, type('')):
-            value = value[0]
         linkcl = self._db.getclass(self._prop.classname)
         l = ['<select name="%s">'%self._name]
         k = linkcl.labelprop(1)
@@ -919,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:
@@ -934,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>')
@@ -1030,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:
@@ -1045,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))
@@ -1102,7 +1177,6 @@ class HTMLRequest:
 
         "form" the CGI form as a cgi.FieldStorage
         "env" the CGI environment variables
-        "url" the current URL path for this request
         "base" the base URL for this instance
         "user" a HTMLUser instance for this user
         "classname" the current classname (possibly None)
@@ -1126,7 +1200,6 @@ class HTMLRequest:
         self.form = client.form
         self.env = client.env
         self.base = client.base
-        self.url = client.url
         self.user = HTMLUser(client, 'user', client.userid)
 
         # store the current class name and action
@@ -1171,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
 
@@ -1253,7 +1328,6 @@ class HTMLRequest:
         d['env'] = e
         return '''
 form: %(form)s
-url: %(url)r
 base: %(base)r
 classname: %(classname)r
 template: %(template)r
@@ -1342,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
@@ -1363,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
@@ -1382,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.
@@ -1399,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)
@@ -1422,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