[web] Move request handling logic into cubicweb application. (closes #2200684)
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Thu, 15 Mar 2012 17:48:20 +0100
changeset 8312 6c2119509fac
parent 8311 76a44a0d7f4b
child 8313 386b6313de28
[web] Move request handling logic into cubicweb application. (closes #2200684) We improve http status handling in the process: ``application.publish`` have been renamed to ``application.handle`` to better reflect it's roles. The request object gain a status_out attribute to convey the HTTP status of the response. WSGI and etwist code have been updated. Exception gain status attribute
devtools/testlib.py
doc/book/en/devweb/request.rst
etwist/server.py
web/_exceptions.py
web/application.py
web/request.py
web/test/data/views.py
web/test/unittest_application.py
web/test/unittest_views_basecontrollers.py
web/test/unittest_views_staticcontrollers.py
wsgi/handler.py
--- a/devtools/testlib.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/devtools/testlib.py	Thu Mar 15 17:48:20 2012 +0100
@@ -608,8 +608,8 @@
         ctrl = self.vreg['controllers'].select('ajax', req)
         return ctrl.publish(), req
 
-    def app_publish(self, req, path='view'):
-        return self.app.publish(path, req)
+    def app_handle_request(self, req, path='view'):
+        return self.app.core_handle(req, path)
 
     def ctrl_publish(self, req, ctrl='edit'):
         """call the publish method of the edit controller"""
@@ -646,6 +646,20 @@
         ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
         return self.ctrl_publish(req, ctrlid)
 
+    @staticmethod
+    def _parse_location(req, location):
+        try:
+            path, params = location.split('?', 1)
+        except ValueError:
+            path = location
+            params = {}
+        else:
+            cleanup = lambda p: (p[0], unquote(p[1]))
+            params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
+        if path.startswith(req.base_url()): # may be relative
+            path = path[len(req.base_url()):]
+        return path, params
+
     def expect_redirect(self, callback, req):
         """call the given callback with req as argument, expecting to get a
         Redirect exception
@@ -653,25 +667,18 @@
         try:
             callback(req)
         except Redirect, ex:
-            try:
-                path, params = ex.location.split('?', 1)
-            except ValueError:
-                path = ex.location
-                params = {}
-            else:
-                cleanup = lambda p: (p[0], unquote(p[1]))
-                params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
-            if path.startswith(req.base_url()): # may be relative
-                path = path[len(req.base_url()):]
-            return path, params
+            return self._parse_location(req, ex.location)
         else:
             self.fail('expected a Redirect exception')
 
-    def expect_redirect_publish(self, req, path='edit'):
+    def expect_redirect_handle_request(self, req, path='edit'):
         """call the publish method of the application publisher, expecting to
         get a Redirect exception
         """
-        return self.expect_redirect(lambda x: self.app_publish(x, path), req)
+        result = self.app_handle_request(req, path)
+        self.assertTrue(300 <= req.status_out <400, req.status_out)
+        location = req.get_response_header('location')
+        return self._parse_location(req, location)
 
     def set_auth_mode(self, authmode, anonuser=None):
         self.set_option('auth-mode', authmode)
--- a/doc/book/en/devweb/request.rst	Thu Mar 15 17:42:31 2012 +0100
+++ b/doc/book/en/devweb/request.rst	Thu Mar 15 17:48:20 2012 +0100
@@ -99,6 +99,7 @@
     document.ready(...) or another ajax-friendly one-time trigger event
   * `add_header(header, values)`: adds the header/value pair to the
     current html headers
+  * `status_out`: control the HTTP status of the response
 
 * `And more...`
 
--- a/etwist/server.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/etwist/server.py	Thu Mar 15 17:48:20 2012 +0100
@@ -162,75 +162,23 @@
             origpath = origpath[6:]
             request.uri = request.uri[6:]
             https = True
-        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https)
-        if req.authmode == 'http':
-            # activate realm-based auth
-            realm = self.config['realm']
-            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
-        try:
-            self.appli.connect(req)
-        except Redirect, ex:
-            return self.redirect(request=req, location=ex.location)
-        if https and req.session.anonymous_session and self.config['https-deny-anonymous']:
-            # don't allow anonymous on https connection
-            return self.request_auth(request=req)
         if self.url_rewriter is not None:
             # XXX should occur before authentication?
-            try:
-                path = self.url_rewriter.rewrite(host, origpath, req)
-            except Redirect, ex:
-                return self.redirect(req, ex.location)
+            path = self.url_rewriter.rewrite(host, origpath, request)
             request.uri.replace(origpath, path, 1)
         else:
             path = origpath
+        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https)
         try:
-            result = self.appli.publish(path, req)
+            ### Try to generate the actual request content
+            content = self.appli.handle_request(req, path)
         except DirectResponse, ex:
             return ex.response
-        except StatusResponse, ex:
-            return HTTPResponse(stream=ex.content, code=ex.status,
-                                twisted_request=req._twreq,
-                                headers=req.headers_out)
-        except AuthenticationError:
-            return self.request_auth(request=req)
-        except LogOut, ex:
-            if self.config['auth-mode'] == 'cookie' and ex.url:
-                return self.redirect(request=req, location=ex.url)
-            # in http we have to request auth to flush current http auth
-            # information
-            return self.request_auth(request=req, loggedout=True)
-        except Redirect, ex:
-            return self.redirect(request=req, location=ex.location)
-        # request may be referenced by "onetime callback", so clear its entity
-        # cache to avoid memory usage
-        req.drop_entity_cache()
-        return HTTPResponse(twisted_request=req._twreq, code=http.OK,
-                            stream=result, headers=req.headers_out)
-
-    def redirect(self, request, location):
-        self.debug('redirecting to %s', str(location))
-        request.headers_out.setHeader('location', str(location))
-        # 303 See other
-        return HTTPResponse(twisted_request=request._twreq, code=303,
-                            headers=request.headers_out)
-
-    def request_auth(self, request, loggedout=False):
-        if self.https_url and request.base_url() != self.https_url:
-            return self.redirect(request, self.https_url + 'login')
-        if self.config['auth-mode'] == 'http':
-            code = http.UNAUTHORIZED
-        else:
-            code = http.FORBIDDEN
-        if loggedout:
-            if request.https:
-                request._base_url =  self.base_url
-                request.https = False
-            content = self.appli.loggedout_content(request)
-        else:
-            content = self.appli.need_login_content(request)
-        return HTTPResponse(twisted_request=request._twreq,
-                            stream=content, code=code,
-                            headers=request.headers_out)
+        # at last: create twisted object
+        return HTTPResponse(code    = req.status_out,
+                            headers = req.headers_out,
+                            stream  = content,
+                            twisted_request=req._twreq)
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining
--- a/web/_exceptions.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/_exceptions.py	Thu Mar 15 17:48:20 2012 +0100
@@ -20,59 +20,90 @@
 
 __docformat__ = "restructuredtext en"
 
+import httplib
+
 from cubicweb._exceptions import *
 from cubicweb.utils import json_dumps
 
+
+class DirectResponse(Exception):
+    """Used to supply a twitted HTTP Response directly"""
+    def __init__(self, response):
+        self.response = response
+
+class InvalidSession(CubicWebException):
+    """raised when a session id is found but associated session is not found or
+    invalid"""
+
+# Publish related exception
+
 class PublishException(CubicWebException):
     """base class for publishing related exception"""
 
+    def __init__(self, *args, **kwargs):
+        self.status = kwargs.pop('status', httplib.OK)
+        super(PublishException, self).__init__(*args, **kwargs)
+
+class LogOut(PublishException):
+    """raised to ask for deauthentication of a logged in user"""
+    def __init__(self, url=None):
+        super(LogOut, self).__init__()
+        self.url = url
+
+class Redirect(PublishException):
+    """raised to redirect the http request"""
+    def __init__(self, location, status=httplib.SEE_OTHER):
+        super(Redirect, self).__init__(status=status)
+        self.location = location
+
+class StatusResponse(PublishException):
+
+    def __init__(self, status, content=''):
+        super(StatusResponse, self).__init__(status=status)
+        self.content = content
+
+    def __repr__(self):
+        return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
+        self.url = url
+
+# Publish related error
+
 class RequestError(PublishException):
     """raised when a request can't be served because of a bad input"""
 
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(RequestError, self).__init__(*args, **kwargs)
+
+
 class NothingToEdit(RequestError):
     """raised when an edit request doesn't specify any eid to edit"""
 
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(NothingToEdit, self).__init__(*args, **kwargs)
+
 class ProcessFormError(RequestError):
     """raised when posted data can't be processed by the corresponding field
     """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.BAD_REQUEST)
+        super(ProcessFormError, self).__init__(*args, **kwargs)
 
 class NotFound(RequestError):
-    """raised when a 404 error should be returned"""
-
-class Redirect(PublishException):
-    """raised to redirect the http request"""
-    def __init__(self, location):
-        self.location = location
-
-class DirectResponse(Exception):
-    def __init__(self, response):
-        self.response = response
+    """raised when something was not found. In most case,
+       a 404 error should be returned"""
 
-class StatusResponse(Exception):
-    def __init__(self, status, content=''):
-        self.status = int(status)
-        self.content = content
-
-    def __repr__(self):
-        return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
-
-class InvalidSession(CubicWebException):
-    """raised when a session id is found but associated session is not found or
-    invalid
-    """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('status', httplib.NOT_FOUND)
+        super(NotFound, self).__init__(*args, **kwargs)
 
 class RemoteCallFailed(RequestError):
     """raised when a json remote call fails
     """
-    def __init__(self, reason=''):
-        super(RemoteCallFailed, self).__init__()
+    def __init__(self, reason='', status=httplib.INTERNAL_SERVER_ERROR):
+        super(RemoteCallFailed, self).__init__(status=status)
         self.reason = reason
 
     def dumps(self):
         return json_dumps({'reason': self.reason})
-
-class LogOut(PublishException):
-    """raised to ask for deauthentication of a logged in user"""
-    def __init__(self, url):
-        super(LogOut, self).__init__()
-        self.url = url
--- a/web/application.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/application.py	Thu Mar 15 17:48:20 2012 +0100
@@ -24,6 +24,9 @@
 import sys
 from time import clock, time
 from contextlib import contextmanager
+from warnings import warn
+
+import httplib
 
 from logilab.common.deprecation import deprecated
 
@@ -39,6 +42,8 @@
     StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
     RemoteCallFailed, InvalidSession, RequestError)
 
