Code

fixed bug in args to new DateHTMLProperty in the local() method (sf bug 901444)
[roundup.git] / roundup / cgi / templating.py
index 49242ef84b0729b2d2dac3ac5bd6683b0de33cc4..3fa5b0154ba9280c3adb11efbbfbecea8855e7c6 100644 (file)
@@ -1,3 +1,7 @@
+"""Implements the API used in the HTML templating for the web interface.
+"""
+__docformat__ = 'restructuredtext'
+
 from __future__ import nested_scopes
 
 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
@@ -143,35 +147,37 @@ class Templates:
             raise KeyError, message
 
 class RoundupPageTemplate(PageTemplate.PageTemplate):
-    ''' A Roundup-specific PageTemplate.
-
-        Interrogate the client to set up the various template variables to
-        be available:
-
-        *context*
-         this is one of three things:
-         1. None - we're viewing a "home" page
-         2. The current class of item being displayed. This is an HTMLClass
-            instance.
-         3. The current item from the database, if we're viewing a specific
-            item, as an HTMLItem instance.
-        *request*
-          Includes information about the current request, including:
-           - the url
-           - the current index information (``filterspec``, ``filter`` args,
-             ``properties``, etc) parsed out of the form. 
-           - methods for easy filterspec link generation
-           - *user*, the current user node as an HTMLItem instance
-           - *form*, the current CGI form information as a FieldStorage
-        *config*
-          The current tracker config.
-        *db*
-          The current database, used to access arbitrary database items.
-        *utils*
-          This is a special class that has its base in the TemplatingUtils
-          class in this file. If the tracker interfaces module defines a
-          TemplatingUtils class then it is mixed in, overriding the methods
-          in the base class.
+    '''A Roundup-specific PageTemplate.
+
+    Interrogate the client to set up the various template variables to
+    be available:
+
+    *context*
+     this is one of three things:
+
+     1. None - we're viewing a "home" page
+     2. The current class of item being displayed. This is an HTMLClass
+        instance.
+     3. The current item from the database, if we're viewing a specific
+        item, as an HTMLItem instance.
+    *request*
+      Includes information about the current request, including:
+
+       - the url
+       - the current index information (``filterspec``, ``filter`` args,
+         ``properties``, etc) parsed out of the form. 
+       - methods for easy filterspec link generation
+       - *user*, the current user node as an HTMLItem instance
+       - *form*, the current CGI form information as a FieldStorage
+    *config*
+      The current tracker config.
+    *db*
+      The current database, used to access arbitrary database items.
+    *utils*
+      This is a special class that has its base in the TemplatingUtils
+      class in this file. If the tracker interfaces module defines a
+      TemplatingUtils class then it is mixed in, overriding the methods
+      in the base class.
     '''
     def getContext(self, client, classname, request):
         # construct the TemplatingUtils class
@@ -199,7 +205,10 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
                 c['context'] = HTMLItem(client, classname, client.nodeid,
                     anonymous=1)
         elif client.db.classes.has_key(classname):
-            c['context'] = HTMLClass(client, classname, anonymous=1)
+            if classname == 'user':
+                c['context'] = HTMLUserClass(client, classname, anonymous=1)
+            else:
+                c['context'] = HTMLClass(client, classname, anonymous=1)
         return c
 
     def render(self, client, classname, request, **options):
@@ -247,6 +256,8 @@ class HTMLDatabase:
             return HTMLItem(self._client, m.group('cl'), m.group('id'))
         else:
             self._client.db.getclass(item)
+            if item == 'user':
+                return HTMLUserClass(self._client, item)
             return HTMLClass(self._client, item)
 
     def __getattr__(self, attr):
@@ -258,7 +269,12 @@ class HTMLDatabase:
     def classes(self):
         l = self._client.db.classes.keys()
         l.sort()
-        return [HTMLClass(self._client, cn) for cn in l]
+        r = []
+        for item in l:
+            if item == 'user':
+                m.append(HTMLUserClass(self._client, item))
+            m.append(HTMLClass(self._client, item))
+        return r
 
 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
     cl = db.getclass(prop.classname)
@@ -401,7 +417,7 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
         ''' Get an item of this class by its item id.
         '''
         # make sure we're looking at an itemid
-        if not num_re.match(itemid):
+        if not isinstance(itemid, type(1)) and not num_re.match(itemid):
             itemid = self._klass.lookup(itemid)
 
         if self.classname == 'user':
@@ -606,14 +622,17 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
             raise AttributeError, attr
 
     def designator(self):
-        ''' Return this item's designator (classname + id) '''
+        """Return this item's designator (classname + id)."""
         return '%s%s'%(self._classname, self._nodeid)
     
     def submit(self, label="Submit Changes"):
