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