Code

forward-porting of fixed edit action / parsePropsFromForm to handle index-page edits...
[roundup.git] / roundup / cgi / templating.py
index cbc742bb44d488411204cf57959ce8a12271508d..dc1530951eb343f35c932b4a84b14cfb5a9682c0 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
@@ -27,6 +31,14 @@ from roundup.cgi import ZTUtils
 class NoTemplate(Exception):
     pass
 
+class Unauthorised(Exception):
+    def __init__(self, action, klass):
+        self.action = action
+        self.klass = klass
+    def __str__(self):
+        return 'You are not allowed to %s items of class %s'%(self.action,
+            self.klass)
+
 def find_template(dir, name, extension):
     ''' Find a template in the nominated dir
     '''
@@ -135,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
@@ -218,6 +232,9 @@ class RoundupPageTemplate(PageTemplate.PageTemplate):
             getEngine().getContext(c), output, tal=1, strictinsert=0)()
         return output.getvalue()
 
+    def __repr__(self):
+        return '<Roundup PageTemplate %r>'%self.id
+
 class HTMLDatabase:
     ''' Return HTMLClasses for valid class fetches
     '''
@@ -271,16 +288,32 @@ class HTMLPermissions:
         '''
         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()
 
+    def view_check(self):
+        ''' Raise the Unauthorised exception if the user's not permitted to
+            view this class.
+        '''
+        if not self.is_view_ok():
+            raise Unauthorised("view", self._classname)
+
+    def edit_check(self):
+        ''' Raise the Unauthorised exception if the user's not permitted to
+            edit this class.
+        '''
+        if not self.is_edit_ok():
+            raise Unauthorised("edit", self._classname)
+
 def input_html4(**attrs):
     """Generate an 'input' (html4) element with given attributes"""
     return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
@@ -374,7 +407,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':
@@ -401,7 +434,7 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
             l.sort(lambda a,b:cmp(a._name, b._name))
         return l
 
-    def list(self):
+    def list(self, sort_on=None):
         ''' List all items in this class.
         '''
         if self.classname == 'user':
@@ -411,7 +444,7 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
 
         # get the list and sort it nicely
         l = self._klass.list()
-        sortfunc = make_sort_function(self._db, self.classname)
+        sortfunc = make_sort_function(self._db, self.classname, sort_on)
         l.sort(sortfunc)
 
         l = [klass(self._client, self.classname, x) for x in l]
@@ -447,19 +480,17 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
         idlessprops.sort()
         return ['id'] + idlessprops
 
-    def filter(self, request=None):
+    def filter(self, request=None, filterspec={}, sort=(None,None),
+            group=(None,None)):
         ''' Return a list of items from this class, filtered and sorted
             by the current requested filterspec/filter/sort/group args
+
+            "request" takes precedence over the other three arguments.
         '''
-        # XXX allow direct specification of the filterspec etc.
         if request is not None:
             filterspec = request.filterspec
             sort = request.sort
             group = request.group
-        else:
-            filterspec = {}
-            sort = (None,None)
-            group = (None,None)
         if self.classname == 'user':
             klass = HTMLUser
         else:
@@ -499,10 +530,14 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
     def submit(self, label="Submit New Entry"):
         ''' Generate a submit button (and action hidden element)
         '''
-        return self.input(type="hidden",name="@action",value="new") + '\n' + \
-               self.input(type="submit",name="submit",value=label)
+        self.view_check()
+        if self.is_edit_ok():
+            return self.input(type="hidden",name="@action",value="new") + \
+                   '\n' + self.input(type="submit",name="submit",value=label)
+        return ''
 
     def history(self):
+        self.view_check()
         return 'New node - no history'
 
     def renderWith(self, name, **kwargs):
@@ -517,7 +552,11 @@ class HTMLClass(HTMLInputMixin, HTMLPermissions):
         pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
 
         # use our fabricated request
-        return pt.render(self._client, self.classname, req)
+        args = {
+            'ok_message': self._client.ok_message,
+            'error_message': self._client.error_message
+        }
+        return pt.render(self._client, self.classname, req, **args)
 
 class HTMLItem(HTMLInputMixin, HTMLPermissions):
     ''' Accesses through an *item*
@@ -589,6 +628,8 @@ class HTMLItem(HTMLInputMixin, HTMLPermissions):
         return []
 
     def history(self, direction='descending', dre=re.compile('\d+')):