+from cubicweb.web.request import CubicWebRequestBase
+
 # make session manager available through a global variable so the debug view can
 # print information about web session
 SESSION_MANAGER = None
@@ -288,11 +293,11 @@
         if config['query-log-file']:
             from threading import Lock
             self._query_log = open(config['query-log-file'], 'a')
-            self.publish = self.log_publish
+            self.handle_request = self.log_handle_request
             self._logfile_lock = Lock()
         else:
             self._query_log = None
-            self.publish = self.main_publish
+            self.handle_request = self.main_handle_request
         # instantiate session and url resolving helpers
         self.session_handler = session_handler_fact(self)
         self.set_urlresolver()
@@ -311,12 +316,12 @@
 
     # publish methods #########################################################
 
-    def log_publish(self, path, req):
+    def log_handle_request(self, req, path):
         """wrapper around _publish to log all queries executed for a given
         accessed path
         """
         try:
-            return self.main_publish(path, req)
+            return self.main_handle_request(req, path)
         finally:
             cnx = req.cnx
             if cnx:
@@ -332,7 +337,78 @@
                     except Exception:
                         self.exception('error while logging queries')
 
-    def main_publish(self, path, req):
+
+
+    def main_handle_request(self, req, path):
+        if not isinstance(req, CubicWebRequestBase):
+            warn('[3.15] Application entry poin arguments are now (req, path) '
+                 'not (path, req)', DeprecationWarning, 2)
+            req, path = path, req
+        if req.authmode == 'http':
+            # activate realm-based auth
+            realm = self.vreg.config['realm']
+            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
+        try:
+            self.connect(req)
+            # DENY https acces for anonymous_user
+            if (req.https
+                and req.session.anonymous_session
+                and self.vreg.config['https-deny-anonymous']):
+                # don't allow anonymous on https connection
+                raise AuthenticationError()
+            content = ''
+            # nested try to allow LogOut to delegate logic to AuthenticationError
+            # handler
+            try:
+                ### Try to generate the actual request content
+                content = self.core_handle(req, path)
+            # Handle user log-out
+            except LogOut, ex:
+                # When authentification is handled by cookie the code that
+                # raised LogOut must has invalidated the cookie. We can just
+                # reload the original url without authentification
+                if self.vreg.config['auth-mode'] == 'cookie' and ex.url:
+                    req.headers_out.setHeader('location', str(ex.url))
+                if ex.status is not None:
+                    req.status_out = httplib.SEE_OTHER
+                # When the authentification is handled by http we must
+                # explicitly ask for authentification to flush current http
+                # authentification information
+                else:
+                    # Render "logged out" content.
+                    # assignement to ``content`` prevent standard
+                    # AuthenticationError code to overwrite it.
+                    content = self.loggedout_content(req)
+                    # let the explicitly reset http credential
+                    raise AuthenticationError()
+        # Wrong, absent or Reseted credential
+        except AuthenticationError:
+            # If there is an https url configured and
+            # the request do not used https, redirect to login form
+            https_url = self.vreg.config['https-url']
+            if https_url and req.base_url() != https_url:
+                req.status_out = httplib.SEE_OTHER
+                req.headers_out.setHeader('location', https_url + 'login')
+            else:
+                # We assume here that in http auth mode the user *May* provide
+                # Authentification Credential if asked kindly.
+                if self.vreg.config['auth-mode'] == 'http':
+                    req.status_out = httplib.UNAUTHORIZED
+                # In the other case (coky auth) we assume that there is no way
+                # for the user to provide them...
+                # XXX But WHY ?
+                else:
+                    req.status_out = httplib.FORBIDDEN
+                # If previous error handling already generated a custom content
+                # do not overwrite it. This is used by LogOut Except
+                # XXX ensure we don't actually serve content
+                if not content:
+                    content = self.need_login_content(req)
+        return content
+
+
+
+    def core_handle(self, req, path):
         """method called by the main publisher to process <path>
 
         should return a string containing the resulting page or raise a
@@ -355,6 +431,7 @@
         tstart = clock()
         commited = False
         try:
+            ### standard processing of the request
             try:
                 ctrlid, rset = self.url_resolver.process(req, path)
                 try:
@@ -364,76 +441,69 @@
                     raise Unauthorized(req._('not authorized'))
                 req.update_search_state()
                 result = controller.publish(rset=rset)
-                if req.cnx:
-                    # no req.cnx if anonymous aren't allowed and we are
-                    # displaying some anonymous enabled view such as the cookie
-                    # authentication form
-                    txuuid = req.cnx.commit()
-                    if txuuid is not None:
-                        req.data['last_undoable_transaction'] = txuuid
-                    commited = True
-            except (StatusResponse, DirectResponse):
-                if req.cnx:
-                    req.cnx.commit()
-                raise
-            except (AuthenticationError, LogOut):
-                raise
-            except Redirect:
-                # redirect is raised by edit controller when everything went fine,
-                # so try to commit
-                try:
-                    if req.cnx:
-                        txuuid = req.cnx.commit()
-                        if txuuid is not None:
-                            req.data['last_undoable_transaction'] = txuuid
-                except ValidationError, ex:
-                    self.validation_error_handler(req, ex)
-                except Unauthorized, ex:
-                    req.data['errmsg'] = req._('You\'re not authorized to access this page. '
-                                               'If you think you should, please contact the site administrator.')
-                    self.error_handler(req, ex, tb=False)
-                except Exception, ex:
-                    self.error_handler(req, ex, tb=True)
-                else:
-                    self.add_undo_link_to_msg(req)
-                    # delete validation errors which may have been previously set
-                    if '__errorurl' in req.form:
-                        req.session.data.pop(req.form['__errorurl'], None)
-                    raise
-            except RemoteCallFailed, ex:
-                req.set_header('content-type', 'application/json')
-                raise StatusResponse(500, ex.dumps())
-            except NotFound:
-                raise StatusResponse(404, self.notfound_content(req))
-            except ValidationError, ex:
-                self.validation_error_handler(req, ex)
-            except Unauthorized, ex:
-                self.error_handler(req, ex, tb=False, code=403)
-            except (BadRQLQuery, RequestError), ex:
-                self.error_handler(req, ex, tb=False)
-            except BaseException, ex:
-                self.error_handler(req, ex, tb=True)
-            except:
-                self.critical('Catch all triggered!!!')
-                self.exception('this is what happened')
-                result = 'oops'
+            except StatusResponse, ex:
+                warn('StatusResponse is deprecated use req.status_out',
+                     DeprecationWarning)
+                result = ex.content
+                req.status_out = ex.status
+            except Redirect, ex:
+                # handle redirect
+                # - comply to ex status
+                # - set header field
+                #
+                # Redirect Maybe be is raised by edit controller when
+                # everything went fine, so try to commit
+                self.debug('redirecting to %s', str(ex.location))
+                req.headers_out.setHeader('location', str(ex.location))
+                assert 300<= ex.status < 400
+                req.status_out = ex.status
+                result = ''
+            if req.cnx:
+                txuuid = req.cnx.commit()
+                commited = True
+                if txuuid is not None:
+                    req.data['last_undoable_transaction'] = txuuid
+        ### error case
+        except NotFound, ex:
+            result = self.notfound_content(req)
+            req.status_out = ex.status
+        except ValidationError, ex:
+            req.status_out = httplib.CONFLICT
+            result = self.validation_error_handler(req, ex)
+        except RemoteCallFailed, ex:
+            result = self.ajax_error_handler(req, ex)
+        except Unauthorized, ex:
+            req.data['errmsg'] = req._('You\'re not authorized to access this page. '
+                                       'If you think you should, please contact the site administrator.')
+            req.status_out = httplib.UNAUTHORIZED
+            result = self.error_handler(req, ex, tb=False)
+        except (BadRQLQuery, RequestError), ex:
+            result = self.error_handler(req, ex, tb=False)
+        ### pass through exception
+        except DirectResponse:
+            if req.cnx:
+                req.cnx.commit()
+            raise
+        except (AuthenticationError, LogOut):
+            # the rollback is handled in the finally
+            raise
+        ### Last defence line
+        except BaseException, ex:
+            result = self.error_handler(req, ex, tb=True)
         finally:
             if req.cnx and not commited:
                 try:
                     req.cnx.rollback()
                 except Exception:
                     pass # ignore rollback error at this point
+            # request may be referenced by "onetime callback", so clear its entity
+            # cache to avoid memory usage
+            req.drop_entity_cache()
         self.add_undo_link_to_msg(req)
         self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
         return result
 
-    def add_undo_link_to_msg(self, req):
-        txuuid = req.data.get('last_undoable_transaction')
-        if txuuid is not None:
-            msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
-            req.build_url('undo', txuuid=txuuid), req._('undo'))
-            req.append_to_redirect_message(msg)
-
+    ### Error handler
     def validation_error_handler(self, req, ex):
         ex.errors = dict((k, v) for k, v in ex.errors.items())
         if '__errorurl' in req.form:
@@ -446,10 +516,13 @@
             # session key is 'url + #<form dom id', though we usually don't want
             # the browser to move to the form since it hides the global
             # messages.
-            raise Redirect(req.form['__errorurl'].rsplit('#', 1)[0])
-        self.error_handler(req, ex, tb=False)
+            location = req.form['__errorurl'].rsplit('#', 1)[0]
+            req.headers_out.setHeader('location', str(location))
+            req.status_out = httplib.SEE_OTHER
+            return ''
+        return self.error_handler(req, ex, tb=False)
 
-    def error_handler(self, req, ex, tb=False, code=500):
+    def error_handler(self, req, ex, tb=False):
         excinfo = sys.exc_info()
         self.exception(repr(ex))
         req.set_header('Cache-Control', 'no-cache')
@@ -457,7 +530,7 @@
         req.reset_message()
         req.reset_headers()
         if req.ajax_request:
-            raise RemoteCallFailed(unicode(ex))
+            return ajax_error_handler(req, ex)
         try:
             req.data['ex'] = ex
             if tb:
@@ -468,7 +541,29 @@
             content = self.vreg['views'].main_template(req, template, view=errview)
         except Exception:
             content = self.vreg['views'].main_template(req, 'error-template')
-        raise StatusResponse(code, content)
+        if getattr(ex, 'status', None) is not None:
+            req.status_out = ex.status
+        return content
+
+    def add_undo_link_to_msg(self, req):
+        txuuid = req.data.get('last_undoable_transaction')
+        if txuuid is not None:
+            msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
+            req.build_url('undo', txuuid=txuuid), req._('undo'))
+            req.append_to_redirect_message(msg)
+
+
+
+    def ajax_error_handler(self, req, ex):
+        req.set_header('content-type', 'application/json')
+        status = ex.status
+        if status is None:
+            status = httplib.INTERNAL_SERVER_ERROR
+        json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+        req.status_out = status
+        return json_dumper()
+
+    # special case handling
 
     def need_login_content(self, req):
         return self.vreg['views'].main_template(req, 'login')
@@ -482,6 +577,8 @@
         template = self.main_template_id(req)
         return self.vreg['views'].main_template(req, template, view=view)
 
+    # template stuff
+
     def main_template_id(self, req):
         template = req.form.get('__template', req.property_value('ui.main-template'))
         if template not in self.vreg['views']:
--- a/web/request.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/request.py	Thu Mar 15 17:48:20 2012 +0100
@@ -122,6 +122,8 @@
         # prepare output header
         #: Header used for the final response
         self.headers_out = Headers()
+        #: HTTP status use by the final response
+        self.status_out  = 200
 
     def _set_pageid(self):
         """initialize self.pageid
