From ff1213e8f2aed987108ba57aed517c033491b1aa Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 14 Apr 2010 02:49:19 +0000 Subject: Add "exception views" work contributed primarily by Andrey Popp by merging the "phash" branch. --- docs/narr/configuration.rst | 5 +- docs/narr/hooks.rst | 107 +++++++++++++-------- docs/narr/urldispatch.rst | 3 +- docs/narr/views.rst | 91 +++++++++++++++++- docs/tutorials/bfgwiki/authorization.rst | 2 +- .../src/authorization/tutorial/configure.zcml | 5 +- docs/tutorials/bfgwiki2/authorization.rst | 6 +- .../src/authorization/tutorial/configure.zcml | 5 +- docs/zcml/forbidden.rst | 16 ++- docs/zcml/notfound.rst | 16 ++- 10 files changed, 197 insertions(+), 59 deletions(-) (limited to 'docs') diff --git a/docs/narr/configuration.rst b/docs/narr/configuration.rst index 66ecd486c..a3336e735 100644 --- a/docs/narr/configuration.rst +++ b/docs/narr/configuration.rst @@ -198,9 +198,8 @@ effectively a "macro" which calls the behalf. The ```` tag is an example of a :mod:`repoze.bfg` declaration -tag. Other such tags include ````, ````, ````, -````, and others. Each of these tags is effectively a -"macro" which calls methods of a +tag. Other such tags include ```` and ````. Each of +these tags is effectively a "macro" which calls methods of a :class:`repoze.bfg.configuration.Configurator` object on your behalf. Essentially, using a :term:`ZCML` file and loading it from the diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 9410f3b79..678b9dbc3 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -3,8 +3,8 @@ Using Hooks =========== -"Hooks" can be used to influence the behavior of the -:mod:`repoze.bfg` framework in various ways. +"Hooks" can be used to influence the behavior of the :mod:`repoze.bfg` +framework in various ways. .. index:: single: not found view @@ -15,23 +15,29 @@ Changing the Not Found View --------------------------- When :mod:`repoze.bfg` can't map a URL to view code, it invokes a -:term:`not found view`, which is a :term:`view callable`. The view it -invokes can be customized through application configuration. This -view can be configured via :term:`imperative configuration` or -:term:`ZCML`. +:term:`not found view`, which is a :term:`view callable`. A default +notfound view exists. The default not found view can be overridden +through application configuration. This override can be done via +:term:`imperative configuration` or :term:`ZCML`. + +The :term:`not found view` callable is a view callable like any other. +The :term:`view configuration` which causes it to be a "not found" +view consists only of naming the :exc:`repoze.bfg.exceptions.NotFound` +class as the ``context`` of the view configuration. .. topic:: Using Imperative Configuration If your application uses :term:`imperative configuration`, you can replace the Not Found view by using the - :meth:`repoze.bfg.configuration.Configurator.set_notfound_view` - method: + :meth:`repoze.bfg.configuration.Configurator.add_view` method to + register an "exception view": .. code-block:: python :linenos: - import helloworld.views - config.set_notfound_view(helloworld.views.notfound_view) + from repoze.bfg.exceptions import NotFound + from helloworld.views import notfound_view + config.add_view(notfound_view, context=NotFound) Replace ``helloworld.views.notfound_view`` with a reference to the Python :term:`view callable` you want to use to represent the Not @@ -46,16 +52,22 @@ view can be configured via :term:`imperative configuration` or .. code-block:: xml :linenos: - + Replace ``helloworld.views.notfound_view`` with the Python dotted name to the notfound view you want to use. - Other attributes of the ``notfound`` directive are documented at - :ref:`notfound_directive`. +Like any other view, the notfound view must accept at least a +``request`` parameter, or both ``context`` and ``request``. The +``request`` is the current :term:`request` representing the denied +action. The ``context`` (if used in the call signature) will be the +instance of the :exc:`repoze.bfg.exceptions.NotFound` exception that +caused the view to be called. -Here's some sample code that implements a minimal NotFound view: +Here's some sample code that implements a minimal NotFound view +callable: .. code-block:: python :linenos: @@ -65,13 +77,21 @@ Here's some sample code that implements a minimal NotFound view: def notfound_view(request): return HTTPNotFound() -.. note:: When a NotFound view is invoked, it is passed a - :term:`request`. The ``environ`` attribute of the request is the - WSGI environment. Within the WSGI environ will be a key named - ``repoze.bfg.message`` that has a value explaining why the not - found error was raised. This error will be different when the - ``debug_notfound`` environment setting is true than it is when it - is false. +.. note:: When a NotFound view callable is invoked, it is passed a + :term:`request`. The ``exception`` attribute of the request will + be an instance of the :exc:`repoze.bfg.exceptions.NotFound` + exception that caused the not found view to be called. The value + of ``request.exception.args[0]`` will be a value explaining why the + not found error was raised. This message will be different when + the ``debug_notfound`` environment setting is true than it is when + it is false. + +.. warning:: When a NotFound view callable accepts an argument list as + described in :ref:`request_and_context_view_definitions`, the + ``context`` passed as the first argument to the view callable will + be the :exc:`repoze.bfg.exceptions.NotFound` exception instance. + If available, the *model* context will still be available as + ``request.context``. .. index:: single: forbidden view @@ -84,21 +104,28 @@ Changing the Forbidden View When :mod:`repoze.bfg` can't authorize execution of a view based on the :term:`authorization policy` in use, it invokes a :term:`forbidden view`. The default forbidden response has a 401 status code and is -very plain, but it can be overridden as necessary using either -:term:`imperative configuration` or :term:`ZCML`: +very plain, but the view which generates it can be overridden as +necessary using either :term:`imperative configuration` or +:term:`ZCML`: + +The :term:`forbidden view` callable is a view callable like any other. +The :term:`view configuration` which causes it to be a "not found" +view consists only of naming the :exc:`repoze.bfg.exceptions.Forbidden` +class as the ``context`` of the view configuration. .. topic:: Using Imperative Configuration If your application uses :term:`imperative configuration`, you can replace the Forbidden view by using the - :meth:`repoze.bfg.configuration.Configurator.set_forbidden_view` - method: + :meth:`repoze.bfg.configuration.Configurator.add_view` method to + register an "exception view": .. code-block:: python :linenos: - import helloworld.views - config.set_forbiddden_view(helloworld.views.forbidden_view) + from helloworld.views import forbidden_view + from repoze.bfg.exceptions import Forbidden + config.add_view(forbidden_view, context=Forbidden) Replace ``helloworld.views.forbidden_view`` with a reference to the Python :term:`view callable` you want to use to represent the @@ -113,16 +140,13 @@ very plain, but it can be overridden as necessary using either .. code-block:: xml :linenos: - - + Replace ``helloworld.views.forbidden_view`` with the Python dotted name to the forbidden view you want to use. - Other attributes of the ``forbidden`` directive are documented at - :ref:`forbidden_directive`. - Like any other view, the forbidden view must accept at least a ``request`` parameter, or both ``context`` and ``request``. The ``context`` (available as ``request.context`` if you're using the @@ -140,13 +164,14 @@ Here's some sample code that implements a minimal forbidden view: def forbidden_view(request): return render_template_to_response('templates/login_form.pt') -.. note:: When a forbidden view is invoked, it is passed the - :term:`request` as the second argument. An attribute of the - request is ``environ``, which is the WSGI environment. Within the - WSGI environ will be a key named ``repoze.bfg.message`` that has a - value explaining why the current view invocation was forbidden. - This error will be different when the ``debug_authorization`` - environment setting is true than it is when it is false. +.. note:: When a forbidden view callable is invoked, it is passed a + :term:`request`. The ``exception`` attribute of the request will + be an instance of the :exc:`repoze.bfg.exceptions.Forbidden` + exception that caused the forbidden view to be called. The value + of ``request.exception.args[0]`` will be a value explaining why the + forbidden was raised. This message will be different when the + ``debug_authorization`` environment setting is true than it is when + it is false. .. warning:: the default forbidden view sends a response with a ``401 Unauthorized`` status code for backwards compatibility reasons. diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 0c6b6cfe6..9e478ef2d 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -936,7 +936,8 @@ stanza: .. code-block:: xml :linenos: - diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 030d19052..a24e4b7b5 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -133,7 +133,7 @@ represent the method expected to return a response, you can use an .. _request_and_context_view_definitions: -Request-And-Context View Callable Definitions +Context-And-Request View Callable Definitions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Usually, view callables are defined to accept only a single argument: @@ -813,6 +813,8 @@ See also :ref:`renderer_directive` and .. index:: single: view exceptions +.. _special_exceptions_in_callables: + Using Special Exceptions In View Callables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -836,7 +838,92 @@ agent which performed the request. In all cases, the message provided to the exception constructor is made available to the view which :mod:`repoze.bfg` invokes as -``request.environ['repoze.bfg.message']``. +``request.exception.args[0]``. + +Exception Views +~~~~~~~~~~~~~~~~ + +The machinery which allows the special +:exc:`repoze.bfg.exceptions.NotFound` and +:exc:`repoze.bfg.exceptions.Forbidden` exceptions to be caught by +specialized views as described in +:ref:`special_exceptions_in_callables` can also be used by application +developers to convert arbitrary exceptions to responses. + +To register a view that should be called whenever a particular +exception is raised from with :mod:`repoze.bfg` view code, use the +exception class or one of its superclasses as the ``context`` of a +view configuration which points at a view callable you'd like to +generate a response. + +For example, given the following exception class in a module named +``helloworld.exceptions``: + +.. code-block:: python + :linenos: + + class ValidationFailure(Exception): + def __init__(self, msg): + self.msg = msg + + +You can wire a view callable to be called whenever any of your *other* +code raises a ``hellworld.exceptions.ValidationFailure`` exception: + +.. code-block:: python + :linenos: + + from helloworld.exceptions import ValidationFailure + + @bfg_view(context=ValidationFailure) + def failed_validation(exc, request): + response = Response('Failed validation: %s' % exc.msg) + response.status_int = 500 + return response + +Assuming that a :term:`scan` was run to pick up this view +registration, this view callable will be invoked whenever a +``helloworld.exceptions.ValidationError`` is raised by your +application's view code. The same exception raised by a custom root +factory or a custom traverser is also caught and hooked. + +Other normal view predicates can also be used in combination with an +exception view registration: + +.. code-block:: python + :linenos: + + from repoze.bfg.view import bfg_view + from repoze.bfg.exceptions import NotFound + from webob.exc import HTTPNotFound + + @bfg_view(context=NotFound, route_name='home') + def notfound_view(request): + return HTTPNotFound() + +The above exception view names the ``route_name`` of ``home``, meaning +that it will only be called when the route matched has a name of +``home``. You can therefore have more than one exception view for any +given exception in the system: the "most specific" one will be called +when the set of request circumstances which match the view +registration. + +The only view predicate that cannot be not be used successfully when +creating an exception view configuration is ``name``. The name used +to look up an exception view is always the empty string. Views +registered as exception views which have a name will be ignored. + +.. note:: + + Normal (non-exception) views registered against a context which + inherits from :exc:`Exception` will work normally. When an + exception view configuraton is processed, *two* exceptions are + registered. One as a "normal" view, the other as an "exception" + view. This means that you can use an exception as ``context`` for a + normal view. + +The feature can be used with any view registration mechanism +(``@bfg_view`` decorator, ZCML, or imperative ``add_view`` styles). .. index:: single: unicode, views, and forms diff --git a/docs/tutorials/bfgwiki/authorization.rst b/docs/tutorials/bfgwiki/authorization.rst index 1b83d3651..8c2ab1df9 100644 --- a/docs/tutorials/bfgwiki/authorization.rst +++ b/docs/tutorials/bfgwiki/authorization.rst @@ -27,7 +27,7 @@ Changing ``configure.zcml`` We'll change our ``configure.zcml`` file to enable an ``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to -enable declarative security checking. We'll also add a ``forbidden`` +enable declarative security checking. We'll also add a new view stanza, which species a :term:`forbidden view`. This configures our login view to show up when :mod:`repoze.bfg` detects that a view invocation can not be authorized. When you're done, your diff --git a/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml b/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml index 837c04089..5297b9ee3 100644 --- a/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml +++ b/docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml @@ -5,9 +5,10 @@ - + renderer="templates/login.pt" + context="repoze.bfg.exceptions.Forbidden"/> - + renderer="templates/login.pt" + for="repoze.bfg.exceptions.Forbidden"/>