+        self.view_check()
+
         l = ['<table class="history">'
              '<tr><th colspan="4" class="header">',
              _('History'),
@@ -839,11 +880,11 @@ class HTMLUser(HTMLItem):
         ''' 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,
+        return self._db.security.hasPermission('View', self._client.userid,
             self._classname) or (self._nodeid == self._client.userid and
             self._db.user.get(self._client.userid, 'username') != 'anonymous')
 
-class HTMLProperty(HTMLInputMixin):
+class HTMLProperty(HTMLInputMixin, HTMLPermissions):
     ''' String, Number, Date, Interval HTMLProperty
 
         Has useful attributes:
@@ -869,7 +910,7 @@ class HTMLProperty(HTMLInputMixin):
             self._formname = name
 
         HTMLInputMixin.__init__(self)
-        
+
     def __repr__(self):
         return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
             self._prop, self._value)
@@ -880,6 +921,26 @@ class HTMLProperty(HTMLInputMixin):
             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\.\-]+)|'
@@ -907,12 +968,14 @@ 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()
+
         if self._value is None:
             return ''
         if escape:
@@ -931,37 +994,59 @@ class StringHTMLProperty(HTMLProperty):
 
             This requires the StructureText module to be installed separately.
         '''
+        self.view_check()
+
         s = self.plain(escape=escape)
         if not StructuredText:
             return s
         return StructuredText(s,level=1,header=0)
 
     def field(self, size = 30):
-        ''' Render a form edit field for the property
+        ''' Render the property as a field in HTML.
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
         if self._value is None:
             value = ''
         else:
             value = cgi.escape(str(self._value))
+
+        if self.is_edit_ok():
             value = '&quot;'.join(value.split('"'))
-        return self.input(name=self._formname,value=value,size=size)
+            return self.input(name=self._formname,value=value,size=size)
+
+        return self.plain()
 
     def multiline(self, escape=0, rows=5, cols=40):
-        ''' Render a multiline form edit field for the property
+        ''' Render a multiline form edit field for the property.
+
+            If not editable, just display the plain() value in a <pre> tag.
         '''
+        self.view_check()
+
         if self._value is None:
             value = ''
         else:
             value = cgi.escape(str(self._value))
+
+        if self.is_edit_ok():
             value = '&quot;'.join(value.split('"'))
-        return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
-            self._formname, rows, cols, value)
+            return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
+                self._formname, rows, cols, value)
+
+        return '<pre>%s</pre>'%self.plain()
 
     def email(self, escape=1):
         ''' Render the value of the property as an obscured email address
         '''
-        if self._value is None: value = ''
-        else: value = str(self._value)
+        self.view_check()
+
+        if self._value is None:
+            value = ''
+        else:
+            value = str(self._value)
         if value.find('@') != -1:
             name, domain = value.split('@')
             domain = ' '.join(domain.split('.')[:-1])
@@ -977,38 +1062,64 @@ class PasswordHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
         '''
+        self.view_check()
+
         if self._value is None:
             return ''
         return _('*encrypted*')
 
     def field(self, size = 30):
         ''' Render a form edit field for the property.
+
+            If not editable, just display the value via plain().
         '''
-        return self.input(type="password", name=self._formname, size=size)
+        self.view_check()
+
+        if self.is_edit_ok():
+            return self.input(type="password", name=self._formname, size=size)
+
+        return self.plain()
 
     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 "@confirm@name".
+
+            If not editable, display nothing.
         '''
-        return self.input(type="password", name="@confirm@%s"%self._formname,
-            size=size)
+        self.view_check()
+
+        if self.is_edit_ok():
+            return self.input(type="password",
+                name="@confirm@%s"%self._formname, size=size)
+
+        return ''
 
 class NumberHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
         '''
+        self.view_check()
+
         return str(self._value)
 
     def field(self, size = 30):
-        ''' Render a form edit field for the property
+        ''' Render a form edit field for the property.
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
         if self._value is None:
             value = ''
         else:
             value = cgi.escape(str(self._value))
+
+        if self.is_edit_ok():
             value = '&quot;'.join(value.split('"'))
-        return self.input(name=self._formname,value=value,size=size)
+            return self.input(name=self._formname,value=value,size=size)
+
+        return self.plain()
 
     def __int__(self):
         ''' Return an int of me
@@ -1025,23 +1136,34 @@ class BooleanHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
         '''
+        self.view_check()
+
         if self._value is None:
             return ''
         return self._value and "Yes" or "No"
 
     def field(self):
         ''' Render a form edit field for the property
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
+        if not is_edit_ok():
+            return self.plain()
+
         checked = self._value and "checked" or ""
         if self._value:
-            s = self.input(type="radio",name=self._formname,value="yes",checked="checked")
+            s = self.input(type="radio", name=self._formname, value="yes",
+                checked="checked")
             s += 'Yes'
-            s +=self.input(type="radio",name=self._formname,value="no")
+            s +=self.input(type="radio", name=self._formname, value="no")
             s += 'No'
         else:
-            s = self.input(type="radio",name=self._formname,value="yes")
+            s = self.input(type="radio", name=self._formname, value="yes")
             s += 'Yes'
-            s +=self.input(type="radio",name=self._formname,value="no",checked="checked")
+            s +=self.input(type="radio", name=self._formname, value="no",
+                checked="checked")
             s += 'No'
         return s
 
@@ -1049,6 +1171,8 @@ class DateHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
         '''
+        self.view_check()
+
         if self._value is None:
             return ''
         return str(self._value.local(self._db.getUserTimezone()))
@@ -1059,24 +1183,37 @@ class DateHTMLProperty(HTMLProperty):
             This is useful for defaulting a new value. Returns a
             DateHTMLProperty.
         '''
+        self.view_check()
+
         return DateHTMLProperty(self._client, self._nodeid, self._prop,
             self._formname, date.Date('.'))
 
     def field(self, size = 30):
         ''' Render a form edit field for the property
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
         if self._value is None:
             value = ''
         else:
-            value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
+            tz = self._db.getUserTimezone()
+            value = cgi.escape(str(self._value.local(tz)))
+
+        if is_edit_ok():
             value = '&quot;'.join(value.split('"'))
-        return self.input(name=self._formname,value=value,size=size)
+            return self.input(name=self._formname,value=value,size=size)
+        
+        return self.plain()
 
     def reldate(self, pretty=1):
         ''' Render the interval between the date and now.
 
             If the "pretty" flag is true, then make the display pretty.
         '''
+        self.view_check()
+
         if not self._value:
             return ''
 
@@ -1095,6 +1232,8 @@ class DateHTMLProperty(HTMLProperty):
             string, then it'll be stripped from the output. This is handy
             for the situatin when a date only specifies a month and a year.
         '''
+        self.view_check()
+
         if format is not self._marker:
             return self._value.pretty(format)
         else:
@@ -1103,6 +1242,8 @@ class DateHTMLProperty(HTMLProperty):
     def local(self, offset):
         ''' Return the date/time as a local (timezone offset) date/time.
         '''
+        self.view_check()
+
         return DateHTMLProperty(self._client, self._nodeid, self._prop,
             self._formname, self._value.local(offset))
 
@@ -1110,6 +1251,8 @@ class IntervalHTMLProperty(HTMLProperty):
     def plain(self):
         ''' Render a "plain" representation of the property
         '''
+        self.view_check()
+
         if self._value is None:
             return ''
         return str(self._value)
@@ -1117,17 +1260,27 @@ class IntervalHTMLProperty(HTMLProperty):
     def pretty(self):
         ''' Render the interval in a pretty format (eg. "yesterday")
         '''
+        self.view_check()
+
         return self._value.pretty()
 
     def field(self, size = 30):
         ''' Render a form edit field for the property
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
         if self._value is None:
             value = ''
         else:
             value = cgi.escape(str(self._value))
+
+        if is_edit_ok():
             value = '&quot;'.join(value.split('"'))
-        return self.input(name=self._formname,value=value,size=size)
+            return self.input(name=self._formname,value=value,size=size)
+
+        return self.plain()
 
 class LinkHTMLProperty(HTMLProperty):
     ''' Link HTMLProperty
@@ -1161,6 +1314,8 @@ class LinkHTMLProperty(HTMLProperty):
     def plain(self, escape=0):
         ''' Render a "plain" representation of the property
         '''
+        self.view_check()
+
         if self._value is None:
             return ''
         linkcl = self._db.classes[self._prop.classname]