--- a/web/test/data/views.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/test/data/views.py	Thu Mar 15 17:48:20 2012 +0100
@@ -21,12 +21,12 @@
 from cubicweb.web import Redirect
 from cubicweb.web.application import CubicWebPublisher
 
-# proof of concept : monkey patch publish method so that if we are in an
+# proof of concept : monkey patch handle method so that if we are in an
 # anonymous session and __fblogin is found is req.form, the user with the
 # given login is created if necessary and then a session is opened for that
 # user
 # NOTE: this require "cookie" authentication mode
-def auto_login_publish(self, path, req):
+def auto_login_handle_request(self, req, path):
     if (not req.cnx or req.cnx.anonymous_connection) and req.form.get('__fblogin'):
         login = password = req.form.pop('__fblogin')
         self.repo.register_user(login, password)
@@ -40,7 +40,7 @@
         except Redirect:
             pass
         assert req.user.login == login
-    return orig_publish(self, path, req)
+    return orig_handle(self, req, path)
 
-orig_publish = CubicWebPublisher.main_publish
-CubicWebPublisher.main_publish = auto_login_publish
+orig_handle = CubicWebPublisher.main_handle_request
+CubicWebPublisher.main_handle_request = auto_login_handle_request
--- a/web/test/unittest_application.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/test/unittest_application.py	Thu Mar 15 17:48:20 2012 +0100
@@ -184,12 +184,12 @@
 
     def test_nonregr_publish1(self):
         req = self.request(u'CWEType X WHERE X final FALSE, X meta FALSE')