-        ''' Generate a submit button (and action hidden element)
-        '''
-        return self.input(type="hidden",name="@action",value="edit") + '\n' + \
-               self.input(type="submit",name="submit",value=label)
+        """Generate a submit button.
+
+        Also sneak in the lastactivity and action hidden elements.
+        """
+        return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
+               self.input(type="hidden", name="@action", value="edit") + '\n' + \
+               self.input(type="submit", name="submit", value=label)
 
     def journal(self, direction='descending'):
         ''' Return a list of HTMLJournalEntry instances.
@@ -841,7 +860,44 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
         # use our fabricated request
         return pt.render(self._client, req.classname, req)
 
-class HTMLUser(HTMLItem):
+class HTMLUserPermission:
+
+    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._user_perm_check('Edit')
+
+    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._user_perm_check('View')
+
+    def _user_perm_check(self, type):
+        # some users may view / edit all users
+        s = self._db.security
+        userid = self._client.userid
+        if s.hasPermission(type, userid, self._classname):
+            return 1
+
+        # users may view their own info
+        is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
+        if getattr(self, '_nodeid', None) == userid and not is_anonymous:
+            return 1
+
+        # may anonymous users register?
+        if (is_anonymous and s.hasPermission('Web Registration', userid,
+                self._classname)):
+            return 1
+
+        # nope, no access here
+        return 0
+
+class HTMLUserClass(HTMLUserPermission, HTMLClass):
+    pass
+
+class HTMLUser(HTMLUserPermission, HTMLItem):
     ''' Accesses through the *user* (a special case of item)
     '''
     def __init__(self, client, classname, nodeid, anonymous=0):
@@ -862,22 +918,6 @@ class HTMLUser(HTMLItem):
             classname = self._default_classname
         return self._security.hasPermission(permission, 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 and
-            self._db.user.get(self._client.userid, 'username') != 'anonymous')
-
-    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 and
-            self._db.user.get(self._client.userid, 'username') != 'anonymous')
-
 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
     ''' String, Number, Date, Interval HTMLProperty
 
@@ -915,6 +955,26 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions):
             return cmp(self._value, other._value)
         return cmp(self._value, other)
 
+    def is_edit_ok(self):
+        ''' Is the user allowed to Edit the current class?
+        '''
+        thing = HTMLDatabase(self._client)[self._classname]
+        if self._nodeid:
+            # this is a special-case for the User class where permission's
+            # on a per-item basis :(
+            thing = thing.getItem(self._nodeid)
+        return thing.is_edit_ok()
+
+    def is_view_ok(self):
+        ''' Is the user allowed to View the current class?
+        '''
+        thing = HTMLDatabase(self._client)[self._classname]
+        if self._nodeid:
+            # this is a special-case for the User class where permission's
+            # on a per-item basis :(
+            thing = thing.getItem(self._nodeid)
+        return thing.is_view_ok()
+
 class StringHTMLProperty(HTMLProperty):
     hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
                           r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
@@ -932,8 +992,10 @@ class StringHTMLProperty(HTMLProperty):
             s2 = match.group('id')
             try:
                 # make sure s1 is a valid tracker classname
-                self._db.getclass(s1)
-                return '<a href="%s">%s %s</a>'%(s, s1, s2)
+                cl = self._db.getclass(s1)
+                if not cl.hasnode(s2):
+                    raise KeyError, 'oops'
+                return '<a href="%s">%s%s</a>'%(s, s1, s2)
             except KeyError:
                 return '%s%s'%(s1, s2)
 
@@ -942,11 +1004,11 @@ class StringHTMLProperty(HTMLProperty):
         return self.plain(hyperlink=1)
 
     def plain(self, escape=0, hyperlink=0):
-        ''' Render a "plain" representation of the property
+        '''Render a "plain" representation of the property
             
-            "escape" turns on/off HTML quoting
-            "hyperlink" turns on/off in-text hyperlinking of URLs, email
-                addresses and designators
+        - "escape" turns on/off HTML quoting
+        - "hyperlink" turns on/off in-text hyperlinking of URLs, email
+          addresses and designators
         '''
         self.view_check()
 
@@ -1218,8 +1280,8 @@ class DateHTMLProperty(HTMLProperty):
         '''
         self.view_check()
 
-        return DateHTMLProperty(self._client, self._nodeid, self._prop,
-            self._formname, self._value.local(offset))
+        return DateHTMLProperty(self._client, self._classname, self._nodeid,
+            self._prop, self._formname, self._value.local(offset))
 
 class IntervalHTMLProperty(HTMLProperty):
     def plain(self):
@@ -1421,7 +1483,7 @@ class MultilinkHTMLProperty(HTMLProperty):
 
     def __contains__(self, value):
         ''' Support the "in" operator. We have to make sure the passed-in
-            value is a string first, not a *HTMLProperty.
+            value is a string first, not a HTMLProperty.
         '''
         return str(value) in self._value
 
@@ -1581,25 +1643,25 @@ class ShowDict:
         return self.columns.has_key(name)
 
 class HTMLRequest(HTMLInputMixin):
-    ''' The *request*, holding the CGI form and environment.
-
-        "form" the CGI form as a cgi.FieldStorage
-        "env" the CGI environment variables
-        "base" the base URL for this instance
-        "user" a HTMLUser instance for this user
-        "classname" the current classname (possibly None)
-        "template" the current template (suffix, also possibly None)
-
-        Index args:
-        "columns" dictionary of the columns to display in an index page
-        "show" a convenience access to columns - request/show/colname will
-               be true if the columns should be displayed, false otherwise
-        "sort" index sort column (direction, column name)
-        "group" index grouping property (direction, column name)
-        "filter" properties to filter the index on
-        "filterspec" values to filter the index on
-        "search_text" text to perform a full-text search on for an index
-
+    '''The *request*, holding the CGI form and environment.
+
+    - "form" the CGI form as a cgi.FieldStorage
+    - "env" the CGI environment variables
+    - "base" the base URL for this instance
+    - "user" a HTMLUser instance for this user
+    - "classname" the current classname (possibly None)
+    - "template" the current template (suffix, also possibly None)
+
+    Index args:
+
+    - "columns" dictionary of the columns to display in an index page
+    - "show" a convenience access to columns - request/show/colname will
+      be true if the columns should be displayed, false otherwise
+    - "sort" index sort column (direction, column name)
+    - "group" index grouping property (direction, column name)
+    - "filter" properties to filter the index on
+    - "filterspec" values to filter the index on
+    - "search_text" text to perform a full-text search on for an index
     '''
     def __init__(self, client):
         # _client is needed by HTMLInputMixin