@@ -1172,55 +1327,40 @@ class LinkHTMLProperty(HTMLProperty):
 
     def field(self, showid=0, size=None):
         ''' Render a form edit field for the property
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
+        if not self.is_edit_ok():
+            return self.plain()
+
+        # edit field
         linkcl = self._db.getclass(self._prop.classname)
-        if linkcl.getprops().has_key('order'):  
-            sort_on = 'order'  
-        else:  
-            sort_on = linkcl.labelprop()  
-        options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
-        # TODO: make this a field display, not a menu one!
-        l = ['<select name="%s">'%self._formname]
-        k = linkcl.labelprop(1)
         if self._value is None:
-            s = 'selected="selected" '
+            value = ''
         else:
-            s = ''
-        l.append(_('<option %svalue="-1">- no selection -</option>')%s)
-
-        # make sure we list the current value if it's retired
-        if self._value and self._value not in options:
-            options.insert(0, self._value)
-
-        for optionid in options:
-            # 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="selected" '
-
-            # figure the label
-            if showid:
-                lab = '%s%s: %s'%(self._prop.classname, optionid, option)
+            k = linkcl.getkey()
+            if k:
+                label = linkcl.get(self._value, k)
             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>')
-        return '\n'.join(l)
+                label = self._value
+            value = cgi.escape(str(self._value))
+            value = '&quot;'.join(value.split('"'))
+        return '<input name="%s" value="%s" size="%s">'%(self._formname,
+            label, size)
 
     def menu(self, size=None, height=None, showid=0, additional=[],
-            **conditions):
+            sort_on=None, **conditions):
         ''' Render a form select list for this property
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
+        if not self.is_edit_ok():
+            return self.plain()
+
         value = self._value
 
         linkcl = self._db.getclass(self._prop.classname)
@@ -1233,7 +1373,10 @@ class LinkHTMLProperty(HTMLProperty):
         if linkcl.getprops().has_key('order'):  
             sort_on = ('+', 'order')
         else:  
-            sort_on = ('+', linkcl.labelprop())
+            if sort_on is None:
+                sort_on = ('+', linkcl.labelprop())
+            else:
+                sort_on = ('+', sort_on)
         options = linkcl.filter(None, conditions, sort_on, (None, None))
 
         # make sure we list the current value if it's retired
@@ -1280,7 +1423,8 @@ class MultilinkHTMLProperty(HTMLProperty):
     def __init__(self, *args, **kwargs):
         HTMLProperty.__init__(self, *args, **kwargs)
         if self._value:
-            self._value.sort(make_sort_function(self._db, self._prop.classname))
+            sortfun = make_sort_function(self._db, self._prop.classname)
+            self._value.sort(sortfun)
     
     def __len__(self):
         ''' length of the multilink '''
@@ -1303,7 +1447,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
 
@@ -1321,6 +1465,8 @@ class MultilinkHTMLProperty(HTMLProperty):
     def plain(self, escape=0):
         ''' Render a "plain" representation of the property
         '''
+        self.view_check()
+
         linkcl = self._db.classes[self._prop.classname]
         k = linkcl.labelprop(1)
         labels = []
@@ -1333,7 +1479,14 @@ class MultilinkHTMLProperty(HTMLProperty):
 
     def field(self, size=30, showid=0):
         ''' Render a form edit field for the property
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
+        if not self.is_edit_ok():
+            return self.plain()
+
         linkcl = self._db.getclass(self._prop.classname)
         value = self._value[:]
         # map the id to the label property
@@ -1346,13 +1499,23 @@ class MultilinkHTMLProperty(HTMLProperty):
         return self.input(name=self._formname,size=size,value=value)
 
     def menu(self, size=None, height=None, showid=0, additional=[],
-            **conditions):
+            sort_on=None, **conditions):
         ''' Render a form select list for this property
+
+            If not editable, just display the value via plain().
         '''
+        self.view_check()
+
+        if not self.is_edit_ok():
+            return self.plain()
+
         value = self._value
 
         linkcl = self._db.getclass(self._prop.classname)
-        sort_on = ('+', find_sort_key(linkcl))
+        if sort_on is None:
+            sort_on = ('+', find_sort_key(linkcl))
+        else:
+            sort_on = ('+', sort_on)
         options = linkcl.filter(None, conditions, sort_on)
         height = height or min(len(options), 7)
         l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
@@ -1405,11 +1568,12 @@ propclasses = (
     (hyperdb.Multilink, MultilinkHTMLProperty),
 )
 
-def make_sort_function(db, classname):
+def make_sort_function(db, classname, sort_on=None):
     '''Make a sort function for a given class
     '''
     linkcl = db.getclass(classname)
-    sort_on = find_sort_key(linkcl)
+    if sort_on is None:
+        sort_on = find_sort_key(linkcl)
     def sortfunc(a, b):
         return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
     return sortfunc
@@ -1443,25 +1607,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
@@ -1716,6 +1880,7 @@ submitted = false;
 function submit_once() {
     if (submitted) {
         alert("Your request is being processed.\\nPlease be patient.");
+        event.returnValue = 0;    // work-around for IE
         return 0;
     }
     submitted = true;