-        self.app.publish('view', req)
+        self.app.handle_request(req, 'view')
 
     def test_nonregr_publish2(self):
         req = self.request(u'Any count(N) WHERE N todo_by U, N is Note, U eid %s'
                            % self.user().eid)
-        self.app.publish('view', req)
+        self.app.handle_request(req, 'view')
 
     def test_publish_validation_error(self):
         req = self.request()
@@ -202,7 +202,7 @@
              # just a sample, missing some necessary information for real life
             '__errorurl': 'view?vid=edition...'
             }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         eidmap = forminfo['eidmap']
         self.assertEqual(eidmap, {})
@@ -232,7 +232,7 @@
                     # necessary to get validation error handling
                     '__errorurl': 'view?vid=edition...',
                     }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         self.assertEqual(set(forminfo['eidmap']), set('XY'))
         self.assertEqual(forminfo['eidmap']['X'], None)
@@ -261,7 +261,7 @@
                     # necessary to get validation error handling
                     '__errorurl': 'view?vid=edition...',
                     }
-        path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         forminfo = req.session.data['view?vid=edition...']
         self.assertEqual(set(forminfo['eidmap']), set('XY'))
         self.assertIsInstance(forminfo['eidmap']['X'], int)
@@ -274,7 +274,7 @@
 
     def _test_cleaned(self, kwargs, injected, cleaned):
         req = self.request(**kwargs)
