commit | author | age
|
fb2824
|
1 |
import itertools |
252fa5
|
2 |
import sys |
CM |
3 |
|
e6fa66
|
4 |
import venusian |
CM |
5 |
|
4174e4
|
6 |
from zope.interface import providedBy |
7e2c6c
|
7 |
|
0c1c39
|
8 |
from pyramid.interfaces import ( |
CM |
9 |
IRoutesMapper, |
85d801
|
10 |
IMultiView, |
MM |
11 |
ISecuredView, |
0c1c39
|
12 |
IView, |
CM |
13 |
IViewClassifier, |
17c7f4
|
14 |
IRequest, |
531428
|
15 |
IExceptionViewClassifier, |
0c1c39
|
16 |
) |
d66bfb
|
17 |
|
849196
|
18 |
from pyramid.compat import decode_path_info |
6f2f04
|
19 |
from pyramid.compat import reraise as reraise_ |
0db4a1
|
20 |
|
e8c66a
|
21 |
from pyramid.exceptions import ( |
MM |
22 |
ConfigurationError, |
|
23 |
PredicateMismatch, |
|
24 |
) |
e6c2d2
|
25 |
|
0c1c39
|
26 |
from pyramid.httpexceptions import ( |
CM |
27 |
HTTPFound, |
e045cf
|
28 |
HTTPNotFound, |
0c1c39
|
29 |
default_exceptionresponse_view, |
CM |
30 |
) |
0db4a1
|
31 |
|
dda9fa
|
32 |
from pyramid.threadlocal import ( |
BJR |
33 |
get_current_registry, |
|
34 |
manager, |
|
35 |
) |
|
36 |
|
19016b
|
37 |
from pyramid.util import hide_attrs |
70d504
|
38 |
|
f66290
|
39 |
_marker = object() |
56d0fe
|
40 |
|
7e2c6c
|
41 |
def render_view_to_response(context, request, name='', secure=True): |
b93d19
|
42 |
""" Call the :term:`view callable` configured with a :term:`view |
c5f24b
|
43 |
configuration` that matches the :term:`view name` ``name`` |
b93d19
|
44 |
registered against the specified ``context`` and ``request`` and |
CM |
45 |
return a :term:`response` object. This function will return |
|
46 |
``None`` if a corresponding :term:`view callable` cannot be found |
|
47 |
(when no :term:`view configuration` matches the combination of |
|
48 |
``name`` / ``context`` / and ``request``). |
8b1f6e
|
49 |
|
b93d19
|
50 |
If `secure`` is ``True``, and the :term:`view callable` found is |
99edc5
|
51 |
protected by a permission, the permission will be checked before calling |
CM |
52 |
the view function. If the permission check disallows view execution |
|
53 |
(based on the current :term:`authorization policy`), a |
|
54 |
:exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised. |
|
55 |
The exception's ``args`` attribute explains why the view access was |
|
56 |
disallowed. |
b93d19
|
57 |
|
7e2c6c
|
58 |
If ``secure`` is ``False``, no permission checking is done.""" |
17ce57
|
59 |
|
849196
|
60 |
registry = getattr(request, 'registry', None) |
CM |
61 |
if registry is None: |
|
62 |
registry = get_current_registry() |
d66bfb
|
63 |
|
849196
|
64 |
context_iface = providedBy(context) |
713bc5
|
65 |
# We explicitly pass in the interfaces provided by the request as |
CM |
66 |
# request_iface to _call_view; we don't want _call_view to use |
|
67 |
# request.request_iface, because render_view_to_response and friends are |
|
68 |
# pretty much limited to finding views that are not views associated with |
|
69 |
# routes, and the only thing request.request_iface is used for is to find |
|
70 |
# route-based views. The render_view_to_response API is (and always has |
|
71 |
# been) a stepchild API reserved for use of those who actually use |
|
72 |
# traversal. Doing this fixes an infinite recursion bug introduced in |
|
73 |
# Pyramid 1.6a1, and causes the render_view* APIs to behave as they did in |
|
74 |
# 1.5 and previous. We should probably provide some sort of different API |
|
75 |
# that would allow people to find views for routes. See |
|
76 |
# https://github.com/Pylons/pyramid/issues/1643 for more info. |
|
77 |
request_iface = providedBy(request) |
849196
|
78 |
|
CM |
79 |
response = _call_view( |
|
80 |
registry, |
|
81 |
request, |
|
82 |
context, |
|
83 |
context_iface, |
|
84 |
name, |
03c11e
|
85 |
secure=secure, |
713bc5
|
86 |
request_iface=request_iface, |
849196
|
87 |
) |
CM |
88 |
|
|
89 |
return response # NB: might be None |
|
90 |
|
7e2c6c
|
91 |
|
CM |
92 |
def render_view_to_iterable(context, request, name='', secure=True): |
b93d19
|
93 |
""" Call the :term:`view callable` configured with a :term:`view |
c5f24b
|
94 |
configuration` that matches the :term:`view name` ``name`` |
b93d19
|
95 |
registered against the specified ``context`` and ``request`` and |
CM |
96 |
return an iterable object which represents the body of a response. |
|
97 |
This function will return ``None`` if a corresponding :term:`view |
|
98 |
callable` cannot be found (when no :term:`view configuration` |
|
99 |
matches the combination of ``name`` / ``context`` / and |
|
100 |
``request``). Additionally, this function will raise a |
|
101 |
:exc:`ValueError` if a view function is found and called but the |
|
102 |
view function's result does not have an ``app_iter`` attribute. |
8b1f6e
|
103 |
|
3d2dd3
|
104 |
You can usually get the bytestring representation of the return value of |
CM |
105 |
this function by calling ``b''.join(iterable)``, or just use |
c81aad
|
106 |
:func:`pyramid.view.render_view` instead. |
8b1f6e
|
107 |
|
99edc5
|
108 |
If ``secure`` is ``True``, and the view is protected by a permission, the |
CM |
109 |
permission will be checked before the view function is invoked. If the |
|
110 |
permission check disallows view execution (based on the current |
|
111 |
:term:`authentication policy`), a |
|
112 |
:exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised; its |
|
113 |
``args`` attribute explains why the view access was disallowed. |
b93d19
|
114 |
|
CM |
115 |
If ``secure`` is ``False``, no permission checking is |
|
116 |
done.""" |
7e2c6c
|
117 |
response = render_view_to_response(context, request, name, secure) |
CM |
118 |
if response is None: |
|
119 |
return None |
|
120 |
return response.app_iter |
|
121 |
|
885bfb
|
122 |
def render_view(context, request, name='', secure=True): |
b93d19
|
123 |
""" Call the :term:`view callable` configured with a :term:`view |
c5f24b
|
124 |
configuration` that matches the :term:`view name` ``name`` |
27d735
|
125 |
registered against the specified ``context`` and ``request`` |
c5f24b
|
126 |
and unwind the view response's ``app_iter`` (see |
23de5b
|
127 |
:ref:`the_response`) into a single bytestring. This function will |
b93d19
|
128 |
return ``None`` if a corresponding :term:`view callable` cannot be |
CM |
129 |
found (when no :term:`view configuration` matches the combination |
|
130 |
of ``name`` / ``context`` / and ``request``). Additionally, this |
|
131 |
function will raise a :exc:`ValueError` if a view function is |
|
132 |
found and called but the view function's result does not have an |
|
133 |
``app_iter`` attribute. This function will return ``None`` if a |
|
134 |
corresponding view cannot be found. |
8b1f6e
|
135 |
|
99edc5
|
136 |
If ``secure`` is ``True``, and the view is protected by a permission, the |
CM |
137 |
permission will be checked before the view is invoked. If the permission |
|
138 |
check disallows view execution (based on the current :term:`authorization |
|
139 |
policy`), a :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be |
|
140 |
raised; its ``args`` attribute explains why the view access was |
b93d19
|
141 |
disallowed. |
CM |
142 |
|
885bfb
|
143 |
If ``secure`` is ``False``, no permission checking is done.""" |
CM |
144 |
iterable = render_view_to_iterable(context, request, name, secure) |
|
145 |
if iterable is None: |
|
146 |
return None |
0a8ea9
|
147 |
return b''.join(iterable) |
885bfb
|
148 |
|
197f0c
|
149 |
class view_config(object): |
8b1f6e
|
150 |
""" A function, class or method :term:`decorator` which allows a |
CM |
151 |
developer to create view registrations nearer to a :term:`view |
c1eb0c
|
152 |
callable` definition than use :term:`imperative |
8b1f6e
|
153 |
configuration` to do the same. |
5a7f9a
|
154 |
|
8b1f6e
|
155 |
For example, this code in a module ``views.py``:: |
5a7f9a
|
156 |
|
3e2f12
|
157 |
from resources import MyResource |
5a7f9a
|
158 |
|
3e2f12
|
159 |
@view_config(name='my_view', context=MyResource, permission='read', |
197f0c
|
160 |
route_name='site1') |
5a7f9a
|
161 |
def my_view(context, request): |
8b1f6e
|
162 |
return 'OK' |
5a7f9a
|
163 |
|
b93d19
|
164 |
Might replace the following call to the |
aff443
|
165 |
:meth:`pyramid.config.Configurator.add_view` method:: |
8b1f6e
|
166 |
|
CM |
167 |
import views |
3e2f12
|
168 |
from resources import MyResource |
CM |
169 |
config.add_view(views.my_view, context=MyResource, name='my_view', |
34606c
|
170 |
permission='read', route_name='site1') |
5a7f9a
|
171 |
|
197f0c
|
172 |
.. note: :class:`pyramid.view.view_config` is also importable, for |
CM |
173 |
backwards compatibility purposes, as the name |
|
174 |
:class:`pyramid.view.bfg_view`. |
|
175 |
|
8eb19b
|
176 |
:class:`pyramid.view.view_config` supports the following keyword |
e8c66a
|
177 |
arguments: ``context``, ``exception``, ``permission``, ``name``, |
cf7d8b
|
178 |
``request_type``, ``route_name``, ``request_method``, ``request_param``, |
CM |
179 |
``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, |
602ac1
|
180 |
``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, |
769da1
|
181 |
``require_csrf``, ``match_param``, ``check_csrf``, ``physical_path``, and |
MM |
182 |
``view_options``. |
5a7f9a
|
183 |
|
cf7d8b
|
184 |
The meanings of these arguments are the same as the arguments passed to |
4c29ef
|
185 |
:meth:`pyramid.config.Configurator.add_view`. If any argument is left |
CM |
186 |
out, its default will be the equivalent ``add_view`` default. |
0b0e74
|
187 |
|
2c5f77
|
188 |
Two additional keyword arguments which will be passed to the |
T |
189 |
:term:`venusian` ``attach`` function are ``_depth`` and ``_category``. |
|
190 |
|
|
191 |
``_depth`` is provided for people who wish to reuse this class from another |
|
192 |
decorator. The default value is ``0`` and should be specified relative to |
|
193 |
the ``view_config`` invocation. It will be passed in to the |
|
194 |
:term:`venusian` ``attach`` function as the depth of the callstack when |
|
195 |
Venusian checks if the decorator is being used in a class or module |
|
196 |
context. It's not often used, but it can be useful in this circumstance. |
|
197 |
|
|
198 |
``_category`` sets the decorator category name. It can be useful in |
|
199 |
combination with the ``category`` argument of ``scan`` to control which |
|
200 |
views should be processed. |
|
201 |
|
010971
|
202 |
See the :py:func:`venusian.attach` function in Venusian for more |
MM |
203 |
information about the ``_depth`` and ``_category`` arguments. |
2033ee
|
204 |
|
SP |
205 |
.. seealso:: |
|
206 |
|
|
207 |
See also :ref:`mapping_views_using_a_decorator_section` for |
|
208 |
details about using :class:`pyramid.view.view_config`. |
a8d71c
|
209 |
|
2033ee
|
210 |
.. warning:: |
SP |
211 |
|
|
212 |
``view_config`` will work ONLY on module top level members |
|
213 |
because of the limitation of ``venusian.Scanner.scan``. |
a8f669
|
214 |
|
5a7f9a
|
215 |
""" |
e6fa66
|
216 |
venusian = venusian # for testing injection |
8ec8e2
|
217 |
def __init__(self, **settings): |
CM |
218 |
if 'for_' in settings: |
|
219 |
if settings.get('context') is None: |
|
220 |
settings['context'] = settings['for_'] |
|
221 |
self.__dict__.update(settings) |
5a7f9a
|
222 |
|
CM |
223 |
def __call__(self, wrapped): |
e6fa66
|
224 |
settings = self.__dict__.copy() |
ed1419
|
225 |
depth = settings.pop('_depth', 0) |
2c5f77
|
226 |
category = settings.pop('_category', 'pyramid') |
e6fa66
|
227 |
|
CM |
228 |
def callback(context, name, ob): |
b2c4e0
|
229 |
config = context.config.with_package(info.module) |
CM |
230 |
config.add_view(view=ob, **settings) |
e6fa66
|
231 |
|
2c5f77
|
232 |
info = self.venusian.attach(wrapped, callback, category=category, |
ed1419
|
233 |
depth=depth + 1) |
e6fa66
|
234 |
|
CM |
235 |
if info.scope == 'class': |
32418e
|
236 |
# if the decorator was attached to a method in a class, or |
CM |
237 |
# otherwise executed at class scope, we need to set an |
|
238 |
# 'attr' into the settings if one isn't already in there |
4c29ef
|
239 |
if settings.get('attr') is None: |
e6fa66
|
240 |
settings['attr'] = wrapped.__name__ |
89968d
|
241 |
|
b2c4e0
|
242 |
settings['_info'] = info.codeinfo # fbo "action_method" |
c89bcb
|
243 |
return wrapped |
5a7f9a
|
244 |
|
33516a
|
245 |
bfg_view = view_config # bw compat (forever) |
5f4780
|
246 |
|
914abe
|
247 |
class view_defaults(view_config): |
4375cf
|
248 |
""" A class :term:`decorator` which, when applied to a class, will |
CM |
249 |
provide defaults for all view configurations that use the class. This |
|
250 |
decorator accepts all the arguments accepted by |
aaedf5
|
251 |
:meth:`pyramid.view.view_config`, and each has the same meaning. |
4375cf
|
252 |
|
CM |
253 |
See :ref:`view_defaults` for more information. |
|
254 |
""" |
a8f669
|
255 |
|
914abe
|
256 |
def __call__(self, wrapped): |
CM |
257 |
wrapped.__view_defaults__ = self.__dict__.copy() |
|
258 |
return wrapped |
|
259 |
|
d96ff9
|
260 |
class AppendSlashNotFoundViewFactory(object): |
CM |
261 |
""" There can only be one :term:`Not Found view` in any |
fd5ae9
|
262 |
:app:`Pyramid` application. Even if you use |
c81aad
|
263 |
:func:`pyramid.view.append_slash_notfound_view` as the Not |
fd5ae9
|
264 |
Found view, :app:`Pyramid` still must generate a ``404 Not |
d96ff9
|
265 |
Found`` response when it cannot redirect to a slash-appended URL; |
CM |
266 |
this not found response will be visible to site users. |
|
267 |
|
|
268 |
If you don't care what this 404 response looks like, and you only |
|
269 |
need redirections to slash-appended route URLs, you may use the |
c81aad
|
270 |
:func:`pyramid.view.append_slash_notfound_view` object as the |
d96ff9
|
271 |
Not Found view. However, if you wish to use a *custom* notfound |
CM |
272 |
view callable when a URL cannot be redirected to a slash-appended |
|
273 |
URL, you may wish to use an instance of this class as the Not |
|
274 |
Found view, supplying a :term:`view callable` to be used as the |
|
275 |
custom notfound view as the first argument to its constructor. |
|
276 |
For instance: |
|
277 |
|
|
278 |
.. code-block:: python |
|
279 |
|
99edc5
|
280 |
from pyramid.httpexceptions import HTTPNotFound |
c81aad
|
281 |
from pyramid.view import AppendSlashNotFoundViewFactory |
d96ff9
|
282 |
|
1b4360
|
283 |
def notfound_view(context, request): return HTTPNotFound('nope') |
d96ff9
|
284 |
|
CM |
285 |
custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) |
a7e625
|
286 |
config.add_view(custom_append_slash, context=HTTPNotFound) |
d96ff9
|
287 |
|
CM |
288 |
The ``notfound_view`` supplied must adhere to the two-argument |
|
289 |
view callable calling convention of ``(context, request)`` |
|
290 |
(``context`` will be the exception object). |
|
291 |
|
2033ee
|
292 |
.. deprecated:: 1.3 |
SP |
293 |
|
d96ff9
|
294 |
""" |
12b6f5
|
295 |
def __init__(self, notfound_view=None, redirect_class=HTTPFound): |
d96ff9
|
296 |
if notfound_view is None: |
CM |
297 |
notfound_view = default_exceptionresponse_view |
|
298 |
self.notfound_view = notfound_view |
12b6f5
|
299 |
self.redirect_class = redirect_class |
d96ff9
|
300 |
|
CM |
301 |
def __call__(self, context, request): |
0db4a1
|
302 |
path = decode_path_info(request.environ['PATH_INFO'] or '/') |
30e64f
|
303 |
registry = request.registry |
d96ff9
|
304 |
mapper = registry.queryUtility(IRoutesMapper) |
CM |
305 |
if mapper is not None and not path.endswith('/'): |
|
306 |
slashpath = path + '/' |
|
307 |
for route in mapper.get_routes(): |
|
308 |
if route.match(slashpath) is not None: |
b596e1
|
309 |
qs = request.query_string |
CM |
310 |
if qs: |
0db4a1
|
311 |
qs = '?' + qs |
17279b
|
312 |
return self.redirect_class(location=request.path + '/' + qs) |
d96ff9
|
313 |
return self.notfound_view(context, request) |
a9454c
|
314 |
|
d96ff9
|
315 |
append_slash_notfound_view = AppendSlashNotFoundViewFactory() |
CM |
316 |
append_slash_notfound_view.__doc__ = """\ |
|
317 |
For behavior like Django's ``APPEND_SLASH=True``, use this view as the |
|
318 |
:term:`Not Found view` in your application. |
a9454c
|
319 |
|
87a85f
|
320 |
When this view is the Not Found view (indicating that no view was found), and |
CM |
321 |
any routes have been defined in the configuration of your application, if the |
|
322 |
value of the ``PATH_INFO`` WSGI environment variable does not already end in |
|
323 |
a slash, and if the value of ``PATH_INFO`` *plus* a slash matches any route's |
|
324 |
path, do an HTTP redirect to the slash-appended PATH_INFO. Note that this |
|
325 |
will *lose* ``POST`` data information (turning it into a GET), so you |
|
326 |
shouldn't rely on this to redirect POST requests. Note also that static |
|
327 |
routes are not considered when attempting to find a matching route. |
d66bfb
|
328 |
|
c1eb0c
|
329 |
Use the :meth:`pyramid.config.Configurator.add_view` method to configure this |
CM |
330 |
view as the Not Found view:: |
8b1f6e
|
331 |
|
99edc5
|
332 |
from pyramid.httpexceptions import HTTPNotFound |
c81aad
|
333 |
from pyramid.view import append_slash_notfound_view |
a7e625
|
334 |
config.add_view(append_slash_notfound_view, context=HTTPNotFound) |
8b1f6e
|
335 |
|
2033ee
|
336 |
.. deprecated:: 1.3 |
d66bfb
|
337 |
|
d96ff9
|
338 |
""" |
d66bfb
|
339 |
|
0db4a1
|
340 |
class notfound_view_config(object): |
CM |
341 |
""" |
05e928
|
342 |
.. versionadded:: 1.3 |
0db4a1
|
343 |
|
CM |
344 |
An analogue of :class:`pyramid.view.view_config` which registers a |
e8c66a
|
345 |
:term:`Not Found View` using |
MM |
346 |
:meth:`pyramid.config.Configurator.add_notfound_view`. |
0db4a1
|
347 |
|
e450ca
|
348 |
The ``notfound_view_config`` constructor accepts most of the same arguments |
0db4a1
|
349 |
as the constructor of :class:`pyramid.view.view_config`. It can be used |
CM |
350 |
in the same places, and behaves in largely the same way, except it always |
8ec8e2
|
351 |
registers a not found exception view instead of a 'normal' view. |
0db4a1
|
352 |
|
CM |
353 |
Example: |
|
354 |
|
|
355 |
.. code-block:: python |
|
356 |
|
|
357 |
from pyramid.view import notfound_view_config |
|
358 |
from pyramid.response import Response |
a8f669
|
359 |
|
15e3b1
|
360 |
@notfound_view_config() |
0db4a1
|
361 |
def notfound(request): |
c898dd
|
362 |
return Response('Not found!', status='404 Not Found') |
0db4a1
|
363 |
|
CM |
364 |
All arguments except ``append_slash`` have the same meaning as |
|
365 |
:meth:`pyramid.view.view_config` and each predicate |
|
366 |
argument restricts the set of circumstances under which this notfound |
|
367 |
view will be invoked. |
|
368 |
|
cec2b0
|
369 |
If ``append_slash`` is ``True``, when the Not Found View is invoked, and |
0db4a1
|
370 |
the current path info does not end in a slash, the notfound logic will |
CM |
371 |
attempt to find a :term:`route` that matches the request's path info |
|
372 |
suffixed with a slash. If such a route exists, Pyramid will issue a |
|
373 |
redirect to the URL implied by the route; if it does not, Pyramid will |
|
374 |
return the result of the view callable provided as ``view``, as normal. |
|
375 |
|
24358c
|
376 |
If the argument provided as ``append_slash`` is not a boolean but |
CM |
377 |
instead implements :class:`~pyramid.interfaces.IResponse`, the |
|
378 |
append_slash logic will behave as if ``append_slash=True`` was passed, |
|
379 |
but the provided class will be used as the response class instead of |
|
380 |
the default :class:`~pyramid.httpexceptions.HTTPFound` response class |
|
381 |
when a redirect is performed. For example: |
|
382 |
|
|
383 |
.. code-block:: python |
|
384 |
|
|
385 |
from pyramid.httpexceptions import ( |
|
386 |
HTTPMovedPermanently, |
|
387 |
HTTPNotFound |
|
388 |
) |
|
389 |
|
|
390 |
@notfound_view_config(append_slash=HTTPMovedPermanently) |
|
391 |
def aview(request): |
|
392 |
return HTTPNotFound('not found') |
|
393 |
|
|
394 |
The above means that a redirect to a slash-appended route will be |
|
395 |
attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound` |
|
396 |
being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will |
|
397 |
be used` for the redirect response if a slash-appended route is found. |
|
398 |
|
0db4a1
|
399 |
See :ref:`changing_the_notfound_view` for detailed usage information. |
010971
|
400 |
|
MM |
401 |
.. versionchanged:: 1.9.1 |
|
402 |
Added the ``_depth`` and ``_category`` arguments. |
0db4a1
|
403 |
|
CM |
404 |
""" |
|
405 |
|
|
406 |
venusian = venusian |
|
407 |
|
8ec8e2
|
408 |
def __init__(self, **settings): |
CM |
409 |
self.__dict__.update(settings) |
0db4a1
|
410 |
|
CM |
411 |
def __call__(self, wrapped): |
|
412 |
settings = self.__dict__.copy() |
010971
|
413 |
depth = settings.pop('_depth', 0) |
MM |
414 |
category = settings.pop('_category', 'pyramid') |
0db4a1
|
415 |
|
CM |
416 |
def callback(context, name, ob): |
|
417 |
config = context.config.with_package(info.module) |
|
418 |
config.add_notfound_view(view=ob, **settings) |
|
419 |
|
010971
|
420 |
info = self.venusian.attach(wrapped, callback, category=category, |
MM |
421 |
depth=depth + 1) |
0db4a1
|
422 |
|
CM |
423 |
if info.scope == 'class': |
|
424 |
# if the decorator was attached to a method in a class, or |
|
425 |
# otherwise executed at class scope, we need to set an |
|
426 |
# 'attr' into the settings if one isn't already in there |
|
427 |
if settings.get('attr') is None: |
|
428 |
settings['attr'] = wrapped.__name__ |
|
429 |
|
|
430 |
settings['_info'] = info.codeinfo # fbo "action_method" |
|
431 |
return wrapped |
|
432 |
|
a7fe30
|
433 |
class forbidden_view_config(object): |
CM |
434 |
""" |
05e928
|
435 |
.. versionadded:: 1.3 |
a7fe30
|
436 |
|
CM |
437 |
An analogue of :class:`pyramid.view.view_config` which registers a |
e8c66a
|
438 |
:term:`forbidden view` using |
MM |
439 |
:meth:`pyramid.config.Configurator.add_forbidden_view`. |
a7fe30
|
440 |
|
CM |
441 |
The forbidden_view_config constructor accepts most of the same arguments |
|
442 |
as the constructor of :class:`pyramid.view.view_config`. It can be used |
|
443 |
in the same places, and behaves in largely the same way, except it always |
8ec8e2
|
444 |
registers a forbidden exception view instead of a 'normal' view. |
a7fe30
|
445 |
|
CM |
446 |
Example: |
|
447 |
|
|
448 |
.. code-block:: python |
|
449 |
|
|
450 |
from pyramid.view import forbidden_view_config |
|
451 |
from pyramid.response import Response |
a8f669
|
452 |
|
15e3b1
|
453 |
@forbidden_view_config() |
bbd3ab
|
454 |
def forbidden(request): |
59e7cc
|
455 |
return Response('You are not allowed', status='403 Forbidden') |
a7fe30
|
456 |
|
8ec8e2
|
457 |
All arguments passed to this function have the same meaning as |
CM |
458 |
:meth:`pyramid.view.view_config` and each predicate argument restricts |
|
459 |
the set of circumstances under which this notfound view will be invoked. |
a7fe30
|
460 |
|
CM |
461 |
See :ref:`changing_the_forbidden_view` for detailed usage information. |
|
462 |
|
010971
|
463 |
.. versionchanged:: 1.9.1 |
MM |
464 |
Added the ``_depth`` and ``_category`` arguments. |
|
465 |
|
a7fe30
|
466 |
""" |
CM |
467 |
|
|
468 |
venusian = venusian |
|
469 |
|
8ec8e2
|
470 |
def __init__(self, **settings): |
CM |
471 |
self.__dict__.update(settings) |
a7fe30
|
472 |
|
CM |
473 |
def __call__(self, wrapped): |
|
474 |
settings = self.__dict__.copy() |
010971
|
475 |
depth = settings.pop('_depth', 0) |
MM |
476 |
category = settings.pop('_category', 'pyramid') |
a7fe30
|
477 |
|
CM |
478 |
def callback(context, name, ob): |
|
479 |
config = context.config.with_package(info.module) |
|
480 |
config.add_forbidden_view(view=ob, **settings) |
|
481 |
|
010971
|
482 |
info = self.venusian.attach(wrapped, callback, category=category, |
MM |
483 |
depth=depth + 1) |
a7fe30
|
484 |
|
CM |
485 |
if info.scope == 'class': |
|
486 |
# if the decorator was attached to a method in a class, or |
|
487 |
# otherwise executed at class scope, we need to set an |
|
488 |
# 'attr' into the settings if one isn't already in there |
|
489 |
if settings.get('attr') is None: |
|
490 |
settings['attr'] = wrapped.__name__ |
|
491 |
|
|
492 |
settings['_info'] = info.codeinfo # fbo "action_method" |
|
493 |
return wrapped |
a8f669
|
494 |
|
93c94b
|
495 |
class exception_view_config(object): |
AL |
496 |
""" |
|
497 |
.. versionadded:: 1.8 |
|
498 |
|
|
499 |
An analogue of :class:`pyramid.view.view_config` which registers an |
e8c66a
|
500 |
:term:`exception view` using |
MM |
501 |
:meth:`pyramid.config.Configurator.add_exception_view`. |
93c94b
|
502 |
|
e8c66a
|
503 |
The ``exception_view_config`` constructor requires an exception context, |
MM |
504 |
and additionally accepts most of the same arguments as the constructor of |
93c94b
|
505 |
:class:`pyramid.view.view_config`. It can be used in the same places, |
e8c66a
|
506 |
and behaves in largely the same way, except it always registers an |
160aab
|
507 |
exception view instead of a "normal" view that dispatches on the request |
e8c66a
|
508 |
:term:`context`. |
93c94b
|
509 |
|
AL |
510 |
Example: |
|
511 |
|
|
512 |
.. code-block:: python |
|
513 |
|
|
514 |
from pyramid.view import exception_view_config |
|
515 |
from pyramid.response import Response |
|
516 |
|
e8c66a
|
517 |
@exception_view_config(ValueError, renderer='json') |
MM |
518 |
def error_view(request): |
|
519 |
return {'error': str(request.exception)} |
93c94b
|
520 |
|
AL |
521 |
All arguments passed to this function have the same meaning as |
160aab
|
522 |
:meth:`pyramid.view.view_config`, and each predicate argument restricts |
93c94b
|
523 |
the set of circumstances under which this exception view will be invoked. |
e8c66a
|
524 |
|
010971
|
525 |
.. versionchanged:: 1.9.1 |
MM |
526 |
Added the ``_depth`` and ``_category`` arguments. |
|
527 |
|
93c94b
|
528 |
""" |
74842a
|
529 |
venusian = venusian |
93c94b
|
530 |
|
e8c66a
|
531 |
def __init__(self, *args, **settings): |
MM |
532 |
if 'context' not in settings and len(args) > 0: |
|
533 |
exception, args = args[0], args[1:] |
|
534 |
settings['context'] = exception |
|
535 |
if len(args) > 0: |
|
536 |
raise ConfigurationError('unknown positional arguments') |
93c94b
|
537 |
self.__dict__.update(settings) |
AL |
538 |
|
|
539 |
def __call__(self, wrapped): |
|
540 |
settings = self.__dict__.copy() |
010971
|
541 |
depth = settings.pop('_depth', 0) |
MM |
542 |
category = settings.pop('_category', 'pyramid') |
93c94b
|
543 |
|
AL |
544 |
def callback(context, name, ob): |
|
545 |
config = context.config.with_package(info.module) |
|
546 |
config.add_exception_view(view=ob, **settings) |
|
547 |
|
010971
|
548 |
info = self.venusian.attach(wrapped, callback, category=category, |
MM |
549 |
depth=depth + 1) |
93c94b
|
550 |
|
AL |
551 |
if info.scope == 'class': |
|
552 |
# if the decorator was attached to a method in a class, or |
|
553 |
# otherwise executed at class scope, we need to set an |
160aab
|
554 |
# 'attr' in the settings if one isn't already in there |
93c94b
|
555 |
if settings.get('attr') is None: |
AL |
556 |
settings['attr'] = wrapped.__name__ |
|
557 |
|
|
558 |
settings['_info'] = info.codeinfo # fbo "action_method" |
|
559 |
return wrapped |
|
560 |
|
17c7f4
|
561 |
def _find_views( |
CM |
562 |
registry, |
|
563 |
request_iface, |
|
564 |
context_iface, |
|
565 |
view_name, |
|
566 |
view_types=None, |
|
567 |
view_classifier=None, |
|
568 |
): |
03c11e
|
569 |
if view_types is None: |
17c7f4
|
570 |
view_types = (IView, ISecuredView, IMultiView) |
03c11e
|
571 |
if view_classifier is None: |
17c7f4
|
572 |
view_classifier = IViewClassifier |
fb2824
|
573 |
registered = registry.adapters.registered |
c15cbc
|
574 |
cache = registry._view_lookup_cache |
CM |
575 |
views = cache.get((request_iface, context_iface, view_name)) |
|
576 |
if views is None: |
|
577 |
views = [] |
|
578 |
for req_type, ctx_type in itertools.product( |
|
579 |
request_iface.__sro__, context_iface.__sro__ |
|
580 |
): |
17c7f4
|
581 |
source_ifaces = (view_classifier, req_type, ctx_type) |
c15cbc
|
582 |
for view_type in view_types: |
CM |
583 |
view_callable = registered( |
|
584 |
source_ifaces, |
|
585 |
view_type, |
|
586 |
name=view_name, |
|
587 |
) |
|
588 |
if view_callable is not None: |
|
589 |
views.append(view_callable) |
|
590 |
if views: |
|
591 |
# do not cache view lookup misses. rationale: dont allow cache to |
|
592 |
# grow without bound if somebody tries to hit the site with many |
|
593 |
# missing URLs. we could use an LRU cache instead, but then |
|
594 |
# purposeful misses by an attacker would just blow out the cache |
|
595 |
# anyway. downside: misses will almost always consume more CPU than |
|
596 |
# hits in steady state. |
|
597 |
with registry._lock: |
|
598 |
cache[(request_iface, context_iface, view_name)] = views |
849196
|
599 |
|
99bc0c
|
600 |
return views |
eb3ac8
|
601 |
|
849196
|
602 |
def _call_view( |
CM |
603 |
registry, |
|
604 |
request, |
|
605 |
context, |
|
606 |
context_iface, |
|
607 |
view_name, |
17c7f4
|
608 |
view_types=None, |
CM |
609 |
view_classifier=None, |
849196
|
610 |
secure=True, |
17c7f4
|
611 |
request_iface=None, |
849196
|
612 |
): |
17c7f4
|
613 |
if request_iface is None: |
CM |
614 |
request_iface = getattr(request, 'request_iface', IRequest) |
eb3ac8
|
615 |
view_callables = _find_views( |
CM |
616 |
registry, |
17c7f4
|
617 |
request_iface, |
eb3ac8
|
618 |
context_iface, |
CM |
619 |
view_name, |
17c7f4
|
620 |
view_types=view_types, |
CM |
621 |
view_classifier=view_classifier, |
eb3ac8
|
622 |
) |
CM |
623 |
|
|
624 |
pme = None |
|
625 |
response = None |
|
626 |
|
|
627 |
for view_callable in view_callables: |
|
628 |
# look for views that meet the predicate criteria |
|
629 |
try: |
849196
|
630 |
if not secure: |
CM |
631 |
# the view will have a __call_permissive__ attribute if it's |
|
632 |
# secured; otherwise it won't. |
|
633 |
view_callable = getattr( |
|
634 |
view_callable, |
|
635 |
'__call_permissive__', |
|
636 |
view_callable |
|
637 |
) |
|
638 |
|
|
639 |
# if this view is secured, it will raise a Forbidden |
|
640 |
# appropriately if the executing user does not have the proper |
|
641 |
# permission |
eb3ac8
|
642 |
response = view_callable(context, request) |
CM |
643 |
return response |
|
644 |
except PredicateMismatch as _pme: |
|
645 |
pme = _pme |
|
646 |
|
|
647 |
if pme is not None: |
|
648 |
raise pme |
|
649 |
|
|
650 |
return response |
531428
|
651 |
|
CM |
652 |
class ViewMethodsMixin(object): |
|
653 |
""" Request methods mixin for BaseRequest having to do with executing |
|
654 |
views """ |
|
655 |
def invoke_exception_view( |
|
656 |
self, |
e40ef2
|
657 |
exc_info=None, |
531428
|
658 |
request=None, |
6f2f04
|
659 |
secure=True, |
MM |
660 |
reraise=False, |
|
661 |
): |
e40ef2
|
662 |
""" Executes an exception view related to the request it's called upon. |
CM |
663 |
The arguments it takes are these: |
|
664 |
|
|
665 |
``exc_info`` |
|
666 |
|
|
667 |
If provided, should be a 3-tuple in the form provided by |
252fa5
|
668 |
``sys.exc_info()``. If not provided, |
CM |
669 |
``sys.exc_info()`` will be called to obtain the current |
e40ef2
|
670 |
interpreter exception information. Default: ``None``. |
CM |
671 |
|
|
672 |
``request`` |
|
673 |
|
|
674 |
If the request to be used is not the same one as the instance that |
|
675 |
this method is called upon, it may be passed here. Default: |
|
676 |
``None``. |
|
677 |
|
|
678 |
``secure`` |
|
679 |
|
|
680 |
If the exception view should not be rendered if the current user |
|
681 |
does not have the appropriate permission, this should be ``True``. |
|
682 |
Default: ``True``. |
|
683 |
|
6f2f04
|
684 |
``reraise`` |
e40ef2
|
685 |
|
6f2f04
|
686 |
A boolean indicating whether the original error should be reraised |
MM |
687 |
if a :term:`response` object could not be created. If ``False`` |
|
688 |
then an :class:`pyramid.httpexceptions.HTTPNotFound`` exception |
|
689 |
will be raised. Default: ``False``. |
e40ef2
|
690 |
|
3b886e
|
691 |
If a response is generated then ``request.exception`` and |
MM |
692 |
``request.exc_info`` will be left at the values used to render the |
|
693 |
response. Otherwise the previous values for ``request.exception`` and |
|
694 |
``request.exc_info`` will be restored. |
|
695 |
|
daf06d
|
696 |
.. versionadded:: 1.7 |
MM |
697 |
|
e2e51b
|
698 |
.. versionchanged:: 1.9 |
MM |
699 |
The ``request.exception`` and ``request.exc_info`` properties will |
|
700 |
reflect the exception used to render the response where previously |
|
701 |
they were reset to the values prior to invoking the method. |
6f2f04
|
702 |
|
MM |
703 |
Also added the ``reraise`` argument. |
e2e51b
|
704 |
|
3b886e
|
705 |
""" |
531428
|
706 |
if request is None: |
CM |
707 |
request = self |
|
708 |
registry = getattr(request, 'registry', None) |
|
709 |
if registry is None: |
|
710 |
registry = get_current_registry() |
dda9fa
|
711 |
|
BJR |
712 |
if registry is None: |
|
713 |
raise RuntimeError("Unable to retrieve registry") |
|
714 |
|
e40ef2
|
715 |
if exc_info is None: |
252fa5
|
716 |
exc_info = sys.exc_info() |
dda9fa
|
717 |
|
d52257
|
718 |
exc = exc_info[1] |
ca529f
|
719 |
attrs = request.__dict__ |
d52257
|
720 |
context_iface = providedBy(exc) |
19016b
|
721 |
|
MM |
722 |
# clear old generated request.response, if any; it may |
|
723 |
# have been mutated by the view, and its state is not |
|
724 |
# sane (e.g. caching headers) |
3b886e
|
725 |
with hide_attrs(request, 'response', 'exc_info', 'exception'): |
d52257
|
726 |
attrs['exception'] = exc |
68b303
|
727 |
attrs['exc_info'] = exc_info |
19016b
|
728 |
# we use .get instead of .__getitem__ below due to |
MM |
729 |
# https://github.com/Pylons/pyramid/issues/700 |
|
730 |
request_iface = attrs.get('request_iface', IRequest) |
dda9fa
|
731 |
|
4f6635
|
732 |
manager.push({'request': request, 'registry': registry}) |
dda9fa
|
733 |
|
4f6635
|
734 |
try: |
dda9fa
|
735 |
response = _call_view( |
BJR |
736 |
registry, |
|
737 |
request, |
|
738 |
exc, |
|
739 |
context_iface, |
|
740 |
'', |
|
741 |
view_types=None, |
|
742 |
view_classifier=IExceptionViewClassifier, |
|
743 |
secure=secure, |
|
744 |
request_iface=request_iface.combined, |
|
745 |
) |
6f2f04
|
746 |
except: |
MM |
747 |
if reraise: |
|
748 |
reraise_(*exc_info) |
|
749 |
raise |
dda9fa
|
750 |
finally: |
BJR |
751 |
manager.pop() |
3b886e
|
752 |
|
MM |
753 |
if response is None: |
6f2f04
|
754 |
if reraise: |
MM |
755 |
reraise_(*exc_info) |
3b886e
|
756 |
raise HTTPNotFound |
MM |
757 |
|
|
758 |
# successful response, overwrite exception/exc_info |
|
759 |
attrs['exception'] = exc |
|
760 |
attrs['exc_info'] = exc_info |
|
761 |
return response |