Chris McDonough
2010-07-04 6bc66257ca3abfc02557b94a8086c811797e2ba9
commit | author | age
c8cf22 1 .. _hooks_chapter:
CM 2
13c923 3 Using Hooks
CM 4 ===========
c8cf22 5
ff1213 6 "Hooks" can be used to influence the behavior of the :mod:`repoze.bfg`
CM 7 framework in various ways.
fab8c5 8
8c56ae 9 .. index::
c5f24b 10    single: not found view
8c56ae 11
7bc20e 12 .. _changing_the_notfound_view:
CM 13
fab8c5 14 Changing the Not Found View
CM 15 ---------------------------
16
17 When :mod:`repoze.bfg` can't map a URL to view code, it invokes a
ff1213 18 :term:`not found view`, which is a :term:`view callable`. A default
CM 19 notfound view exists.  The default not found view can be overridden
20 through application configuration.  This override can be done via
21 :term:`imperative configuration` or :term:`ZCML`.
22
23 The :term:`not found view` callable is a view callable like any other.
24 The :term:`view configuration` which causes it to be a "not found"
25 view consists only of naming the :exc:`repoze.bfg.exceptions.NotFound`
26 class as the ``context`` of the view configuration.
0ac818 27
CM 28 .. topic:: Using Imperative Configuration
29
30    If your application uses :term:`imperative configuration`, you can
cb8e34 31    replace the Not Found view by using the
ff1213 32    :meth:`repoze.bfg.configuration.Configurator.add_view` method to
CM 33    register an "exception view":
0ac818 34
CM 35    .. code-block:: python
36       :linenos:
37
ff1213 38       from repoze.bfg.exceptions import NotFound
CM 39       from helloworld.views import notfound_view
40       config.add_view(notfound_view, context=NotFound)
0ac818 41
CM 42    Replace ``helloworld.views.notfound_view`` with a reference to the
43    Python :term:`view callable` you want to use to represent the Not
44    Found view.
fab8c5 45
13c923 46 .. topic:: Using ZCML
fab8c5 47
13c923 48    If your application uses :term:`ZCML`, you can replace the Not Found
CM 49    view by placing something like the following ZCML in your
50    ``configure.zcml`` file.
fab8c5 51
13c923 52    .. code-block:: xml
CM 53       :linenos:
54
ff1213 55       <view
CM 56         view="helloworld.views.notfound_view"
57         context="repoze.bfg.exceptions.NotFound"/>
13c923 58
CM 59    Replace ``helloworld.views.notfound_view`` with the Python dotted name
60    to the notfound view you want to use.
61
ff1213 62 Like any other view, the notfound view must accept at least a
CM 63 ``request`` parameter, or both ``context`` and ``request``.  The
64 ``request`` is the current :term:`request` representing the denied
65 action.  The ``context`` (if used in the call signature) will be the
66 instance of the :exc:`repoze.bfg.exceptions.NotFound` exception that
67 caused the view to be called.
13c923 68
ff1213 69 Here's some sample code that implements a minimal NotFound view
CM 70 callable:
fab8c5 71
CM 72 .. code-block:: python
601289 73    :linenos:
fab8c5 74
CM 75    from webob.exc import HTTPNotFound
76
6103bf 77    def notfound_view(request):
fab8c5 78        return HTTPNotFound()
CM 79
ff1213 80 .. note:: When a NotFound view callable is invoked, it is passed a
CM 81    :term:`request`.  The ``exception`` attribute of the request will
82    be an instance of the :exc:`repoze.bfg.exceptions.NotFound`
83    exception that caused the not found view to be called.  The value
84    of ``request.exception.args[0]`` will be a value explaining why the
85    not found error was raised.  This message will be different when
86    the ``debug_notfound`` environment setting is true than it is when
87    it is false.
88
89 .. warning:: When a NotFound view callable accepts an argument list as
90    described in :ref:`request_and_context_view_definitions`, the
91    ``context`` passed as the first argument to the view callable will
92    be the :exc:`repoze.bfg.exceptions.NotFound` exception instance.
93    If available, the *model* context will still be available as
94    ``request.context``.
8c56ae 95
CM 96 .. index::
c5f24b 97    single: forbidden view
fab8c5 98
7bc20e 99 .. _changing_the_forbidden_view:
CM 100
fab8c5 101 Changing the Forbidden View
CM 102 ---------------------------
103
104 When :mod:`repoze.bfg` can't authorize execution of a view based on
831da8 105 the :term:`authorization policy` in use, it invokes a :term:`forbidden
CM 106 view`.  The default forbidden response has a 401 status code and is
ff1213 107 very plain, but the view which generates it can be overridden as
CM 108 necessary using either :term:`imperative configuration` or
109 :term:`ZCML`:
110
111 The :term:`forbidden view` callable is a view callable like any other.
112 The :term:`view configuration` which causes it to be a "not found"
113 view consists only of naming the :exc:`repoze.bfg.exceptions.Forbidden`
114 class as the ``context`` of the view configuration.
0ac818 115
CM 116 .. topic:: Using Imperative Configuration
117
118    If your application uses :term:`imperative configuration`, you can
cb8e34 119    replace the Forbidden view by using the
ff1213 120    :meth:`repoze.bfg.configuration.Configurator.add_view` method to
CM 121    register an "exception view":
0ac818 122
CM 123    .. code-block:: python
124       :linenos:
125
ff1213 126       from helloworld.views import forbidden_view
CM 127       from repoze.bfg.exceptions import Forbidden
128       config.add_view(forbidden_view, context=Forbidden)
0ac818 129
CM 130    Replace ``helloworld.views.forbidden_view`` with a reference to the
131    Python :term:`view callable` you want to use to represent the
132    Forbidden view.
fab8c5 133
13c923 134 .. topic:: Using ZCML
fab8c5 135
13c923 136    If your application uses :term:`ZCML`, you can replace the
CM 137    Forbidden view by placing something like the following ZCML in your
138    ``configure.zcml`` file.
fab8c5 139
13c923 140    .. code-block:: xml
CM 141       :linenos:
142
ff1213 143       <view
CM 144         view="helloworld.views.notfound_view"
145         context="repoze.bfg.exceptions.Forbidden"/>
13c923 146
CM 147    Replace ``helloworld.views.forbidden_view`` with the Python
148    dotted name to the forbidden view you want to use.
149
150 Like any other view, the forbidden view must accept at least a
151 ``request`` parameter, or both ``context`` and ``request``.  The
152 ``context`` (available as ``request.context`` if you're using the
153 request-only view argument pattern) is the context found by the router
154 when the view invocation was denied.  The ``request`` is the current
155 :term:`request` representing the denied action.
156
157 Here's some sample code that implements a minimal forbidden view:
fab8c5 158
CM 159 .. code-block:: python
601289 160    :linenos:
fab8c5 161
CM 162    from repoze.bfg.chameleon_zpt import render_template_to_response
163
6103bf 164    def forbidden_view(request):
fab8c5 165        return render_template_to_response('templates/login_form.pt')
CM 166
ff1213 167 .. note:: When a forbidden view callable is invoked, it is passed a
CM 168    :term:`request`.  The ``exception`` attribute of the request will
169    be an instance of the :exc:`repoze.bfg.exceptions.Forbidden`
170    exception that caused the forbidden view to be called.  The value
171    of ``request.exception.args[0]`` will be a value explaining why the
172    forbidden was raised.  This message will be different when the
173    ``debug_authorization`` environment setting is true than it is when
174    it is false.
fab8c5 175
CM 176 .. warning:: the default forbidden view sends a response with a ``401
177    Unauthorized`` status code for backwards compatibility reasons.
178    You can influence the status code of Forbidden responses by using
727d34 179    an alternate forbidden view.  For example, it would make sense to
fab8c5 180    return a response with a ``403 Forbidden`` status code.
8c56ae 181
CM 182 .. index::
c5f24b 183    single: traverser
def444 184
80a25e 185 .. _changing_the_traverser:
CM 186
187 Changing the Traverser
188 ----------------------
189
190 The default :term:`traversal` algorithm that BFG uses is explained in
223d4c 191 :ref:`traversal_algorithm`.  Though it is rarely necessary, this
CM 192 default algorithm can be swapped out selectively for a different
193 traversal pattern via configuration.
80a25e 194
CM 195 Use an ``adapter`` stanza in your application's ``configure.zcml`` to
196 change the default traverser:
197
090142 198 .. code-block:: xml
80a25e 199    :linenos:
CM 200
201     <adapter
202       factory="myapp.traversal.Traverser"
077c3c 203       provides="repoze.bfg.interfaces.ITraverser"
80a25e 204       for="*"
CM 205       />
206
207 In the example above, ``myapp.traversal.Traverser`` is assumed to be
208 a class that implements the following interface:
209
210 .. code-block:: python
211    :linenos:
212
213    class Traverser(object):
214        def __init__(self, root):
215            """ Accept the root object returned from the root factory """
216
acc776 217        def __call__(self, request):
80a25e 218            """ Return a dictionary with (at least) the keys ``root``,
CM 219            ``context``, ``view_name``, ``subpath``, ``traversed``,
220            ``virtual_root``, and ``virtual_root_path``.  These values are
221            typically the result of an object graph traversal.  ``root``
222            is the physical root object, ``context`` will be a model
223            object, ``view_name`` will be the view name used (a Unicode
224            name), ``subpath`` will be a sequence of Unicode names that
225            followed the view name but were not traversed, ``traversed``
226            will be a sequence of Unicode names that were traversed
227            (including the virtual root path, if any) ``virtual_root``
228            will be a model object representing the virtual root (or the
229            physical root if traversal was not performed), and
230            ``virtual_root_path`` will be a sequence representing the
231            virtual root path (a sequence of Unicode names) or None if
232            traversal was not performed.
233
234            Extra keys for special purpose functionality can be added as
235            necessary.
236
237            All values returned in the dictionary will be made available
238            as attributes of the ``request`` object.
239            """
240
acc776 241 .. warning:: In :mod:`repoze.bfg.` 1.0 and previous versions, the
CM 242      traverser ``__call__`` method accepted a WSGI *environment*
243      dictionary rather than a :term:`request` object.  The request
244      object passed to the traverser implements a dictionary-like API
245      which mutates and queries the environment, as a backwards
246      compatibility shim, in order to allow older code to work.
247      However, for maximum forward compatibility, traverser code
248      targeting :mod:`repoze.bfg` 1.1 and higher should expect a
249      request object directly.
250
80a25e 251 More than one traversal algorithm can be active at the same time.  For
CM 252 instance, if your :term:`root factory` returns more than one type of
eecdbc 253 object conditionally, you could claim that an alternate traverser
CM 254 adapter is ``for`` only one particular class or interface.  When the
255 root factory returned an object that implemented that class or
256 interface, a custom traverser would be used.  Otherwise, the default
257 traverser would be used.  For example:
80a25e 258
090142 259 .. code-block:: xml
80a25e 260    :linenos:
CM 261
262     <adapter
263       factory="myapp.traversal.Traverser"
077c3c 264       provides="repoze.bfg.interfaces.ITraverser"
80a25e 265       for="myapp.models.MyRoot"
CM 266       />
267
268 If the above stanza was added to a ``configure.zcml`` file,
269 :mod:`repoze.bfg` would use the ``myapp.traversal.Traverser`` only
270 when the application :term:`root factory` returned an instance of the
271 ``myapp.models.MyRoot`` object.  Otherwise it would use the default
272 :mod:`repoze.bfg` traverser to do traversal.
273
274 Example implementations of alternate traversers can be found "in the
275 wild" within `repoze.bfg.traversalwrapper
276 <http://pypi.python.org/pypi/repoze.bfg.traversalwrapper>`_ and
277 `repoze.bfg.metatg <http://svn.repoze.org/repoze.bfg.metatg/trunk/>`_.
278
8c56ae 279 .. index::
c5f24b 280    single: url generator
8c56ae 281
80a25e 282 Changing How :mod:`repoze.bfg.url.model_url` Generates a URL
CM 283 ------------------------------------------------------------
284
285 When you add a traverser as described in
286 :ref:`changing_the_traverser`, it's often convenient to continue to
cb8e34 287 use the :func:`repoze.bfg.url.model_url` API.  However, since the way
80a25e 288 traversal is done will have been modified, the URLs it generates by
CM 289 default may be incorrect.
290
cb8e34 291 If you've added a traverser, you can change how
CM 292 :func:`repoze.bfg.url.model_url` generates a URL for a specific type
293 of :term:`context` by adding an adapter stanza for
294 :class:`repoze.bfg.interfaces.IContextURL` to your application's
80a25e 295 ``configure.zcml``:
CM 296
090142 297 .. code-block:: xml
80a25e 298    :linenos:
CM 299
300     <adapter
301       factory="myapp.traversal.URLGenerator"
302       provides="repoze.bfg.interfaces.IContextURL"
303       for="myapp.models.MyRoot *"
304       />
305
306 In the above example, the ``myapp.traversal.URLGenerator`` class will
cb8e34 307 be used to provide services to :func:`repoze.bfg.url.model_url` any
CM 308 time the :term:`context` passed to ``model_url`` is of class
80a25e 309 ``myapp.models.MyRoot``.  The asterisk following represents the type
CM 310 of interface that must be possessed by the :term:`request` (in this
311 case, any interface, represented by asterisk).
312
313 The API that must be implemented by a class that provides
cb8e34 314 :class:`repoze.bfg.interfaces.IContextURL` is as follows:
80a25e 315
CM 316 .. code-block:: python
317   :linenos:
318
0ac7b0 319   from zope.interface import Interface
CM 320
ca866f 321   class IContextURL(Interface):
CM 322       """ An adapter which deals with URLs related to a context.
323       """
324       def __init__(self, context, request):
325           """ Accept the context and request """
80a25e 326
ca866f 327       def virtual_root(self):
CM 328           """ Return the virtual root object related to a request and the
329           current context"""
80a25e 330
ca866f 331       def __call__(self):
CM 332           """ Return a URL that points to the context """
80a25e 333
CM 334 The default context URL generator is available for perusal as the
cb8e34 335 class :class:`repoze.bfg.traversal.TraversalContextURL` in the
CM 336 `traversal module
80a25e 337 <http://svn.repoze.org/repoze.bfg/trunk/repoze/bfg/traversal.py>`_ of
601289 338 the :term:`Repoze` Subversion repository.
7aa7fd 339
6bc662 340 .. _registering_configuration_decorators:
CM 341
7200cb 342 Registering Configuration Decorators
CM 343 ------------------------------------
7aa7fd 344
7200cb 345 Decorators such as :class:`repoze.bfg.view.bfg_view` don't change the
CM 346 behavior of the functions or classes they're decorating.  Instead,
347 when a :term:`scan` is performed, a modified version of the function
348 or class is registered with :mod:`repoze.bfg`.
7aa7fd 349
7200cb 350 You may wish to have your own decorators that offer such
7aa7fd 351 behaviour. This is possible by using the :term:`Venusian` package in
7200cb 352 the same way that it is used by :mod:`repoze.bfg`.
7aa7fd 353
7200cb 354 By way of example, let's suppose you want to write a decorator that
CM 355 registers the function it wraps with a :term:`Zope Component
356 Architecture` "utility" within the :term:`application registry`
357 provided by :mod:`repoze.bfg`. The application registry and the
358 utility inside the registry is likely only to be available once your
7aa7fd 359 application's configuration is at least partially completed. A normal
7200cb 360 decorator would fail as it would be executed before the configuration
CM 361 had even begun.
7aa7fd 362
CW 363 However, using :term:`Venusian`, the decorator could be written as
364 follows:
365
366 .. code-block:: python
367    :linenos:
368
369    import venusian
7200cb 370    from repoze.bfg.threadlocal import get_current_registry
CM 371    from mypackage.interfaces import IMyUtility
7aa7fd 372     
CW 373    class registerFunction(object):
374         
c70c74 375        def __init__(self, path):
7aa7fd 376            self.path = path
CW 377
c70c74 378        def register(self, scanner, name, wrapped):
7200cb 379            registry = get_current_registry()
CM 380            registry.getUtility(IMyUtility).register(
381                self.path, wrapped
7aa7fd 382                )
CW 383         
c70c74 384        def __call__(self, wrapped):
7200cb 385            venusian.attach(wrapped, self.register)
7aa7fd 386            return wrapped
CW 387     
388 This decorator could then be used to register functions throughout
389 your code:
390
391 .. code-block:: python
392    :linenos:
393
394    @registerFunction('/some/path')
395    def my_function():
396       do_stuff()
397
7200cb 398 However, the utility would only be looked up when a :term:`scan` was
7aa7fd 399 performed, enabling you to set up the utility in advance:
CW 400
401 .. code-block:: python
402    :linenos:
403
404    from paste.httpserver import serve
405    from repoze.bfg.configuration import Configurator
406
407    class UtilityImplementation:
408
409        implements(ISomething)
410
411        def __init__(self):
412           self.registrations = {}
413
414        def register(self,path,callable_):
415           self.registrations[path]=callable_
416
417    if __name__ == '__main__':
418        config = Configurator()
419        config.begin()
420        config.registry.registerUtility(UtilityImplementation())
421        config.scan()
422        config.end()
423        app = config.make_wsgi_app()
424        serve(app, host='0.0.0.0')
425
7200cb 426 For full details, please read the `Venusian documentation
CM 427 <http://docs.repoze.org/venusian>`_.
6bc662 428
CM 429 .. note::
430
431    Application-developer-registerable configuration decorators were
432    introduced in :mod:`repoze.bfg` 1.3.