Code

Second patch from issue2550688 -- with some changes:
[roundup.git] / roundup / cgi / templating.py
index f536774b4215b0a7e0e04ff540bc2117310c8226..04cfd76ab6cb3689d59d7ebc108c8d0904f1c3d9 100644 (file)
@@ -27,6 +27,8 @@ from roundup import hyperdb, date, support
 from roundup import i18n
 from roundup.i18n import _
 
+from KeywordsExpr import render_keywords_expression_editor
+
 try:
     import cPickle as pickle
 except ImportError:
@@ -115,9 +117,9 @@ def find_template(dir, name, view):
     if os.path.exists(src):
         return (src, generic)
 
-    raise NoTemplate, 'No template file exists for templating "%s" '\
+    raise NoTemplate('No template file exists for templating "%s" '
         'with template "%s" (neither "%s" nor "%s")'%(name, view,
-        filename, generic)
+        filename, generic))
 
 class Templates:
     templates = {}
@@ -183,12 +185,28 @@ class Templates:
             return self.templates[src]
 
         # compile the template
-        self.templates[src] = pt = RoundupPageTemplate()
+        pt = RoundupPageTemplate()
         # use pt_edit so we can pass the content_type guess too
         content_type = mimetypes.guess_type(filename)[0] or 'text/html'
         pt.pt_edit(open(src).read(), content_type)
         pt.id = filename
         pt.mtime = stime
+        # Add it to the cache.  We cannot do this until the template
+        # is fully initialized, as we could otherwise have a race
+        # condition when running with multiple threads:
+        #
+        # 1. Thread A notices the template is not in the cache,
+        #    adds it, but has not yet set "mtime".
+        #
+        # 2. Thread B notices the template is in the cache, checks
+        #    "mtime" (above) and crashes.
+        #
+        # Since Python dictionary access is atomic, as long as we
+        # insert "pt" only after it is fully initialized, we avoid
+        # this race condition.  It's possible that two separate
+        # threads will both do the work of initializing the template,
+        # but the risk of wasted work is offset by avoiding a lock.
+        self.templates[src] = pt
         return pt
 
     def __getitem__(self, name):
@@ -420,17 +438,19 @@ def _set_input_default_args(dic):
         except KeyError:
             pass
 
+def cgi_escape_attrs(**attrs):
+    return ' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
+        for k,v in attrs.items()])
+
 def input_html4(**attrs):
     """Generate an 'input' (html4) element with given attributes"""
     _set_input_default_args(attrs)
-    return '<input %s>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
-        for k,v in attrs.items()])
+    return '<input %s>'%cgi_escape_attrs(**attrs)
 
 def input_xhtml(**attrs):
     """Generate an 'input' (xhtml) element with given attributes"""
     _set_input_default_args(attrs)
-    return '<input %s/>'%' '.join(['%s="%s"'%(k,cgi.escape(str(v), True))
-        for k,v in attrs.items()])
+    return '<input %s/>'%cgi_escape_attrs(**attrs)
 
 class HTMLInputMixin:
     """ requires a _client property """
@@ -502,20 +522,23 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
     def is_edit_ok(self):
         """ Is the user allowed to Create the current class?
         """
-        return self._db.security.hasPermission('Create', self._client.userid,
-            self._classname)
+        perm = self._db.security.hasPermission
+        return perm('Web Access', self._client.userid) and perm('Create',
+            self._client.userid, self._classname)
 
     def is_retire_ok(self):
         """ Is the user allowed to retire items of the current class?
         """
