[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
--- 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