-        page = self.app.publish('view', req)
+        page = self.app.handle_request(req, 'view')
         self.assertFalse(injected in page, (kwargs, injected))
         self.assertTrue(cleaned in page, (kwargs, cleaned))
 
@@ -315,7 +315,7 @@
         req = self.request()
         origcnx = req.cnx
         req.form['__fblogin'] = u'turlututu'
-        page = self.app_publish(req)
+        page = self.app.handle_request(req, '')
         self.assertFalse(req.cnx is origcnx)
         self.assertEqual(req.user.login, 'turlututu')
         self.assertTrue('turlututu' in page, page)
@@ -326,19 +326,19 @@
     def test_http_auth_no_anon(self):
         req, origsession = self.init_authentication('http')
         self.assertAuthFailure(req)
-        self.assertRaises(AuthenticationError, self.app_publish, req, 'login')
+        self.assertRaises(AuthenticationError, self.app_handle_request, req, 'login')
         self.assertEqual(req.cnx, None)
         authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
         req.set_request_header('Authorization', 'basic %s' % authstr)
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_no_anon(self):
         req, origsession = self.init_authentication('cookie')
         self.assertAuthFailure(req)
         try:
-            form = self.app_publish(req, 'login')
+            form = self.app_handle_request(req, 'login')
         except Redirect, redir:
             self.fail('anonymous user should get login form')
         self.assertTrue('__login' in form)