-        return self._db.security.hasPermission('Retire', self._client.userid,
-            self._classname)
+        perm = self._db.security.hasPermission
+        return perm('Web Access', self._client.userid) and perm('Retire',
+            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)
+        perm = self._db.security.hasPermission
+        return perm('Web Access', self._client.userid) and perm('View',
+            self._client.userid, self._classname)
 
     def is_only_view_ok(self):
         """ Is the user only allowed to View (ie. not Create) the current class?
@@ -544,10 +567,7 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
         for klass, htmlklass in propclasses:
             if not isinstance(prop, klass):
                 continue
-            if isinstance(prop, hyperdb.Multilink):
-                value = []
-            else:
-                value = None
+            value = prop.get_default_value()
             return htmlklass(self._client, self._classname, None, prop, item,
                 value, self._anonymous)
 
@@ -580,13 +600,10 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
         l = []
         for name, prop in self._props.items():
             for klass, htmlklass in propclasses:
-                if isinstance(prop, hyperdb.Multilink):
-                    value = []
-                else:
-                    value = None
                 if isinstance(prop, klass):
+                    value = prop.get_default_value()
                     l.append(htmlklass(self._client, self._classname, '',
-                        prop, name, value, self._anonymous))
+                                       prop, name, value, self._anonymous))
         if sort:
             l.sort(lambda a,b:cmp(a._name, b._name))
         return l
@@ -602,6 +619,8 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
         # check perms
         check = self._client.db.security.hasPermission
         userid = self._client.userid
+        if not check('Web Access', userid):
+            return []
 
         l = [HTMLItem(self._client, self._classname, id) for id in l
             if check('View', userid, self._classname, itemid=id)]
@@ -616,11 +635,14 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
         writer = csv.writer(s)
         writer.writerow(props)
         check = self._client.db.security.hasPermission
+        userid = self._client.userid
+        if not check('Web Access', userid):
+            return ''
         for nodeid in self._klass.list():
             l = []
             for name in props:
                 # check permission to view this property on this item
-                if not check('View', self._client.userid, itemid=nodeid,
+                if not check('View', userid, itemid=nodeid,
                         classname=self._klass.classname, property=name):
                     raise Unauthorised('view', self._klass.classname,
                         translator=self._client.translator)
@@ -647,13 +669,23 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
 
             "request" takes precedence over the other three arguments.
         """
+        security = self._db.security
+        userid = self._client.userid
         if request is not None:
+            # for a request we asume it has already been
+            # security-filtered
             filterspec = request.filterspec
             sort = request.sort
             group = request.group
+        else:
+            cn = self.classname
+            filterspec = security.filterFilterspec(userid, cn, filterspec)
+            sort = security.filterSortspec(userid, cn, sort)
+            group = security.filterSortspec(userid, cn, group)
 
-        check = self._db.security.hasPermission
-        userid = self._client.userid
+        check = security.hasPermission
+        if not check('Web Access', userid):
+            return []
 
         l = [HTMLItem(self._client, self.classname, id)
              for id in self._klass.filter(None, filterspec, sort, group)
@@ -783,20 +815,23 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions):
     def is_edit_ok(self):
         """ Is the user allowed to Edit this item?
         """
-        return self._db.security.hasPermission('Edit', self._client.userid,
-            self._classname, itemid=self._nodeid)
+        perm = self._db.security.hasPermission
+        return perm('Web Access', self._client.userid) and perm('Edit',
+            self._client.userid, self._classname, itemid=self._nodeid)
 
     def is_retire_ok(self):
         """ Is the user allowed to Reture this item?
         """
-        return self._db.security.hasPermission('Retire', self._client.userid,
-            self._classname, itemid=self._nodeid)
+        perm = self._db.security.hasPermission
+        return perm('Web Access', self._client.userid) and perm('Retire',
+            self._client.userid, self._classname, itemid=self._nodeid)
 
     def is_view_ok(self):
         """ Is the user allowed to View this item?
         """
-        if self._db.security.hasPermission('View', self._client.userid,
-                self._classname, itemid=self._nodeid):
+        perm = self._db.security.hasPermission
+        if perm('Web Access', self._client.userid) and perm('View',
+                self._client.userid, self._classname, itemid=self._nodeid):
             return 1
         return self.is_edit_ok()
 
@@ -880,7 +915,8 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions):
         # XXX do this
         return []
 
