Code

- replaced the content() callback ickiness with Page Template macro usage
[roundup.git] / roundup / cgi / client.py
index 264814442e6a51e150a21c929dd5eea6815d35c8..a2e1e1ee03e7c49937ddadc23ae04a047dca56bf 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.25 2002-09-09 23:55:19 richard Exp $
+# $Id: client.py,v 1.42 2002-09-25 02:10:25 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -10,7 +10,7 @@ import binascii, Cookie, time, random
 from roundup import roundupdb, date, hyperdb, password
 from roundup.i18n import _
 
-from roundup.cgi.templating import getTemplate, HTMLRequest, NoTemplate
+from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate
 from roundup.cgi import cgitb
 
 from roundup.cgi.PageTemplates import PageTemplate
@@ -63,8 +63,7 @@ class Client:
     keeps the nodeid of the session as the "session" attribute.
 
     Client attributes:
-        "url" is the current url path
-        "path" is the PATH_INFO inside the instance
+        "path" is the PATH_INFO inside the instance (with no leading '/')
         "base" is the base URL for the instance
     '''
 
@@ -74,32 +73,43 @@ class Client:
         self.request = request
         self.env = env
 
+        # save off the path
         self.path = env['PATH_INFO']
-        self.split_path = self.path.split('/')
-        self.instance_path_name = env['INSTANCE_NAME']
 
         # this is the base URL for this instance
-        url = self.env['SCRIPT_NAME'] + '/' + self.instance_path_name
-        self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
-            None, None, None))
-
-        # request.path is the full request path
-        x, x, path, x, x, x = urlparse.urlparse(request.path)
-        self.url = urlparse.urlunparse(('http', env['HTTP_HOST'], path,
-            None, None, None))
+        self.base = self.instance.config.TRACKER_WEB
 
+        # see if we need to re-parse the environment for the form (eg Zope)
         if form is None:
             self.form = cgi.FieldStorage(environ=env)
         else:
             self.form = form
-        self.headers_done = 0
+
+        # turn debugging on/off
         try:
             self.debug = int(env.get("ROUNDUP_DEBUG", 0))
         except ValueError:
             # someone gave us a non-int debug level, turn it off
             self.debug = 0
 
+        # flag to indicate that the HTTP headers have been sent
+        self.headers_done = 0
+
+        # additional headers to send with the request - must be registered
+        # before the first write
+        self.additional_headers = {}
+        self.response_code = 200
+
     def main(self):
+        ''' Wrap the real main in a try/finally so we always close off the db.
+        '''
+        try:
+            self.inner_main()
+        finally:
+            if hasattr(self, 'db'):
+                self.db.close()
+
+    def inner_main(self):
         ''' Process a request.
 
             The most common requests are handled like so:
@@ -136,20 +146,22 @@ class Client:
             # and self.template, and may also append error/ok_messages)
             self.handle_action()
             # now render the page
-            if self.form.has_key(':contentonly'):
-                # just the content
-                self.write(self.content())
-            else:
-                # render the content inside the page template
-                self.write(self.renderTemplate('page', '',
-                    ok_message=self.ok_message,
-                    error_message=self.error_message))
+
+            # we don't want clients caching our dynamic pages
+            self.additional_headers['Cache-Control'] = 'no-cache'
+            self.additional_headers['Pragma'] = 'no-cache'
+            self.additional_headers['Expires'] = 'Thu, 1 Jan 1970 00:00:00 GMT'
+
+            # render the content
+            self.write(self.renderContext())
         except Redirect, url:
             # let's redirect - if the url isn't None, then we need to do
             # the headers, otherwise the headers have been set before the
             # exception was raised
             if url:
-                self.header({'Location': url}, response=302)
+                self.additional_headers['Location'] = url
+                self.response_code = 302
+            self.write('Redirecting to <a href="%s">%s</a>'%(url, url))
         except SendFile, designator:
             self.serve_file(designator)
         except SendStaticFile, file:
@@ -255,7 +267,7 @@ class Client:
         self.nodeid = None
 
         # determine the classname and possibly nodeid
-        path = self.split_path
+        path = self.path.split('/')
         if not path or path[0] in ('', 'home', 'index'):
             if self.form.has_key(':template'):
                 self.template = self.form[':template'].value
@@ -275,6 +287,8 @@ class Client:
         if m:
             self.classname = m.group(1)
             self.nodeid = m.group(2)
+            if not self.db.getclass(self.classname).hasnode(self.nodeid):
+                raise NotFound, '%s/%s'%(self.classname, self.nodeid)
             # with a designator, we default to item view
             self.template = 'item'
         else:
@@ -303,65 +317,53 @@ class Client:
 
         # we just want to serve up the file named
         file = self.db.file
-        self.header({'Content-Type': file.get(nodeid, 'type')})
+        self.additional_headers['Content-Type'] = file.get(nodeid, 'type')
         self.write(file.get(nodeid, 'content'))
 
     def serve_static_file(self, file):
         # we just want to serve up the file named
         mt = mimetypes.guess_type(str(file))[0]
