diff options
| -rw-r--r-- | CHANGES.txt | 45 | ||||
| -rw-r--r-- | TODO.txt | 2 | ||||
| -rw-r--r-- | docs/api/config.rst | 1 | ||||
| -rw-r--r-- | docs/api/view.rst | 3 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 29 | ||||
| -rw-r--r-- | docs/tutorials/wiki/authorization.rst | 28 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/authorization/tutorial/views.py | 8 | ||||
| -rw-r--r-- | docs/tutorials/wiki/src/tests/tutorial/views.py | 8 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/authorization.rst | 26 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/src/authorization/tutorial/views.py | 8 | ||||
| -rw-r--r-- | docs/tutorials/wiki2/src/tests/tutorial/views.py | 8 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 46 | ||||
| -rw-r--r-- | pyramid/config/views.py | 94 | ||||
| -rw-r--r-- | pyramid/tests/pkgs/forbiddenview/__init__.py | 31 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 24 | ||||
| -rw-r--r-- | pyramid/tests/test_integration.py | 9 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 44 | ||||
| -rw-r--r-- | pyramid/view.py | 65 |
18 files changed, 353 insertions, 126 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index efeba0447..39bf59210 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -15,8 +15,9 @@ Features - New API: ``pyramid.config.Configurator.add_notfound_view``. This is a wrapper for ``pyramid.Config.configurator.add_view`` which provides easy - append_slash support. It should be preferred over calling ``add_view`` - directly with ``context=HTTPNotFound`` as was previously recommended. + append_slash support and does the right thing about permissions. It should + be preferred over calling ``add_view`` directly with + ``context=HTTPNotFound`` as was previously recommended. - New API: ``pyramid.view.notfound_view_config``. This is a decorator constructor like ``pyramid.view.view_config`` that calls @@ -24,6 +25,17 @@ Features be preferred over using ``pyramid.view.view_config`` with ``context=HTTPNotFound`` as was previously recommended. +- New API: ``pyramid.config.Configurator.add_forbidden_view``. This is a + wrapper for ``pyramid.Config.configurator.add_view`` which does the right + thing about permissions. It should be preferred over calling ``add_view`` + directly with ``context=HTTPForbidden`` as was previously recommended. + +- New API: ``pyramid.view.forbidden_view_config``. This is a decorator + constructor like ``pyramid.view.view_config`` that calls + ``pyramid.config.Configurator.add_forbidden_view`` when scanned. It should + be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPForbidden`` as was previously recommended. + Backwards Incompatibilities --------------------------- @@ -40,14 +52,16 @@ Backwards Incompatibilities - The ``pyramid.registry.noop_introspector`` API object has been removed. - The older deprecated ``set_notfound_view`` Configurator method is now an - alias for the new ``add_notfound_view`` Configurator method. This has the - following impact: the ``context`` sent to views with a ``(context, - request)`` call signature registered via the deprecated - ``add_notfound_view``/``set_notfound_view`` will now be the HTTPNotFound - exception object instead of the actual resource context found. Use + alias for the new ``add_notfound_view`` Configurator method. Likewise, the + older deprecated ``set_forbidden_view`` is now an alias for the new + ``add_forbidden_view``. This has the following impact: the ``context`` sent + to views with a ``(context, request)`` call signature registered via the + ``set_notfound_view`` or ``set_forbidden_view`` will now be an exception + object instead of the actual resource context found. Use ``request.context`` to get the actual resource context. It's also recommended to disuse ``set_notfound_view`` in favor of - ``add_notfound_view``, despite the aliasing. + ``add_notfound_view``, and disuse ``set_forbidden_view`` in favor of + ``add_forbidden_view`` despite the aliasing. Deprecations ------------ @@ -59,8 +73,9 @@ Deprecations ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same behavior. -- The ``set_forbidden_view`` method of the Configurator was removed from the - documentation. It has been deprecated since Pyramid 1.1. +- The ``set_forbidden_view`` and ``set_notfound_view`` methods of the + Configurator were removed from the documentation. They have been + deprecated since Pyramid 1.1. Bug Fixes --------- @@ -76,16 +91,24 @@ Bug Fixes Documentation ------------- -- Updated the "Registering a Not Found View" section of the "Hooks" chapter, +- Updated the "Creating a Not Found View" section of the "Hooks" chapter, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config``. +- Updated the "Creating a Not Forbidden View" section of the "Hooks" chapter, + replacing explanations of registering a view using ``add_view`` or + ``view_config`` with ones using ``add_forbidden_view`` or + ``forbidden_view_config``. + - Updated the "Redirecting to Slash-Appended Routes" section of the "URL Dispatch" chapter, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config`` +- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather + than ``pyramid.view.view_config`` with an HTTPForbidden context. + 1.3a8 (2012-02-19) ================== @@ -4,8 +4,6 @@ Pyramid TODOs Nice-to-Have ------------ -- Add forbidden_view_config? - - Add docs about upgrading between Pyramid versions (e.g. how to see deprecation warnings). diff --git a/docs/api/config.rst b/docs/api/config.rst index bf5fdbb7c..cd58e74d3 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -25,6 +25,7 @@ .. automethod:: add_static_view(name, path, cache_max_age=3600, permission=NO_PERMISSION_REQUIRED) .. automethod:: add_view .. automethod:: add_notfound_view + .. automethod:: add_forbidden_view :methodcategory:`Adding an Event Subscriber` diff --git a/docs/api/view.rst b/docs/api/view.rst index cb269e48e..21d2bb90d 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -22,6 +22,9 @@ .. autoclass:: notfound_view_config :members: + .. autoclass:: forbidden_view_config + :members: + .. autoclass:: static :members: :inherited-members: diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index cbc40ceee..b7f052b00 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -145,23 +145,40 @@ the view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "forbidden" view consists -only of naming the :exc:`pyramid.httpexceptions.HTTPForbidden` class as the -``context`` of the view configuration. +of using the meth:`pyramid.config.Configurator.add_forbidden_view` API or the +:class:`pyramid.view.forbidden_view_config` decorator. -You can replace the forbidden view by using the -:meth:`pyramid.config.Configurator.add_view` method to register an "exception -view": +For example, you can add a forbidden view by using the +:meth:`pyramid.config.Configurator.add_forbidden_view` method to register a +forbidden view: .. code-block:: python :linenos: from helloworld.views import forbidden_view from pyramid.httpexceptions import HTTPForbidden - config.add_view(forbidden_view, context=HTTPForbidden) + config.add_forbidden_view(forbidden_view) Replace ``helloworld.views.forbidden_view`` with a reference to the Python :term:`view callable` you want to use to represent the Forbidden view. +If instead you prefer to use decorators and a :term:`scan`, you can use the +:class:`pyramid.view.forbidden_view_config` decorator to mark a view callable +as a forbidden view: + +.. code-block:: python + :linenos: + + from pyramid.view import forbidden_view_config + + forbidden_view_config() + def forbidden(request): + return Response('forbidden') + + def main(globals, **settings): + config = Configurator() + config.scan() + 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 request-only view argument diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 8f583ece7..c1be2cc72 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -132,14 +132,14 @@ We'll add these views to the existing ``views.py`` file we have in our project. Here's what the ``login`` view callable will look like: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 83-111 + :lines: 86-113 :linenos: :language: python Here's what the ``logout`` view callable will look like: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 113-117 + :lines: 115-119 :linenos: :language: python @@ -149,18 +149,18 @@ different :term:`view configuration` for the ``login`` view callable. The first view configuration decorator configures the ``login`` view callable so it will be invoked when someone visits ``/login`` (when the context is a -Wiki and the view name is ``login``). The second decorator (with context of -``pyramid.httpexceptions.HTTPForbidden``) specifies a :term:`forbidden view`. -This configures our login view to be presented to the user when -:app:`Pyramid` detects that a view invocation can not be authorized. Because -we've configured a forbidden view, the ``login`` view callable will be -invoked whenever one of our users tries to execute a view callable that they -are not allowed to invoke as determined by the :term:`authorization policy` -in use. In our application, for example, this means that if a user has not -logged in, and he tries to add or edit a Wiki page, he will be shown the -login form. Before being allowed to continue on to the add or edit form, he -will have to provide credentials that give him permission to add or edit via -this login form. +Wiki and the view name is ``login``). The second decorator, named +``forbidden_view_config`` specifies a :term:`forbidden view`. This +configures our login view to be presented to the user when :app:`Pyramid` +detects that a view invocation can not be authorized. Because we've +configured a forbidden view, the ``login`` view callable will be invoked +whenever one of our users tries to execute a view callable that they are not +allowed to invoke as determined by the :term:`authorization policy` in use. +In our application, for example, this means that if a user has not logged in, +and he tries to add or edit a Wiki page, he will be shown the login form. +Before being allowed to continue on to the add or edit form, he will have to +provide credentials that give him permission to add or edit via this login +form. Note that we're relying on some additional imports within the bodies of these views (e.g. ``remember`` and ``forget``). We'll see a rendering of the diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 2f0502c17..fcbe6fe25 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -3,7 +3,10 @@ import re from pyramid.httpexceptions import HTTPFound -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( authenticated_userid, @@ -82,8 +85,7 @@ def edit_page(context, request): @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') -@view_config(context='pyramid.httpexceptions.HTTPForbidden', - renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.resource_url(request.context, 'login') referrer = request.url diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 2f0502c17..fcbe6fe25 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -3,7 +3,10 @@ import re from pyramid.httpexceptions import HTTPFound -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( authenticated_userid, @@ -82,8 +85,7 @@ def edit_page(context, request): @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') -@view_config(context='pyramid.httpexceptions.HTTPForbidden', - renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.resource_url(request.context, 'login') referrer = request.url diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index b1d0bf37c..900bf0975 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -159,33 +159,35 @@ logged in user and redirect back to the front page. The ``login`` view callable will look something like this: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 87-113 + :lines: 89-115 :linenos: :language: python The ``logout`` view callable will look something like this: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 115-119 + :lines: 117-121 :linenos: :language: python -The ``login`` view callable is decorated with two ``@view_config`` -decorators, one which associates it with the ``login`` route, the other which -associates it with the ``HTTPForbidden`` context. The one which associates -it with the ``login`` route makes it visible when we visit ``/login``. The -one which associates it with the ``HTTPForbidden`` context makes it the -:term:`forbidden view`. The forbidden view is displayed whenever Pyramid or -your application raises an HTTPForbidden exception. In this case, we'll be -relying on the forbidden view to show the login form whenver someone attempts -to execute an action which they're not yet authorized to perform. +The ``login`` view callable is decorated with two decorators, a +``@view_config`` decorators, which associates it with the ``login`` route, +the other a ``@forbidden_view_config`` decorator which turns it in to an +:term:`exception view` when Pyramid raises a +:class:`pyramid.httpexceptions.HTTPForbidden` exception. The one which +associates it with the ``login`` route makes it visible when we visit +``/login``. The other one makes it a :term:`forbidden view`. The forbidden +view is displayed whenever Pyramid or your application raises an +HTTPForbidden exception. In this case, we'll be relying on the forbidden +view to show the login form whenver someone attempts to execute an action +which they're not yet authorized to perform. The ``logout`` view callable is decorated with a ``@view_config`` decorator which associates it with the ``logout`` route. This makes it visible when we visit ``/login``. We'll need to import some stuff to service the needs of these two functions: -the ``HTTPForbidden`` exception, a number of values from the +the ``pyramid.view.forbidden_view_config`` class, a number of values from the ``pyramid.security`` module, and a value from our newly added ``tutorial.security`` package. diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index 087e6076b..1453cd2e6 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -4,10 +4,12 @@ from docutils.core import publish_parts from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, - HTTPForbidden, ) -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( remember, @@ -85,7 +87,7 @@ def edit_page(request): ) @view_config(route_name='login', renderer='templates/login.pt') -@view_config(context=HTTPForbidden, renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.route_url('login') referrer = request.url diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index 375f1f5a5..465d98ae1 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -4,10 +4,12 @@ from docutils.core import publish_parts from pyramid.httpexceptions import ( HTTPFound, HTTPNotFound, - HTTPForbidden, ) -from pyramid.view import view_config +from pyramid.view import ( + view_config, + forbidden_view_config, + ) from pyramid.security import ( remember, @@ -88,7 +90,7 @@ def edit_page(request): ) @view_config(route_name='login', renderer='templates/login.pt') -@view_config(context=HTTPForbidden, renderer='templates/login.pt') +@forbidden_view_config(renderer='templates/login.pt') def login(request): login_url = request.route_url('login') referrer = request.url diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 7f6c3d7cb..101caed94 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -211,8 +211,10 @@ added, as well, but the configurator method should be preferred as it provides conflict detection and consistency in the lifetime of the properties. -Not Found View Helpers -~~~~~~~~~~~~~~~~~~~~~~ +Not Found and Forbidden View Helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not Found helpers: - New API: :meth:`pyramid.config.Configurator.add_notfound_view`. This is a wrapper for :meth:`pyramid.Config.configurator.add_view` which provides @@ -227,6 +229,20 @@ Not Found View Helpers should be preferred over using ``pyramid.view.view_config`` with ``context=HTTPNotFound`` as was previously recommended. +Forbidden helpers: + +- New API: :meth:`pyramid.config.Configurator.add_forbidden_view`. This is a + wrapper for :meth:`pyramid.Config.configurator.add_view` which does the + right thing about permissions. It should be preferred over calling + ``add_view`` directly with ``context=HTTPForbidden`` as was previously + recommended. + +- New API: :class:`pyramid.view.forbidden_view_config`. This is a decorator + constructor like :class:`pyramid.view.view_config` that calls + :meth:`pyramid.config.Configurator.add_forbidden_view` when scanned. It + should be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPForbidden`` as was previously recommended. + Minor Feature Additions ----------------------- @@ -431,14 +447,16 @@ Backwards Incompatibilities Pyramid. - The older deprecated ``set_notfound_view`` Configurator method is now an - alias for the new :meth:`pyramid.config.Configurator.add_notfound_view` - method. This has the following impact: the ``context`` sent to views with - a ``(context, request)`` call signature registered via the deprecated - ``add_notfound_view`` / ``set_notfound_view`` will now be the HTTPNotFound - exception object instead of the actual resource context found. Use + alias for the new ``add_notfound_view`` Configurator method. Likewise, the + older deprecated ``set_forbidden_view`` is now an alias for the new + ``add_forbidden_view`` Configurator method. This has the following impact: + the ``context`` sent to views with a ``(context, request)`` call signature + registered via the ``set_notfound_view`` or ``set_forbidden_view`` will now + be an exception object instead of the actual resource context found. Use ``request.context`` to get the actual resource context. It's also recommended to disuse ``set_notfound_view`` in favor of - ``add_notfound_view``, despite the aliasing. + ``add_notfound_view``, and disuse ``set_forbidden_view`` in favor of + ``add_forbidden_view`` despite the aliasing. Deprecations ------------ @@ -450,8 +468,9 @@ Deprecations ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same behavior. -- The ``set_forbidden_view`` method of the Configurator was removed from the - documentation. It has been deprecated since Pyramid 1.1. +- The ``set_forbidden_view`` and ``set_notfound_view`` methods of the + Configurator were removed from the documentation. They have been + deprecated since Pyramid 1.1. Documentation Enhancements -------------------------- @@ -485,6 +504,10 @@ Documentation Enhancements Rationale: it provides the correct info for the Python 2.5 version of GAE only, and this version of Pyramid does not support Python 2.5. +- Updated the :ref:`changing_the_forbidden_view` section, replacing + explanations of registering a view using ``add_view`` or ``view_config`` + with ones using ``add_forbidden_view`` or ``forbidden_view_config``. + - Updated the :ref:`changing_the_notfound_view` section, replacing explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config``. @@ -493,6 +516,9 @@ Documentation Enhancements explanations of registering a view using ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or ``notfound_view_config`` +- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather + than ``pyramid.view.view_config`` with an HTTPForbidden context. + Dependency Changes ------------------ diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9bd7b4b50..b4216c220 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1329,42 +1329,59 @@ class ViewsConfiguratorMixin(object): return deriver(view) @action_method - def set_forbidden_view(self, view=None, attr=None, renderer=None, - wrapper=None): - """ Add a default forbidden view to the current configuration - state. + def add_forbidden_view( + self, view=None, attr=None, renderer=None, wrapper=None, + route_name=None, request_type=None, request_method=None, + request_param=None, containment=None, xhr=None, accept=None, + header=None, path_info=None, custom_predicates=(), decorator=None, + mapper=None, match_param=None): + """ Add a forbidden view to the current configuration state. The + view will be called when Pyramid or application code raises a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of + circumstances implied by the predicates provided are matched. The + simplest example is: - .. warning:: + .. code-block:: python - This method has been deprecated in :app:`Pyramid` 1.0. *Do not use - it for new development; it should only be used to support older code - bases which depend upon it.* See :ref:`changing_the_forbidden_view` - to see how a forbidden view should be registered in new projects. - - The ``view`` argument should be a :term:`view callable` or a - :term:`dotted Python name` which refers to a view callable. - - The ``attr`` argument should be the attribute of the view - callable used to retrieve the response (see the ``add_view`` - method's ``attr`` argument for a description). - - The ``renderer`` argument should be the name of (or path to) a - :term:`renderer` used to generate a response for this view - (see the - :meth:`pyramid.config.Configurator.add_view` - method's ``renderer`` argument for information about how a - configurator relates to a renderer). - - The ``wrapper`` argument should be the name of another view - which will wrap this view when rendered (see the ``add_view`` - method's ``wrapper`` argument for a description).""" - view = self._derive_view(view, attr=attr, renderer=renderer) - def bwcompat_view(context, request): - context = getattr(request, 'context', None) - return view(context, request) - return self.add_view(bwcompat_view, context=HTTPForbidden, - wrapper=wrapper, renderer=renderer) + def forbidden(request): + return Response('Forbidden', status='403 Forbidden') + + config.add_forbidden_view(forbidden) + + All arguments have the same meaning as + :meth:`pyramid.config.Configurator.add_view` and each predicate + argument restricts the set of circumstances under which this notfound + view will be invoked. + + .. note:: + This method is new as of Pyramid 1.3. + """ + settings = dict( + view=view, + context=HTTPForbidden, + wrapper=wrapper, + request_type=request_type, + request_method=request_method, + request_param=request_param, + containment=containment, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + custom_predicates=custom_predicates, + decorator=decorator, + mapper=mapper, + match_param=match_param, + route_name=route_name, + permission=NO_PERMISSION_REQUIRED, + attr=attr, + renderer=renderer, + ) + return self.add_view(**settings) + + set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias + @action_method def add_notfound_view( self, view=None, attr=None, renderer=None, wrapper=None, @@ -1373,13 +1390,16 @@ class ViewsConfiguratorMixin(object): header=None, path_info=None, custom_predicates=(), decorator=None, mapper=None, match_param=None, append_slash=False): """ Add a default notfound view to the current configuration state. - The view will be called when a view cannot otherwise be found for the - set of circumstances implied by the predicates provided. The - simplest example is: + The view will be called when Pyramid or application code raises an + :exc:`pyramid.httpexceptions.HTTPForbidden` exception (e.g. when a + view cannot be found for the request). The simplest example is: .. code-block:: python - config.add_notfound_view(someview) + def notfound(request): + return Response('Not Found', status='404 Not Found') + + config.add_notfound_view(notfound) All arguments except ``append_slash`` have the same meaning as :meth:`pyramid.config.Configurator.add_view` and each predicate diff --git a/pyramid/tests/pkgs/forbiddenview/__init__.py b/pyramid/tests/pkgs/forbiddenview/__init__.py new file mode 100644 index 000000000..631a442d2 --- /dev/null +++ b/pyramid/tests/pkgs/forbiddenview/__init__.py @@ -0,0 +1,31 @@ +from pyramid.view import forbidden_view_config, view_config +from pyramid.response import Response +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +@forbidden_view_config(route_name='foo') +def foo_forbidden(request): # pragma: no cover + return Response('foo_forbidden') + +@forbidden_view_config() +def forbidden(request): + return Response('generic_forbidden') + +@view_config(route_name='foo') +def foo(request): # pragma: no cover + return Response('OK foo') + +@view_config(route_name='bar') +def bar(request): # pragma: no cover + return Response('OK bar') + +def includeme(config): + authn_policy = AuthTktAuthenticationPolicy('seekri1') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + config.set_default_permission('a') + config.add_route('foo', '/foo') + config.add_route('bar', '/bar') + config.scan('pyramid.tests.pkgs.forbiddenview') + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 668fd7185..7bfe174b7 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1649,14 +1649,14 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual(info.added, [(config, 'static', static_path, {})]) - def test_set_forbidden_view(self): + def test_add_forbidden_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy from pyramid.interfaces import IRequest from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: 'OK' - config.set_forbidden_view(view, renderer=null_renderer) + config.add_forbidden_view(view, renderer=null_renderer) request = self._makeRequest(config) view = self._getViewCallable(config, ctx_iface=implementedBy(HTTPForbidden), @@ -1664,24 +1664,6 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result, 'OK') - def test_set_forbidden_view_request_has_context(self): - from pyramid.renderers import null_renderer - from zope.interface import implementedBy - from pyramid.interfaces import IRequest - from pyramid.httpexceptions import HTTPForbidden - config = self._makeOne(autocommit=True) - view = lambda *arg: arg - config.set_forbidden_view(view, renderer=null_renderer) - request = self._makeRequest(config) - request.context = 'abc' - view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPForbidden), - request_iface=IRequest) - result = view(None, request) - self.assertEqual(result, ('abc', request)) - - - def test_add_notfound_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy @@ -1743,7 +1725,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: {} - config.set_forbidden_view( + config.add_forbidden_view( view, renderer='pyramid.tests.test_config:files/minimal.pt') config.begin() diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 57b7e40b2..bf3bafc09 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -373,6 +373,15 @@ class TestNotFoundView(IntegrationBase, unittest.TestCase): self.assertTrue(b'OK foo2' in res.body) res = self.testapp.get('/baz', status=200) self.assertTrue(b'baz_notfound' in res.body) + +class TestForbiddenView(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.forbiddenview' + + def test_it(self): + res = self.testapp.get('/foo', status=200) + self.assertTrue(b'foo_forbidden' in res.body) + res = self.testapp.get('/bar', status=200) + self.assertTrue(b'generic_forbidden' in res.body) class TestViewPermissionBug(IntegrationBase, unittest.TestCase): # view_execution_permitted bug as reported by Shane at http://lists.repoze.org/pipermail/repoze-dev/2010-October/003603.html diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index f092f281b..a775e7bc9 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -94,6 +94,48 @@ class Test_notfound_view_config(BaseTest, unittest.TestCase): self.assertEqual(settings[0]['attr'], 'view') self.assertEqual(settings[0]['_info'], 'codeinfo') +class Test_forbidden_view_config(BaseTest, unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.view import forbidden_view_config + return forbidden_view_config(**kw) + + def test_ctor(self): + inst = self._makeOne(attr='attr', path_info='path_info') + self.assertEqual(inst.__dict__, + {'attr':'attr', 'path_info':'path_info'}) + + def test_it_function(self): + def view(request): pass + decorator = self._makeOne(attr='attr', renderer='renderer') + venusian = DummyVenusian() + decorator.venusian = venusian + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual( + settings, + [{'attr': 'attr', 'venusian': venusian, + 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}] + ) + + def test_it_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class view(object): pass + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'view') + self.assertEqual(settings[0]['_info'], 'codeinfo') + class RenderViewToResponseTests(BaseTest, unittest.TestCase): def _callFUT(self, *arg, **kw): from pyramid.view import render_view_to_response @@ -716,7 +758,7 @@ class DummyConfig(object): def add_view(self, **kw): self.settings.append(kw) - add_notfound_view = add_view + add_notfound_view = add_forbidden_view = add_view def with_package(self, pkg): self.pkg = pkg diff --git a/pyramid/view.py b/pyramid/view.py index 9f049bf09..d722c0cbb 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -391,6 +391,71 @@ class notfound_view_config(object): settings['_info'] = info.codeinfo # fbo "action_method" return wrapped +class forbidden_view_config(object): + """ + + An analogue of :class:`pyramid.view.view_config` which registers a + :term:`forbidden view`. + + The forbidden_view_config constructor accepts most of the same arguments + as the constructor of :class:`pyramid.view.view_config`. It can be used + in the same places, and behaves in largely the same way, except it always + registers a forbidden exception view instead of a "normal" view. + + Example: + + .. code-block:: python + + from pyramid.view import forbidden_view_config + from pyramid.response import Response + + forbidden_view_config() + def notfound(request): + return Response('You are not allowed', status='401 Unauthorized') + + All have the same meaning as :meth:`pyramid.view.view_config` and each + predicate argument restricts the set of circumstances under which this + notfound view will be invoked. + + See :ref:`changing_the_forbidden_view` for detailed usage information. + + .. note:: + + This class is new as of Pyramid 1.3. + """ + + venusian = venusian + + def __init__(self, request_type=default, request_method=default, + route_name=default, request_param=default, attr=default, + renderer=default, containment=default, wrapper=default, + xhr=default, accept=default, header=default, + path_info=default, custom_predicates=default, + decorator=default, mapper=default, match_param=default): + L = locals() + for k, v in L.items(): + if k not in ('self', 'L') and v is not default: + self.__dict__[k] = v + + def __call__(self, wrapped): + settings = self.__dict__.copy() + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_forbidden_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category='pyramid') + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + def is_response(ob): """ Return ``True`` if ``ob`` implements the interface implied by :ref:`the_response`. ``False`` if not. |