-    def history(self, direction='descending', dre=re.compile('^\d+$')):
+    def history(self, direction='descending', dre=re.compile('^\d+$'),
+            limit=None):
         if not self.is_view_ok():
             return self._('[hidden]')
 
@@ -912,6 +948,10 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions):
         history.sort()
         history.reverse()
 
+        # restrict the volume
+        if limit:
+            history = history[:limit]
+
         timezone = self._db.getUserTimezone()
         l = []
         comments = {}
@@ -1068,6 +1108,13 @@ class _HTMLItem(HTMLInputMixin, HTMLPermissions):
                             cell[-1] += ' -> %s'%current[k]
                             current[k] = val
 
+                    elif isinstance(prop, hyperdb.Password) and args[k] is not None:
+                        val = args[k].dummystr()
+                        cell.append('%s: %s'%(self._(k), val))
+                        if current.has_key(k):
+                            cell[-1] += ' -> %s'%current[k]
+                            current[k] = val
+
                     elif not args[k]:
                         if current.has_key(k):
                             cell.append('%s: %s'%(self._(k), current[k]))
@@ -1182,12 +1229,9 @@ class _HTMLUser(_HTMLItem):
         return self._db.security.hasPermission(permission,
             self._nodeid, classname, property, itemid)
 
-    def hasRole(self, rolename):
-        """Determine whether the user has the Role."""
-        roles = self._db.user.get(self._nodeid, 'roles').split(',')
-        for role in roles:
-            if role.strip() == rolename: return True
-        return False
+    def hasRole(self, *rolenames):
+        """Determine whether the user has any role in rolenames."""
+        return self._db.user.has_role(self._nodeid, *rolenames)
 
 def HTMLItem(client, classname, nodeid, anonymous=0):
     if classname == 'user':
@@ -1217,7 +1261,12 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions):
         self._anonymous = anonymous
         self._name = name
         if not anonymous:
-            self._formname = '%s%s@%s'%(classname, nodeid, name)
+            if nodeid:
+                self._formname = '%s%s@%s'%(classname, nodeid, name)
+            else:
+                # This case occurs when creating a property for a
+                # non-anonymous class.
+                self._formname = '%s@%s'%(classname, name)
         else:
             self._formname = name
 
@@ -1243,8 +1292,9 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions):
         HTMLInputMixin.__init__(self)
 
     def __repr__(self):
-        return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
-            self._prop, self._value)
+        classname = self.__class__.__name__
+        return '<%s(0x%x) %s %r %r>'%(classname, id(self), self._formname,
+                                      self._prop, self._value)
     def __str__(self):
         return self.plain()
     def __cmp__(self, other):