@@ -347,7 +347,7 @@
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_login_by_email(self):
@@ -367,7 +367,7 @@
         req.form['__login'] = address
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def _reset_cookie(self, req):
@@ -407,7 +407,7 @@
         authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
         req.set_request_header('Authorization', 'basic %s' % authstr)
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_cookie_auth_anon_allowed(self):
@@ -419,7 +419,7 @@
         req.form['__login'] = self.admlogin
         req.form['__password'] = self.admpassword
         self.assertAuthSuccess(req, origsession)
-        self.assertRaises(LogOut, self.app_publish, req, 'logout')
+        self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
         self.assertEqual(len(self.open_sessions), 0)
 
     def test_anonymized_request(self):
@@ -438,7 +438,7 @@
         req = self.request()
         # expect a rset with None in [0][0]
         req.form['rql'] = 'rql:Any OV1, X WHERE X custom_workflow OV1?'
-        self.app_publish(req)
+        self.app_handle_request(req)
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_views_basecontrollers.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Thu Mar 15 17:48:20 2012 +0100
@@ -96,7 +96,7 @@
             'firstname-subject:'+eid:   u'Sylvain',
             'in_group-subject:'+eid:  groups,
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
         self.assertEqual(e.firstname, u'Sylvain')
         self.assertEqual(e.surname, u'Th\xe9nault')
@@ -115,7 +115,7 @@
             'upassword-subject:'+eid: 'tournicoton',
             'upassword-subject-confirm:'+eid: 'tournicoton',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         cnx.commit() # commit to check we don't get late validation error for instance
         self.assertEqual(path, 'cwuser/user')
         self.assertFalse('vid' in params)
@@ -136,7 +136,7 @@
             'firstname-subject:'+eid: u'Th\xe9nault',
             'surname-subject:'+eid:   u'Sylvain',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
         self.assertEqual(e.login, user.login)
         self.assertEqual(e.firstname, u'Th\xe9nault')
@@ -162,7 +162,7 @@
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': 'X',
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created person
         self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Mascio"').get_entity(0, 0)
@@ -184,7 +184,7 @@
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': peid,
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created person
         self.assertEqual(path, 'cwuser/adim')
         e = self.execute('Any P WHERE P surname "Di Masci"').get_entity(0, 0)
@@ -204,7 +204,7 @@
                     'address-subject:'+emaileid: u'adim@logilab.fr',
                     'use_email-object:'+emaileid: peid,
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         email.cw_clear_all_caches()
         self.assertEqual(email.address, 'adim@logilab.fr')
 
@@ -267,7 +267,7 @@
                     'amount-subject:X': u'10',
                     'described_by_test-subject:X': u(feid),
                     }
