summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt19
-rw-r--r--docs/glossary.rst19
-rw-r--r--docs/narr/views.rst396
-rw-r--r--docs/whatsnew-1.2.rst15
-rw-r--r--repoze/bfg/router.py116
-rw-r--r--repoze/bfg/tests/test_router.py43
6 files changed, 365 insertions, 243 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index f9ee7d81a..024ed8929 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,20 @@
Next release
============
+Features
+--------
+
+- When the ``repoze.bfg.exceptions.NotFound`` or
+ ``repoze.bfg.exceptions.Forbidden`` error is raised from within a
+ custom root factory or the ``factory`` of a route, the appropriate
+ response is now sent to the requesting user agent (the result of the
+ notfound view or the forbidden view, respectively). When these
+ errors are raised from within a root factory, the ``context`` passed
+ to the notfound or forbidden view will be ``None``. Also, the
+ request will not be decorated with ``view_name``, ``subpath``,
+ ``context``, etc. as would normally be the case if traversal had
+ been allowed to take place.
+
Internals
---------
@@ -22,6 +36,11 @@ Documentation
- Added "Thread Locals" narrative chapter to documentation, and added
a API chapter documenting the ``repoze.bfg.threadlocals`` module.
+- Added a "Special Exceptions" section to the "Views" narrative
+ documentation chapter explaining the effect of raising
+ ``repoze.bfg.exceptions.NotFound`` and
+ ``repoze.bfg.exceptions.Forbidden`` from within view code.
+
Dependencies
------------
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 491ad6d48..0fa827188 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -490,3 +490,22 @@ Glossary
The configuration mode in which you use Python to call methods on
a :term:`Configurator` in order to add each :term:`configuration
declaration` required by your application.
+ Not Found View
+ The :term:`view callable` invoked by :mod:`repoze.bfg` when the
+ developer explicitly raises a
+ ``repoze.bfg.exceptions.NotFound`` exception from within
+ :term:`view` code or :term:`root factory` code, or when the
+ current request doesn't match any :term:`view configuration`.
+ :mod:`repoze.bfg` provides a default implementation of a not
+ found view; it can be overridden. See
+ :ref:`changing_the_notfound_view`.
+ Forbidden View
+ The :term:`view callable` invoked by :mod:`repoze.bfg` when the
+ developer explicitly raises a
+ ``repoze.bfg.exceptions.Forbidden`` exception from within
+ :term:`view` code or :term:`root factory` code, or when the the
+ :term:`view configuration` and :term:`authorization policy` found
+ for a request disallows a particular view invocation.
+ :mod:`repoze.bfg` provides a default implementation of a
+ forbidden view; it can be overridden. See
+ :ref:`changing_the_forbidden_view`.
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index e4eba6a0b..01389cec0 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -1154,6 +1154,192 @@ Views with use a Chameleon renderer can vary response attributes by
attaching properties to the request. See
:ref:`response_request_attrs`.
+.. _response_request_attrs:
+
+Varying Attributes of Rendered Responses
+----------------------------------------
+
+Before a response that is constructed as the result of the use of a
+:term:`renderer` is returned to BFG, several attributes of the request
+are examined which have the potential to influence response behavior.
+
+View callables that don't directly return a response should set these
+values on the ``request`` object via ``setattr`` within the view
+callable to influence automatically constructed response attributes.
+
+``response_content_type``
+
+ Defines the content-type of the resulting response,
+ e.g. ``text/xml``.
+
+``response_headerlist``
+
+ A sequence of tuples describing cookie values that should be set in
+ the response, e.g. ``[('Set-Cookie', 'abc=123'), ('X-My-Header',
+ 'foo')]``.
+
+``response_status``
+
+ A WSGI-style status code (e.g. ``200 OK``) describing the status of
+ the response.
+
+``response_charset``
+
+ The character set (e.g. ``UTF-8``) of the response.
+
+``response_cache_for``
+
+ A value in seconds which will influence ``Cache-Control`` and
+ ``Expires`` headers in the returned response. The same can also be
+ achieved by returning various values in the ``response_headerlist``,
+ this is purely a convenience.
+
+.. _adding_and_overriding_renderers:
+
+Adding and Overriding Renderers
+-------------------------------
+
+Additional configuration declarations can be made which override an
+existing :term:`renderer` or which add a new renderer. Adding or
+overriding a renderer is accomplished via :term:`ZCML` or via
+imperative configuration.
+
+For example, to add a renderer which renders views which have a
+``renderer`` attribute that is a path that ends in ``.jinja2``:
+
+.. topic:: Via ZCML
+
+ .. code-block:: xml
+ :linenos:
+
+ <renderer
+ name=".jinja2"
+ factory="my.package.MyJinja2Renderer"/>
+
+ The ``factory`` attribute is a dotted Python name that must point
+ to an implementation of a :term:`renderer`.
+
+ The ``name`` attribute is the renderer name.
+
+.. topic:: Via Imperative Configuration
+
+ .. code-block:: python
+ :linenos:
+
+ from my.package import MyJinja2Renderer
+ config.add_renderer('.jinja2', MyJinja2Renderer)
+
+ The first argument is the renderer name.
+
+ The second argument is a reference to an to an implementation of a
+ :term:`renderer`.
+
+A renderer implementation is usually a class which has the following
+interface:
+
+.. code-block:: python
+ :linenos:
+
+ class RendererFactory:
+ def __init__(self, name):
+ """ Constructor: ``name`` may be a path """
+
+ def __call__(self, value, system): """ Call a the renderer
+ implementation with the value and the system value passed
+ in as arguments and return the result (a string or unicode
+ object). The value is the return value of a view. The
+ system value is a dictionary containing available system
+ values (e.g. ``view``, ``context``, and ``request``). """
+
+There are essentially two different kinds of ``renderer``
+registrations: registrations that use a dot (``.``) in their ``name``
+argument and ones which do not.
+
+Renderer registrations that have a ``name`` attribute which starts
+with a dot are meant to be *wildcard* registrations. When a ``view``
+configuration is encountered which has a ``name`` attribute that
+contains a dot, at startup time, the path is split on its final dot,
+and the second element of the split (the filename extension,
+typically) is used to look up a renderer for the configured view. The
+renderer's factory is still passed the entire ``name`` attribute value
+(not just the extension).
+
+Renderer registrations that have ``name`` attribute which *does not*
+start with a dot are meant to be absolute registrations. When a
+``view`` configuration is encountered which has a ``name`` argument
+that does not contain a dot, the full value of the ``name`` attribute
+is used to look up the renderer for the configured view.
+
+Here's an example of a renderer registration in ZCML:
+
+.. code-block:: xml
+ :linenos:
+
+ <renderer
+ name="amf"
+ factory="my.package.MyAMFRenderer"/>
+
+Adding the above ZCML to your application will allow you to use the
+``my.package.MyAMFRenderer`` renderer implementation in ``view``
+configurations by referring to it as ``amf`` in the ``renderer``
+attribute:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.view import bfg_view
+
+ @bfg_view(renderer='amf')
+ def myview(request):
+ return {'Hello':'world'}
+
+By default, when a template extension is unrecognized, an error is
+thrown at rendering time. You can associate more than one filename
+extension with the same renderer implementation as necessary if you
+need to use a different file extension for the same kinds of
+templates. For example, to associate the ``.zpt`` extension with the
+Chameleon page template renderer factory, use:
+
+.. code-block:: xml
+ :linenos:
+
+ <renderer
+ name=".zpt"
+ factory="repoze.bfg.chameleon_zpt.renderer_factory"/>
+
+To override the default mapping in which files with a ``.pt``
+extension are rendered via a Chameleon ZPT page template renderer, use
+a variation on the following in your application's ZCML:
+
+.. code-block:: xml
+ :linenos:
+
+ <renderer
+ name=".pt"
+ factory="my.package.pt_renderer"/>
+
+To override the default mapping in which files with a ``.txt``
+extension are rendered via a Chameleon text template renderer, use a
+variation on the following in your application's ZCML:
+
+.. code-block:: xml
+ :linenos:
+
+ <renderer
+ name=".txt"
+ factory="my.package.text_renderer"/>
+
+To associate a *default* renderer with *all* view configurations (even
+ones which do not possess a ``renderer`` attribute), use a variation
+on the following (ie. omit the ``name`` attribute to the renderer
+tag):
+
+.. code-block:: xml
+ :linenos:
+
+ <renderer
+ factory="repoze.bfg.renderers.json_renderer_factory"/>
+
.. _view_security_section:
View Security
@@ -1426,6 +1612,31 @@ In this case, ``.models.Root`` refers to the class of which your
``/static/foo.js``. See :ref:`traversal_chapter` for information
about "goggles" (``@@``).
+Special Exceptions
+------------------
+
+Usually when a Python exception is raised within view code,
+:mod:`repoze.bfg` allows the exception to propagate all the way out to
+the :term:`WSGI` server which invoked the application.
+
+However, for convenience, two special exceptions exist which are
+always handled by :mod:`repoze.bfg` itself. These are
+``repoze.bfg.exceptions.NotFound`` and
+``repoze.bfg.exceptions.Forbidden``. Both is an exception classe
+which accept a single positional constructor argument: a message.
+
+If ``repoze.bfg.exceptions.NotFound`` is raised within view code, the
+result of the :term:`Not Found View` will be returned to the user
+agent which performed the request.
+
+If ``repoze.bfg.exceptions.Forbidden`` is raised within view code, the
+result of the :term:`Forbidden View` will be returned to the user
+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']``.
+
Using Views to Handle Form Submissions (Unicode and Character Set Issues)
-------------------------------------------------------------------------
@@ -1553,188 +1764,3 @@ rendered in a request that has a ``;charset=utf-8`` stanza on its
to Unicode objects implicitly in :mod:`repoze.bfg`'s default
configuration. The keys are still strings.
-.. _response_request_attrs:
-
-Varying Attributes of Rendered Responses
-----------------------------------------
-
-Before a response that is constructed as the result of the use of a
-:term:`renderer` is returned to BFG, several attributes of the request
-are examined which have the potential to influence response behavior.
-
-View callables that don't directly return a response should set these
-values on the ``request`` object via ``setattr`` within the view
-callable to influence automatically constructed response attributes.
-
-``response_content_type``
-
- Defines the content-type of the resulting response,
- e.g. ``text/xml``.
-
-``response_headerlist``
-
- A sequence of tuples describing cookie values that should be set in
- the response, e.g. ``[('Set-Cookie', 'abc=123'), ('X-My-Header',
- 'foo')]``.
-
-``response_status``
-
- A WSGI-style status code (e.g. ``200 OK``) describing the status of
- the response.
-
-``response_charset``
-
- The character set (e.g. ``UTF-8``) of the response.
-
-``response_cache_for``
-
- A value in seconds which will influence ``Cache-Control`` and
- ``Expires`` headers in the returned response. The same can also be
- achieved by returning various values in the ``response_headerlist``,
- this is purely a convenience.
-
-.. _adding_and_overriding_renderers:
-
-Adding and Overriding Renderers
--------------------------------
-
-Additional configuration declarations can be made which override an
-existing :term:`renderer` or which add a new renderer. Adding or
-overriding a renderer is accomplished via :term:`ZCML` or via
-imperative configuration.
-
-For example, to add a renderer which renders views which have a
-``renderer`` attribute that is a path that ends in ``.jinja2``:
-
-.. topic:: Via ZCML
-
- .. code-block:: xml
- :linenos:
-
- <renderer
- name=".jinja2"
- factory="my.package.MyJinja2Renderer"/>
-
- The ``factory`` attribute is a dotted Python name that must point
- to an implementation of a :term:`renderer`.
-
- The ``name`` attribute is the renderer name.
-
-.. topic:: Via Imperative Configuration
-
- .. code-block:: python
- :linenos:
-
- from my.package import MyJinja2Renderer
- config.add_renderer('.jinja2', MyJinja2Renderer)
-
- The first argument is the renderer name.
-
- The second argument is a reference to an to an implementation of a
- :term:`renderer`.
-
-A renderer implementation is usually a class which has the following
-interface:
-
-.. code-block:: python
- :linenos:
-
- class RendererFactory:
- def __init__(self, name):
- """ Constructor: ``name`` may be a path """
-
- def __call__(self, value, system): """ Call a the renderer
- implementation with the value and the system value passed
- in as arguments and return the result (a string or unicode
- object). The value is the return value of a view. The
- system value is a dictionary containing available system
- values (e.g. ``view``, ``context``, and ``request``). """
-
-There are essentially two different kinds of ``renderer``
-registrations: registrations that use a dot (``.``) in their ``name``
-argument and ones which do not.
-
-Renderer registrations that have a ``name`` attribute which starts
-with a dot are meant to be *wildcard* registrations. When a ``view``
-configuration is encountered which has a ``name`` attribute that
-contains a dot, at startup time, the path is split on its final dot,
-and the second element of the split (the filename extension,
-typically) is used to look up a renderer for the configured view. The
-renderer's factory is still passed the entire ``name`` attribute value
-(not just the extension).
-
-Renderer registrations that have ``name`` attribute which *does not*
-start with a dot are meant to be absolute registrations. When a
-``view`` configuration is encountered which has a ``name`` argument
-that does not contain a dot, the full value of the ``name`` attribute
-is used to look up the renderer for the configured view.
-
-Here's an example of a renderer registration in ZCML:
-
-.. code-block:: xml
- :linenos:
-
- <renderer
- name="amf"
- factory="my.package.MyAMFRenderer"/>
-
-Adding the above ZCML to your application will allow you to use the
-``my.package.MyAMFRenderer`` renderer implementation in ``view``
-configurations by referring to it as ``amf`` in the ``renderer``
-attribute:
-
-.. code-block:: python
- :linenos:
-
- from repoze.bfg.view import bfg_view
-
- @bfg_view(renderer='amf')
- def myview(request):
- return {'Hello':'world'}
-
-By default, when a template extension is unrecognized, an error is
-thrown at rendering time. You can associate more than one filename
-extension with the same renderer implementation as necessary if you
-need to use a different file extension for the same kinds of
-templates. For example, to associate the ``.zpt`` extension with the
-Chameleon page template renderer factory, use:
-
-.. code-block:: xml
- :linenos:
-
- <renderer
- name=".zpt"
- factory="repoze.bfg.chameleon_zpt.renderer_factory"/>
-
-To override the default mapping in which files with a ``.pt``
-extension are rendered via a Chameleon ZPT page template renderer, use
-a variation on the following in your application's ZCML:
-
-.. code-block:: xml
- :linenos:
-
- <renderer
- name=".pt"
- factory="my.package.pt_renderer"/>
-
-To override the default mapping in which files with a ``.txt``
-extension are rendered via a Chameleon text template renderer, use a
-variation on the following in your application's ZCML:
-
-.. code-block:: xml
- :linenos:
-
- <renderer
- name=".txt"
- factory="my.package.text_renderer"/>
-
-To associate a *default* renderer with *all* view configurations (even
-ones which do not possess a ``renderer`` attribute), use a variation
-on the following (ie. omit the ``name`` attribute to the renderer
-tag):
-
-.. code-block:: xml
- :linenos:
-
- <renderer
- factory="repoze.bfg.renderers.json_renderer_factory"/>
diff --git a/docs/whatsnew-1.2.rst b/docs/whatsnew-1.2.rst
index 32a878616..c417ba3ae 100644
--- a/docs/whatsnew-1.2.rst
+++ b/docs/whatsnew-1.2.rst
@@ -90,6 +90,17 @@ Minor Miscellaneous Feature Additions
attached to the constructed model via
``zope.interface.alsoProvides``).
+- When the ``repoze.bfg.exceptions.NotFound`` or
+ ``repoze.bfg.exceptions.Forbidden`` error is raised from within a
+ custom :term:`root factory` or the factory of a :term:`route`, the
+ appropriate response is sent to the requesting user agent (the
+ result of the notfound view or the forbidden view, respectively).
+ When these errors are raised from within a root factory, the
+ :term:`context` passed to the notfound or forbidden view will be
+ ``None``. Also, the request will not be decorated with
+ ``view_name``, ``subpath``, ``context``, etc. as would normally be
+ the case if traversal had been allowed to take place.
+
Backwards Incompatibilites
--------------------------
@@ -281,3 +292,7 @@ Documentation Enhancements
- Added "Thread Locals" narrative chapter to documentation, and added
a API chapter documenting the ``repoze.bfg.threadlocals`` module.
+- Added a "Special Exceptions" section to the "Views" narrative
+ documentation chapter explaining the effect of raising
+ ``repoze.bfg.exceptions.NotFound`` and
+ ``repoze.bfg.exceptions.Forbidden`` from within view code.
diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py
index deffa4e17..630aa201c 100644
--- a/repoze/bfg/router.py
+++ b/repoze/bfg/router.py
@@ -65,71 +65,73 @@ class Router(object):
try:
# setup
request = Request(environ)
+ context = None
threadlocals['request'] = request
attrs = request.__dict__
attrs['registry'] = registry
has_listeners and registry.notify(NewRequest(request))
+
+ try:
+ # root resolution
+ root_factory = self.root_factory
+ if self.routes_mapper is not None:
+ info = self.routes_mapper(request)
+ match, route = info['match'], info['route']
+ if route is not None:
+ environ['wsgiorg.routing_args'] = ((), match)
+ environ['bfg.routes.route'] = route
+ environ['bfg.routes.matchdict'] = match
+ request.matchdict = match
+ iface = registry.queryUtility(IRouteRequest,
+ name=route.name)
+ if iface is not None:
+ alsoProvides(request, iface)
+ root_factory = route.factory or self.root_factory
+
+ root = root_factory(request)
+ attrs['root'] = root
+
+ # view lookup
+ traverser = registry.adapters.queryAdapter(root, ITraverser)
+ if traverser is None:
+ traverser = ModelGraphTraverser(root)
+ tdict = traverser(request)
+ context, view_name, subpath, traversed, vroot, vroot_path = (
+ tdict['context'], tdict['view_name'], tdict['subpath'],
+ tdict['traversed'], tdict['virtual_root'],
+ tdict['virtual_root_path'])
+ attrs.update(tdict)
+ has_listeners and registry.notify(AfterTraversal(request))
+ provides = map(providedBy, (context, request))
+ view_callable = registry.adapters.lookup(
+ provides, IView, name=view_name, default=None)
- # root resolution
- root_factory = self.root_factory
- if self.routes_mapper is not None:
- info = self.routes_mapper(request)
- match, route = info['match'], info['route']
- if route is not None:
- environ['wsgiorg.routing_args'] = ((), match)
- environ['bfg.routes.route'] = route
- environ['bfg.routes.matchdict'] = match
- request.matchdict = match
- iface = registry.queryUtility(IRouteRequest,
- name=route.name)
- if iface is not None:
- alsoProvides(request, iface)
- root_factory = route.factory or self.root_factory
-
- root = root_factory(request)
- attrs['root'] = root
-
- # view lookup
- traverser = registry.adapters.queryAdapter(root, ITraverser)
- if traverser is None:
- traverser = ModelGraphTraverser(root)
- tdict = traverser(request)
- context, view_name, subpath, traversed, vroot, vroot_path = (
- tdict['context'], tdict['view_name'], tdict['subpath'],
- tdict['traversed'], tdict['virtual_root'],
- tdict['virtual_root_path'])
- attrs.update(tdict)
- has_listeners and registry.notify(AfterTraversal(request))
- provides = map(providedBy, (context, request))
- view_callable = registry.adapters.lookup(
- provides, IView, name=view_name, default=None)
-
- # view execution
- if view_callable is None:
- if self.debug_notfound:
- msg = (
- 'debug_notfound of url %s; path_info: %r, context: %r, '
- 'view_name: %r, subpath: %r, traversed: %r, '
- 'root: %r, vroot: %r, vroot_path: %r' % (
- request.url, request.path_info, context, view_name,
- subpath, traversed, root, vroot, vroot_path)
- )
- logger and logger.debug(msg)
+ # view execution
+ if view_callable is None:
+ if self.debug_notfound:
+ msg = (
+ 'debug_notfound of url %s; path_info: %r, context: %r, '
+ 'view_name: %r, subpath: %r, traversed: %r, '
+ 'root: %r, vroot: %r, vroot_path: %r' % (
+ request.url, request.path_info, context, view_name,
+ subpath, traversed, root, vroot, vroot_path)
+ )
+ logger and logger.debug(msg)
+ else:
+ msg = request.path_info
+ environ['repoze.bfg.message'] = msg
+ response = self.notfound_view(context, request)
else:
- msg = request.path_info
+ response = view_callable(context, request)
+
+ except Forbidden, why:
+ msg = why[0]
+ environ['repoze.bfg.message'] = msg
+ response = self.forbidden_view(context, request)
+ except NotFound, why:
+ msg = why[0]
environ['repoze.bfg.message'] = msg
response = self.notfound_view(context, request)
- else:
- try:
- response = view_callable(context, request)
- except Forbidden, why:
- msg = why[0]
- environ['repoze.bfg.message'] = msg
- response = self.forbidden_view(context, request)
- except NotFound, why:
- msg = why[0]
- environ['repoze.bfg.message'] = msg
- response = self.notfound_view(context, request)
# response handling
has_listeners and registry.notify(NewResponse(response))
diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py
index 45e66b8fe..5352c6d79 100644
--- a/repoze/bfg/tests/test_router.py
+++ b/repoze/bfg/tests/test_router.py
@@ -42,7 +42,8 @@ class TestRouter(unittest.TestCase):
def _registerTraverserFactory(self, context, view_name='', subpath=None,
traversed=None, virtual_root=None,
- virtual_root_path=None, **kw):
+ virtual_root_path=None, raise_error=None,
+ **kw):
from repoze.bfg.interfaces import ITraverser
if virtual_root is None:
@@ -59,6 +60,8 @@ class TestRouter(unittest.TestCase):
self.root = root
def __call__(self, request):
+ if raise_error:
+ raise raise_error
values = {'root':self.root,
'context':context,
'view_name':view_name,
@@ -504,6 +507,44 @@ class TestRouter(unittest.TestCase):
self.failUnless(req_iface.providedBy(request))
self.failUnless(IFoo.providedBy(request))
+ def test_root_factory_raises_notfound(self):
+ from repoze.bfg.interfaces import IRootFactory
+ from repoze.bfg.exceptions import NotFound
+ from zope.interface import Interface
+ from zope.interface import directlyProvides
+ def rootfactory(request):
+ raise NotFound('from root factory')
+ self.registry.registerUtility(rootfactory, IRootFactory)
+ class IContext(Interface):
+ pass
+ context = DummyContext()
+ directlyProvides(context, IContext)
+ environ = self._makeEnviron()
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ app_iter = router(environ, start_response)
+ self.assertEqual(start_response.status, '404 Not Found')
+ self.failUnless('from root factory' in app_iter[0])
+
+ def test_root_factory_raises_forbidden(self):
+ from repoze.bfg.interfaces import IRootFactory
+ from repoze.bfg.exceptions import Forbidden
+ from zope.interface import Interface
+ from zope.interface import directlyProvides
+ def rootfactory(request):
+ raise Forbidden('from root factory')
+ self.registry.registerUtility(rootfactory, IRootFactory)
+ class IContext(Interface):
+ pass
+ context = DummyContext()
+ directlyProvides(context, IContext)
+ environ = self._makeEnviron()
+ router = self._makeOne()
+ start_response = DummyStartResponse()
+ app_iter = router(environ, start_response)
+ self.assertEqual(start_response.status, '401 Unauthorized')
+ self.failUnless('from root factory' in app_iter[0])
+
class DummyContext:
pass