@@ -1264,17 +1314,22 @@ class HTMLProperty(HTMLInputMixin, HTMLPermissions):
         property. Check "Create" for new items, or "Edit" for existing
         ones.
         """
+        perm = self._db.security.hasPermission
+        userid = self._client.userid
         if self._nodeid:
-            return self._db.security.hasPermission('Edit', self._client.userid,
-                self._classname, self._name, self._nodeid)
-        return self._db.security.hasPermission('Create', self._client.userid,
-            self._classname, self._name)
+            if not perm('Web Access', userid):
+                return False
+            return perm('Edit', userid, self._classname, self._name,
+                self._nodeid)
+        return perm('Create', userid, self._classname, self._name) or \
+            perm('Register', userid, self._classname, self._name)
 
     def is_view_ok(self):
         """ Is the user allowed to View the current class?
         """
-        if self._db.security.hasPermission('View', self._client.userid,
-                self._classname, self._name, self._nodeid):
+        perm = self._db.security.hasPermission
+        if perm('Web Access',  self._client.userid) and perm('View',
+                self._client.userid, self._classname, self._name, self._nodeid):
             return 1
         return self.is_edit_ok()
 
@@ -1300,7 +1355,42 @@ class StringHTMLProperty(HTMLProperty):
     )''', re.X | re.I)
     protocol_re = re.compile('^(ht|f)tp(s?)://', re.I)
 
-    def _hyper_repl_item(self,match,replacement):
+
+
+    def _hyper_repl(self, match):
+        if match.group('url'):
+            return self._hyper_repl_url(match, '<a href="%s">%s</a>%s')
+        elif match.group('email'):
+            return self._hyper_repl_email(match, '<a href="mailto:%s">%s</a>')
+        elif len(match.group('id')) < 10:
+            return self._hyper_repl_item(match,
+                '<a href="%(cls)s%(id)s">%(item)s</a>')
+        else:
+            # just return the matched text
+            return match.group(0)
+
+    def _hyper_repl_url(self, match, replacement):
+        u = s = match.group('url')
+        if not self.protocol_re.search(s):
+            u = 'http://' + s
+        end = ''
+        if '&gt;' in s:
+            # catch an escaped ">" in the URL
+            pos = s.find('&gt;')
+            end = s[pos:]
+            u = s = s[:pos]
+        if ')' in s and s.count('(') != s.count(')'):
+            # don't include extraneous ')' in the link
+            pos = s.rfind(')')
+            end = s[pos:] + end
+            u = s = s[:pos]
+        return replacement % (u, s, end)
+
+    def _hyper_repl_email(self, match, replacement):
+        s = match.group('email')
+        return replacement % (s, s)
+
+    def _hyper_repl_item(self, match, replacement):
         item = match.group('item')
         cls = match.group('class').lower()
         id = match.group('id')
@@ -1313,24 +1403,6 @@ class StringHTMLProperty(HTMLProperty):
         except KeyError:
             return item
 
-    def _hyper_repl(self, match):
-        if match.group('url'):
-            u = s = match.group('url')
-            if not self.protocol_re.search(s):
-                u = 'http://' + s
-            # catch an escaped ">" at the end of the URL
-            if s.endswith('&gt;'):
-                u = s = s[:-4]
-                e = '&gt;'
-            else:
-                e = ''
-            return '<a href="%s">%s</a>%s'%(u, s, e)
-        elif match.group('email'):
-            s = match.group('email')
-            return '<a href="mailto:%s">%s</a>'%(s, s)
-        else:
-            return self._hyper_repl_item(match,
-                '<a href="%(cls)s%(id)s">%(item)s</a>')
 
     def _hyper_repl_rst(self, match):
         if match.group('url'):
@@ -1339,8 +1411,11 @@ class StringHTMLProperty(HTMLProperty):
         elif match.group('email'):
             s = match.group('email')
             return '`%s <mailto:%s>`_'%(s, s)
-        else:
+        elif len(match.group('id')) < 10:
             return self._hyper_repl_item(match,'`%(item)s <%(cls)s%(id)s>`_')
+        else:
+            # just return the matched text
+            return match.group(0)
 
     def hyperlinked(self):
         """ Render a "hyperlinked" version of the text """
@@ -1457,8 +1532,7 @@ class StringHTMLProperty(HTMLProperty):
 
             value = '&quot;'.join(value.split('"'))
         name = self._formname
-        passthrough_args = ' '.join(['%s="%s"' % (k, cgi.escape(str(v), True))
-            for k,v in kwargs.items()])
+        passthrough_args = cgi_escape_attrs(**kwargs)
         return ('<textarea %(passthrough_args)s name="%(name)s" id="%(name)s"'
                 ' rows="%(rows)s" cols="%(cols)s">'
                  '%(value)s</textarea>') % locals()
@@ -1494,9 +1568,12 @@ class PasswordHTMLProperty(HTMLProperty):
 
         if self._value is None:
             return ''
-        return self._('*encrypted*')
+        value = self._value.dummystr()
+        if escape:
+            value = cgi.escape(value)
+        return value
 
-    def field(self, size=30):
+    def field(self, size=30, **kwargs):
         """ Render a form edit field for the property.
 
             If not editable, just display the value via plain().