-        self.expect_redirect_publish(req, 'edit')
+        self.expect_redirect_handle_request(req, 'edit')
         # should be redirected on the created
         #eid = params['rql'].split()[-1]
         e = self.execute('Salesterm X').get_entity(0, 0)
@@ -279,7 +279,7 @@
         user = self.user()
         req = self.request(**req_form(user))
         req.session.data['pending_insert'] = set([(user.eid, 'in_group', tmpgroup.eid)])
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertItemsEqual(usergroups, ['managers', 'test'])
@@ -298,7 +298,7 @@
         # now try to delete the relation
         req = self.request(**req_form(user))
         req.session.data['pending_delete'] = set([(user.eid, 'in_group', groupeid)])
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         usergroups = [gname for gname, in
                       self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
         self.assertItemsEqual(usergroups, ['managers'])
@@ -318,7 +318,7 @@
             '__form_id': 'edition',
             '__action_apply': '',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertTrue(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         self.assertEqual(params['vid'], 'edition')
@@ -340,7 +340,7 @@
             '__redirectparams': 'toto=tutu&tata=titi',
             '__form_id': 'edition',
             }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'view')
         self.assertEqual(params['rql'], redirectrql)
         self.assertEqual(params['vid'], 'primary')
@@ -352,7 +352,7 @@
         eid = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
         req.form = {'eid': u(eid), '__type:%s'%eid: 'BlogEntry',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'blogentry')
         self.assertIn('_cwmsgid', params)
         eid = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
@@ -362,7 +362,7 @@
         req = req
         req.form = {'eid': u(eid), '__type:%s'%eid: 'EmailAddress',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'cwuser/admin')
         self.assertIn('_cwmsgid', params)
         eid1 = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
@@ -372,7 +372,7 @@
                     '__type:%s'%eid1: 'BlogEntry',
                     '__type:%s'%eid2: 'EmailAddress',
                     '__action_delete': ''}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'view')
         self.assertIn('_cwmsgid', params)
 
@@ -388,7 +388,7 @@
                     'title-subject:X': u'entry1-copy',
                     'content-subject:X': u'content1',
                     }
-        self.expect_redirect_publish(req, 'edit')
+        self.expect_redirect_handle_request(req, 'edit')
         blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
         self.assertEqual(blogentry2.entry_of[0].eid, blog.eid)
 
@@ -406,7 +406,7 @@
                         'title-subject:X': u'entry1-copy',
                         'content-subject:X': u'content1',
                         }
-            self.expect_redirect_publish(req, 'edit')
+            self.expect_redirect_handle_request(req, 'edit')
             blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
             # entry_of should not be copied
             self.assertEqual(len(blogentry2.entry_of), 0)
@@ -432,7 +432,7 @@
             'read_permission-subject:'+cwetypeeid:  groups,
             }
         try:
-            path, params = self.expect_redirect_publish(req, 'edit')
+            path, params = self.expect_redirect_handle_request(req, 'edit')
             e = self.execute('Any X WHERE X eid %(x)s', {'x': cwetypeeid}).get_entity(0, 0)
             self.assertEqual(e.name, 'CWEType')
             self.assertEqual(sorted(g.eid for g in e.read_permission), groupeids)
@@ -452,7 +452,7 @@
             '__type:A': 'BlogEntry', '_cw_entity_fields:A': 'title-subject,content-subject',
             'title-subject:A': u'"13:03:40"',
             'content-subject:A': u'"13:03:43"',}
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertTrue(path.startswith('blogentry/'))
         eid = path.split('/')[1]
         e = self.execute('Any C, T WHERE C eid %(x)s, C content T', {'x': eid}).get_entity(0, 0)
@@ -490,7 +490,7 @@
                     'login-subject:X': u'toto',
                     'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                     }
-        path, params = self.expect_redirect_publish(req, 'edit')
+        path, params = self.expect_redirect_handle_request(req, 'edit')
         self.assertEqual(path, 'cwuser/toto')
         e = self.execute('Any X WHERE X is CWUser, X login "toto"').get_entity(0, 0)
         self.assertEqual(e.login, 'toto')
@@ -520,12 +520,12 @@
             #    which fires a Redirect
             # 2/ When re-publishing the copy form, the publisher implicitly commits
             try:
-                self.app_publish(req, 'edit')
+                self.app_handle_request(req, 'edit')
             except Redirect:
                 req = self.request()
                 req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
                 req.form['vid'] = 'copy'
-                self.app_publish(req, 'view')
+                self.app_handle_request(req, 'view')
             rset = self.execute('CWUser P WHERE P surname "Boom"')
             self.assertEqual(len(rset), 0)
         finally:
--- a/web/test/unittest_views_staticcontrollers.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/test/unittest_views_staticcontrollers.py	Thu Mar 15 17:48:20 2012 +0100
@@ -26,7 +26,7 @@
         head = HTMLHead(req)
         url = head.concat_urls([req.data_url(js_file) for js_file in js_files])[len(req.base_url()):]
         req._url = url
-        return self.app_publish(req, url)
+        return self.app_handle_request(req, url), req
 
     def expected_content(self, js_files):
         content = u''
@@ -39,13 +39,8 @@
 
     def test_cache(self):
         js_files = ('cubicweb.ajax.js', 'jquery.js')
-        try:
-            result = self._publish_js_files(js_files)
-        except StatusResponse, exc:
-            if exc.status == 404:
-                self.fail('unable to serve cubicweb.js+jquery.js')
-            # let the exception propagate for any other status (e.g 500)
-            raise
+        result, req = self._publish_js_files(js_files)
+        self.assertNotEqual(404, req.status_out)
         # check result content
         self.assertEqual(result, self.expected_content(js_files))
         # make sure we kept a cached version on filesystem
@@ -59,23 +54,16 @@
         # in debug mode, an error is raised
         self.config.debugmode = True
         try:
-            result = self._publish_js_files(js_files)
-            self.fail('invalid concat js should return a 404 in debug mode')
-        except StatusResponse, exc:
-            if exc.status != 404:
-                self.fail('invalid concat js should return a 404 in debug mode')
+            result, req = self._publish_js_files(js_files)
+            #print result
+            self.assertEqual(404, req.status_out)
         finally:
             self.config.debugmode = False
 
     def test_invalid_file_in_production_mode(self):
         js_files = ('cubicweb.ajax.js', 'dummy.js')
-        try:
-            result = self._publish_js_files(js_files)
-        except StatusResponse, exc:
-            if exc.status == 404:
-                self.fail('invalid concat js should NOT return a 404 in debug mode')
-            # let the exception propagate for any other status (e.g 500)
-            raise
+        result, req = self._publish_js_files(js_files)
+        self.assertNotEqual(404, req.status_out)
         # check result content
         self.assertEqual(result, self.expected_content(js_files))
 
--- a/wsgi/handler.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/wsgi/handler.py	Thu Mar 15 17:48:20 2012 +0100
@@ -112,51 +112,15 @@
 
     def _render(self, req):
         """this function performs the actual rendering
-        XXX missing: https handling, url rewriting, cache management,
-                     authentication
         """
         if self.base_url is None:
             self.base_url = self.config._base_url = req.base_url()
-        # XXX https handling needs to be implemented
-        if req.authmode == 'http':
-            # activate realm-based auth
-            realm = self.config['realm']
-            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
         try:
-            self.appli.connect(req)
-        except Redirect, ex:
-            return self.redirect(req, ex.location)
-        try:
-            result = self.appli.publish(path, req)
+            path = req.path
+            result = self.appli.handle_request(req, path)
         except DirectResponse, ex:
-            return WSGIResponse(200, req, ex.response)
-        except StatusResponse, ex:
-            return WSGIResponse(ex.status, req, ex.content)
-        except AuthenticationError:  # must be before AuthenticationError
-            return self.request_auth(req)
-        except LogOut:
-            if self.config['auth-mode'] == 'cookie':
-                # in cookie mode redirecting to the index view is enough :
-                # either anonymous connection is allowed and the page will
-                # be displayed or we'll be redirected to the login form
-                msg = req._('you have been logged out')
-#                 if req.https:
-#                     req._base_url =  self.base_url
-#                     req.https = False
-                url = req.build_url('view', vid='index', __message=msg)
-                return self.redirect(req, url)
-            else:
-                # in http we have to request auth to flush current http auth
-                # information
-                return self.request_auth(req, loggedout=True)
-        except Redirect, ex:
-            return self.redirect(req, ex.location)
-        if not result:
-            # no result, something went wrong...
-            self.error('no data (%s)', req)
-            # 500 Internal server error
-            return self.redirect(req, req.build_url('error'))
-        return WSGIResponse(200, req, result)
+            return ex.response
+        return WSGIResponse(req.status_out, req, result)
 
 
     def __call__(self, environ, start_response):
@@ -166,29 +130,7 @@
         start_response(response.status, response.headers)
         return response.body
 
-    def redirect(self, req, location):
-        """convenience function which builds a redirect WSGIResponse"""
-        self.debug('redirecting to %s', location)
-        req.set_header('location', str(location))
-        return WSGIResponse(303, req)
 
-    def request_auth(self, req, loggedout=False):
-        """returns the appropriate WSGIResponse to require the user to log in
-        """
-#         if self.https_url and req.base_url() != self.https_url:
-#             return self.redirect(self.https_url + 'login')
-        if self.config['auth-mode'] == 'http':
-            code = 401 # UNAUTHORIZED
-        else:
-            code = 403 # FORBIDDEN
-        if loggedout:
-#             if req.https:
-#                 req._base_url =  self.base_url
-#                 req.https = False
-            content = self.appli.loggedout_content(req)
-        else:
-            content = self.appli.need_login_content(req)
-        return WSGIResponse(code, req, content)
 
     # these are overridden by set_log_methods below
     # only defining here to prevent pylint from complaining