-        self.header({'Content-Type': mt})
+        self.additional_headers['Content-Type'] = mt
         self.write(open(os.path.join(self.instance.config.TEMPLATES,
             file)).read())
 
-    def renderTemplate(self, name, extension, **kwargs):
+    def renderContext(self):
         ''' Return a PageTemplate for the named page
         '''
-        pt = getTemplate(self.instance.config.TEMPLATES, name, extension)
-        # XXX handle PT rendering errors here more nicely
+        name = self.classname
+        extension = self.template
+        pt = Templates(self.instance.config.TEMPLATES).get(name, extension)
+
+        # catch errors so we can handle PT rendering errors more nicely
+        args = {
+            'ok_message': self.ok_message,
+            'error_message': self.error_message
+        }
         try:
             # let the template render figure stuff out
-            return pt.render(self, None, None, **kwargs)
-        except PageTemplate.PTRuntimeError, message:
-            return '<strong>%s</strong><ol>%s</ol>'%(message,
-                '<li>'.join(pt._v_errors))
+            return pt.render(self, None, None, **args)
         except NoTemplate, message:
             return '<strong>%s</strong>'%message
         except:
             # everything else
             return cgitb.pt_html()
 
-    def content(self):
-        ''' Callback used by the page template to render the content of 
-            the page.
-
-            If we don't have a specific class to display, that is none was
-            determined in determine_context(), then we display a "home"
-            template.
-        '''
-        # now render the page content using the template we determined in
-        # determine_context
-        if self.classname is None:
-            name = 'home'
-        else:
-            name = self.classname
-        return self.renderTemplate(self.classname, self.template)
-
     # these are the actions that are available
-    actions = {
-        'edit':     'editItemAction',
-        'editCSV':  'editCSVAction',
-        'new':      'newItemAction',
-        'register': 'registerAction',
-        'login':    'loginAction',
-        'logout':   'logout_action',
-        'search':   'searchAction',
-    }
+    actions = (
+        ('edit',     'editItemAction'),
+        ('editCSV',  'editCSVAction'),
+        ('new',      'newItemAction'),
+        ('register', 'registerAction'),
+        ('login',    'loginAction'),
+        ('logout',   'logout_action'),
+        ('search',   'searchAction'),
+    )
     def handle_action(self):
         ''' Determine whether there should be an _action called.
 
             The action is defined by the form variable :action which
             identifies the method on this object to call. The four basic
-            actions are defined in the "actions" dictionary on this class:
+            actions are defined in the "actions" sequence on this class:
              "edit"      -> self.editItemAction
              "new"       -> self.newItemAction
              "register"  -> self.registerAction
@@ -375,11 +377,14 @@ class Client:
         try:
             # get the action, validate it
             action = self.form[':action'].value
-            if not self.actions.has_key(action):
+            for name, method in self.actions:
+                if name == action:
+                    break
+            else:
                 raise ValueError, 'No such action "%s"'%action
 
             # call the mapped action
-            getattr(self, self.actions[action])()
+            getattr(self, method)()
         except Redirect:
             raise
         except Unauthorised:
@@ -395,11 +400,17 @@ class Client:
             self.header()
         self.request.wfile.write(content)
 
-    def header(self, headers=None, response=200):
+    def header(self, headers=None, response=None):
         '''Put up the appropriate header.
         '''
         if headers is None:
             headers = {'Content-Type':'text/html'}
+        if response is None:
+            response = self.response_code
+
+        # update with additional info
+        headers.update(self.additional_headers)
+
         if not headers.has_key('Content-Type'):
             headers['Content-Type'] = 'text/html'
         self.request.send_response(response)
@@ -431,10 +442,10 @@ class Client:
         expire = Cookie._getdate(86400*365)
 
         # generate the cookie path - make sure it has a trailing '/'
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
+        path = '/'.join((self.env['SCRIPT_NAME'], self.env['TRACKER_NAME'],
             ''))
-        self.header({'Set-Cookie': 'roundup_user_2=%s; expires=%s; Path=%s;'%(
-            self.session, expire, path)})
+        self.additional_headers['Set-Cookie'] = \
+          'roundup_user_2=%s; expires=%s; Path=%s;'%(self.session, expire, path)
 
     def make_user_anonymous(self):
         ''' Make us anonymous
@@ -445,25 +456,13 @@ class Client:
         self.userid = self.db.user.lookup('anonymous')
         self.user = 'anonymous'
 
-    def logout(self):
-        ''' Make us really anonymous - nuke the cookie too
-        '''
-        self.make_user_anonymous()
-
-        # construct the logout cookie
-        now = Cookie._getdate()
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        self.header({'Set-Cookie':
-            'roundup_user_2=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
-            path)})
-        self.login()
-
     def opendb(self, user):
         ''' Open the database.
         '''
         # open the db if the user has changed
         if not hasattr(self, 'db') or user != self.db.journaltag:
+            if hasattr(self, 'db'):
+                self.db.close()
             self.db = self.instance.open(user)
 
     #
@@ -528,10 +527,10 @@ class Client:
 
         # construct the logout cookie
         now = Cookie._getdate()
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
+        path = '/'.join((self.env['SCRIPT_NAME'], self.env['TRACKER_NAME'],
             ''))