@@ -1504,7 +1581,8 @@ class PasswordHTMLProperty(HTMLProperty):
         if not self.is_edit_ok():
             return self.plain(escape=1)
 
-        return self.input(type="password", name=self._formname, size=size)
+        return self.input(type="password", name=self._formname, size=size,
+                          **kwargs)
 
     def confirm(self, size=30):
         """ Render a second form edit field for the property, used for
@@ -1533,7 +1611,7 @@ class NumberHTMLProperty(HTMLProperty):
 
         return str(self._value)
 
-    def field(self, size=30):
+    def field(self, size=30, **kwargs):
         """ Render a form edit field for the property.
 
             If not editable, just display the value via plain().
@@ -1545,7 +1623,8 @@ class NumberHTMLProperty(HTMLProperty):
         if value is None:
             value = ''
 
-        return self.input(name=self._formname, value=value, size=size)
+        return self.input(name=self._formname, value=value, size=size,
+                          **kwargs)
 
     def __int__(self):
         """ Return an int of me
@@ -1569,7 +1648,7 @@ class BooleanHTMLProperty(HTMLProperty):
             return ''
         return self._value and self._("Yes") or self._("No")
 
-    def field(self):
+    def field(self, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -1585,15 +1664,17 @@ class BooleanHTMLProperty(HTMLProperty):
         checked = value and "checked" or ""
         if value:
             s = self.input(type="radio", name=self._formname, value="yes",
-                checked="checked")
+                checked="checked", **kwargs)
             s += self._('Yes')
-            s +=self.input(type="radio", name=self._formname, value="no")
+            s +=self.input(type="radio", name=self._formname,  value="no",
+                           **kwargs)
             s += self._('No')
         else:
-            s = self.input(type="radio", name=self._formname, value="yes")
+            s = self.input(type="radio", name=self._formname,  value="yes",
+                           **kwargs)
             s += self._('Yes')
             s +=self.input(type="radio", name=self._formname, value="no",
-                checked="checked")
+                checked="checked", **kwargs)
             s += self._('No')
         return s
 
@@ -1651,7 +1732,8 @@ class DateHTMLProperty(HTMLProperty):
         return DateHTMLProperty(self._client, self._classname, self._nodeid,
             self._prop, self._formname, ret)
 
-    def field(self, size=30, default=None, format=_marker, popcal=True):
+    def field(self, size=30, default=None, format=_marker, popcal=True,
+              **kwargs):
         """Render a form edit field for the property
 
         If not editable, just display the value via plain().
@@ -1686,7 +1768,8 @@ class DateHTMLProperty(HTMLProperty):
         elif isinstance(value, str) or isinstance(value, unicode):
             # most likely erroneous input to be passed back to user
             if isinstance(value, unicode): value = value.encode('utf8')
-            return self.input(name=self._formname, value=value, size=size)
+            return self.input(name=self._formname, value=value, size=size,
+                              **kwargs)
         else:
             raw_value = value
 
@@ -1706,7 +1789,8 @@ class DateHTMLProperty(HTMLProperty):
             if format is not self._marker:
                 value = value.pretty(format)
 
-        s = self.input(name=self._formname, value=value, size=size)
+        s = self.input(name=self._formname, value=value, size=size,
+                       **kwargs)
         if popcal:
             s += self.popcal()
         return s
@@ -1801,7 +1885,7 @@ class IntervalHTMLProperty(HTMLProperty):
 
         return self._value.pretty()
 
-    def field(self, size=30):
+    def field(self, size=30, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -1813,7 +1897,8 @@ class IntervalHTMLProperty(HTMLProperty):
         if value is None:
             value = ''
 
-        return self.input(name=self._formname, value=value, size=size)
+        return self.input(name=self._formname, value=value, size=size,
+                          **kwargs)
 
 class LinkHTMLProperty(HTMLProperty):
     """ Link HTMLProperty
@@ -1866,7 +1951,7 @@ class LinkHTMLProperty(HTMLProperty):
             value = cgi.escape(value)
         return value
 
-    def field(self, showid=0, size=None):
+    def field(self, showid=0, size=None, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -1884,10 +1969,11 @@ class LinkHTMLProperty(HTMLProperty):
                 value = linkcl.get(self._value, k)
             else:
                 value = self._value
-        return self.input(name=self._formname, value=value, size=size)
+        return self.input(name=self._formname, value=value, size=size,
+                          **kwargs)
 
     def menu(self, size=None, height=None, showid=0, additional=[], value=None,
-            sort_on=None, **conditions):
+             sort_on=None, html_kwargs = {}, **conditions):
         """ Render a form select list for this property
 
             "size" is used to limit the length of the list labels
@@ -1920,7 +2006,8 @@ class LinkHTMLProperty(HTMLProperty):
             value = None
 
         linkcl = self._db.getclass(self._prop.classname)
-        l = ['<select name="%s">'%self._formname]
+        l = ['<select %s>'%cgi_escape_attrs(name = self._formname,
+                                            **html_kwargs)]
         k = linkcl.labelprop(1)
         s = ''
         if value is None:
@@ -2029,9 +2116,10 @@ class MultilinkHTMLProperty(HTMLProperty):
         check = self._db.security.hasPermission
         userid = self._client.userid
         classname = self._prop.classname
-        for value in values:
-            if check('View', userid, classname, itemid=value):
-                yield HTMLItem(self._client, classname, value)
+        if check('Web Access', userid):
+            for value in values:
+                if check('View', userid, classname, itemid=value):
+                    yield HTMLItem(self._client, classname, value)
 
     def __iter__(self):
         """ iterate and return a new HTMLItem
@@ -2086,7 +2174,7 @@ class MultilinkHTMLProperty(HTMLProperty):
             value = cgi.escape(value)
         return value
 
-    def field(self, size=30, showid=0):
+    def field(self, size=30, showid=0, **kwargs):
         """ Render a form edit field for the property
 
             If not editable, just display the value via plain().
@@ -2095,18 +2183,22 @@ class MultilinkHTMLProperty(HTMLProperty):
             return self.plain(escape=1)
 
         linkcl = self._db.getclass(self._prop.classname)
-        value = self._value[:]
-        # map the id to the label property
-        if not linkcl.getkey():
-            showid=1
-        if not showid:
-            k = linkcl.labelprop(1)
-            value = lookupKeys(linkcl, k, value)
-        value = ','.join(value)
-        return self.input(name=self._formname, size=size, value=value)
+
+        if 'value' not in kwargs:
+            value = self._value[:]
+            # map the id to the label property
+            if not linkcl.getkey():
+                showid=1
+            if not showid:
+                k = linkcl.labelprop(1)
+                value = lookupKeys(linkcl, k, value)
+            value = ','.join(value)
+            kwargs["value"] = value
+
+        return self.input(name=self._formname, size=size, **kwargs)
 
     def menu(self, size=None, height=None, showid=0, additional=[],
-             value=None, sort_on=None, **conditions):
+             value=None, sort_on=None, html_kwargs = {}, **conditions):
         """ Render a form <select> list for this property.
 
             "size" is used to limit the length of the list labels
@@ -2159,7 +2251,9 @@ class MultilinkHTMLProperty(HTMLProperty):
                 # The "no selection" option.
                 height += 1
             height = min(height, 7)
-        l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
+        l = ['<select multiple %s>'%cgi_escape_attrs(name = self._formname,
+                                                     size = height,
+                                                     **html_kwargs)]
         k = linkcl.labelprop(1)
 
         if value:
@@ -2211,8 +2305,9 @@ class MultilinkHTMLProperty(HTMLProperty):
         l.append('</select>')
         return '\n'.join(l)
 
+
 # set the propclasses for HTMLItem
-propclasses = (
+propclasses = [
     (hyperdb.String, StringHTMLProperty),
     (hyperdb.Number, NumberHTMLProperty),
     (hyperdb.Boolean, BooleanHTMLProperty),
@@ -2221,16 +2316,32 @@ propclasses = (
     (hyperdb.Password, PasswordHTMLProperty),
     (hyperdb.Link, LinkHTMLProperty),
     (hyperdb.Multilink, MultilinkHTMLProperty),
-)
+]
+
+def register_propclass(prop, cls):
+    for index,propclass in enumerate(propclasses):
+        p, c = propclass
+        if prop == p:
+            propclasses[index] = (prop, cls)
+            break
+    else:
+        propclasses.append((prop, cls))
+
 
 def make_sort_function(db, classname, sort_on=None):
-    """Make a sort function for a given class
+    """Make a sort function for a given class.
+
+    The list being sorted may contain mixed ids and labels.
     """
     linkcl = db.getclass(classname)
     if sort_on is None:
         sort_on = linkcl.orderprop()
     def sortfunc(a, b):
-        return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
+        if num_re.match(a):
+            a = linkcl.get(a, sort_on)
+        if num_re.match(b):
+            b = linkcl.get(b, sort_on)
+        return cmp(a, b)
     return sortfunc
 
 def handleListCGIValue(value):
@@ -2350,12 +2461,16 @@ class HTMLRequest(HTMLInputMixin):
                 self.columns = handleListCGIValue(self.form[name])
                 break
         self.show = support.TruthDict(self.columns)
+        security = self._client.db.security
+        userid = self._client.userid
 
         # sorting and grouping
         self.sort = []
         self.group = []
         self._parse_sort(self.sort, 'sort')
         self._parse_sort(self.group, 'group')
+        self.sort = security.filterSortspec(userid, self.classname, self.sort)
+        self.group = security.filterSortspec(userid, self.classname, self.group)
 
         # filtering
         self.filter = []
@@ -2385,6 +2500,8 @@ class HTMLRequest(HTMLInputMixin):
                         self.filterspec[name] = handleListCGIValue(fv)
                     else:
                         self.filterspec[name] = fv.value
+        self.filterspec = security.filterFilterspec(userid, self.classname,
+            self.filterspec)
 
         # full-text search argument
         self.search_text = None
@@ -2604,6 +2721,12 @@ function help_window(helpurl, width, height) {
     def batch(self):
         """ Return a batch object for results from the "current search"
         """
+        check = self._client.db.security.hasPermission
+        userid = self._client.userid
+        if not check('Web Access', userid):
+            return Batch(self.client, [], self.pagesize, self.startwith,
+                classname=self.classname)
+
         filterspec = self.filterspec
         sort = self.sort
         group = self.group
@@ -2620,8 +2743,6 @@ function help_window(helpurl, width, height) {
             matches = None
 
         # filter for visibility
-        check = self._client.db.security.hasPermission
-        userid = self._client.userid
         l = [id for id in klass.filter(matches, filterspec, sort, group)
             if check('View', userid, self.classname, itemid=id)]
 
@@ -2748,6 +2869,9 @@ class TemplatingUtils:
             raise AttributeError, name
         return self.client.instance.templating_utils[name]
 
+    def keywords_expressions(self, request):
+        return render_keywords_expression_editor(request)
+
     def html_calendar(self, request):
         """Generate a HTML calendar.
 
@@ -2761,7 +2885,9 @@ class TemplatingUtils:
 
         html will simply be a table.
         """
-        date_str  = request.form.getfirst("date", ".")
+        tz = request.client.db.getUserTimezone()
+        current_date = date.Date(".").local(tz)
+        date_str  = request.form.getfirst("date", current_date)
         display   = request.form.getfirst("display", date_str)
         template  = request.form.getfirst("@template", "calendar")
         form      = request.form.getfirst("form")