-        self.header(headers={'Set-Cookie':
-          'roundup_user_2=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
+        self.additional_headers['Set-Cookie'] = \
+           'roundup_user_2=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)
 
         # Let the user know what's going on
         self.ok_message.append(_('You are logged out'))
@@ -569,6 +568,7 @@ class Client:
             self.db.commit()
         except ValueError, message:
             self.error_message.append(message)
+            return
 
         # log the new user in
         self.user = cl.get(self.userid, 'username')
@@ -578,7 +578,11 @@ class Client:
         self.set_cookie(self.user, password)
 
         # nice message
-        self.ok_message.append(_('You are now registered, welcome!'))
+        message = _('You are now registered, welcome!')
+
+        # redirect to the item's edit page
+        raise Redirect, '%s%s%s?:ok_message=%s'%(
+            self.base, self.classname, self.userid,  urllib.quote(message))
 
     def registerPermission(self, props):
         ''' Determine whether the user has permission to register
@@ -654,7 +658,7 @@ class Client:
             message = _('nothing changed')
 
         # redirect to the item's edit page
-        raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
+        raise Redirect, '%s%s%s?:ok_message=%s'%(self.base, self.classname,
             self.nodeid,  urllib.quote(message))
 
     def editItemPermission(self, props):
@@ -739,7 +743,7 @@ class Client:
             return
 
         # redirect to the new item's page
-        raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
+        raise Redirect, '%s%s%s?:ok_message=%s'%(self.base, self.classname,
             nid,  urllib.quote(message))
 
     def newItemPermission(self, props):
@@ -1078,10 +1082,11 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
 
     props = {}
     keys = form.keys()
+    properties = cl.getprops()
     for key in keys:
-        if not cl.properties.has_key(key):
+        if not properties.has_key(key):
             continue
-        proptype = cl.properties[key]
+        proptype = properties[key]
 
         # Get the form value. This value may be a MiniFieldStorage or a list
         # of MiniFieldStorages.
@@ -1103,31 +1108,42 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
             if not value:
                 # ignore empty password values
                 continue
+            if not form.has_key('%s:confirm'%key):
+                raise ValueError, 'Password and confirmation text do not match'
+            confirm = form['%s:confirm'%key]
+            if isinstance(confirm, type([])):
+                raise ValueError, 'You have submitted more than one value'\
+                    ' for the %s property'%key
+            if value != confirm.value:
+                raise ValueError, 'Password and confirmation text do not match'
             value = password.Password(value)
         elif isinstance(proptype, hyperdb.Date):
             if value:
                 value = date.Date(form[key].value.strip())
             else:
-                value = None
+                continue
         elif isinstance(proptype, hyperdb.Interval):
             if value:
                 value = date.Interval(form[key].value.strip())
             else:
-                value = None
+                continue
         elif isinstance(proptype, hyperdb.Link):
             # see if it's the "no selection" choice
             if value == '-1':
-                value = None
-            else:
-                # handle key values
-                link = cl.properties[key].classname
-                if not num_re.match(value):
-                    try:
-                        value = db.classes[link].lookup(value)
-                    except KeyError:
-                        raise ValueError, _('property "%(propname)s": '
-                            '%(value)s not a %(classname)s')%{'propname':key, 
-                            'value': value, 'classname': link}
+                continue
+            # handle key values
+            link = proptype.classname
+            if not num_re.match(value):
+                try:
+                    value = db.classes[link].lookup(value)
+                except KeyError:
+                    raise ValueError, _('property "%(propname)s": '
+                        '%(value)s not a %(classname)s')%{'propname':key, 
+                        'value': value, 'classname': link}
+                except TypeError, message:
+                    raise ValueError, _('you may only enter ID values '
+                        'for property "%(propname)s": %(message)s')%{
+                        'propname':key, 'message': message}
         elif isinstance(proptype, hyperdb.Multilink):
             if isinstance(value, type([])):
                 # it's a list of MiniFieldStorages
@@ -1136,7 +1152,7 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
                 # it's a MiniFieldStorage, but may be a comma-separated list
                 # of values
                 value = [i.strip() for i in value.value.split(',')]
-            link = cl.properties[key].classname
+            link = proptype.classname
             l = []
             for entry in map(str, value):
                 if entry == '': continue
@@ -1147,6 +1163,10 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
                         raise ValueError, _('property "%(propname)s": '
                             '"%(value)s" not an entry of %(classname)s')%{
                             'propname':key, 'value': entry, 'classname': link}
+                    except TypeError, message:
+                        raise ValueError, _('you may only enter ID values '
+                            'for property "%(propname)s": %(message)s')%{
+                            'propname':key, 'message': message}
                 l.append(entry)
             l.sort()
             value = l
@@ -1166,7 +1186,7 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
             except KeyError:
                 # this might be a new property for which there is no existing
                 # value
-                if not cl.properties.has_key(key): raise
+                if not properties.has_key(key): raise
 
             # if changed, set it
             if value != existing: