diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-10-14 11:14:44 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-10-14 11:14:44 +0000 |
| commit | 083422c0c66c1aa53f9d96c6fd185e238bc51708 (patch) | |
| tree | dfed56fdd9d595e1d081370791eeb3a30b307904 | |
| parent | 069db05dec4d8352d37cf05cf3c6ffd04b69fc91 (diff) | |
| download | pyramid-083422c0c66c1aa53f9d96c6fd185e238bc51708.tar.gz pyramid-083422c0c66c1aa53f9d96c6fd185e238bc51708.tar.bz2 pyramid-083422c0c66c1aa53f9d96c6fd185e238bc51708.zip | |
- Add ``xhr``, ``accept``, and ``header`` view configuration
predicates to ZCML view declaration, ZCML route declaration, and
``bfg_view`` decorator. See the ``Views`` narrative documentation
chapter for more information about these predicates.
| -rw-r--r-- | CHANGES.txt | 11 | ||||
| -rw-r--r-- | docs/narr/urldispatch.rst | 50 | ||||
| -rw-r--r-- | docs/narr/views.rst | 72 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_zcml.py | 620 | ||||
| -rw-r--r-- | repoze/bfg/view.py | 30 | ||||
| -rw-r--r-- | repoze/bfg/zcml.py | 91 |
6 files changed, 743 insertions, 131 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index d072cd79c..d045d261a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,12 +1,23 @@ Next release ============ +Features +-------- + +- Add ``xhr``, ``accept``, and ``header`` view configuration + predicates to ZCML view declaration, ZCML route declaration, and + ``bfg_view`` decorator. See the ``Views`` narrative documentation + chapter for more information about these predicates. + Documentation ------------- - Virtual hosting narrative docs chapter updated with info about ``mod_wsgi``. +- Point all index URLs at the literal 1.1 index (this alpha cycle may + go on a while). + 1.1a5 (2009-10-10) ================== diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index a43d5e125..7a2a184f7 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -215,6 +215,56 @@ view_renderer .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. +view_xhr + + Thie value should be either ``True`` or ``False``. If this value is + specified and is ``True``, the :term:`request` must possess an + ``HTTP_X_REQUESTED_WITH`` (aka ``X-Requested-With``) header for this + view to be found and called. This is useful for detecting AJAX + requests issued from jQuery, Prototype and other Javascript + libraries. + + This attribute can also be spelled as ``xhr``. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. + +view_accept + + The value of this attribute represents a match query for one or more + mimetypes in the ``Accept`` HTTP request header. If this value is + specified, it must be in one of the following forms: a mimetype + match token in the form ``text/plain``, a wildcard mimetype match + token in the form ``text/*`` or a match-all wildcard mimetype match + token in the form ``*/*``. If any of the forms matches the + ``Accept`` header of the request, this predicate will be true. + + This attribute can also be spelled as ``accept``. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. + +view_header + + The value of this attribute represents an HTTP header name or a + header name/value pair. If the value contains a ``:`` (colon), it + will be considered a name/value pair (e.g. ``User-Agent:Mozilla/.*`` + or ``Host:localhost``). The *value* of an attribute that represent + a name/value pair should be a regular expression. If the value does + not contain a colon, the entire value will be considered to be the + header name (e.g. ``If-Modified-Since``). If the value evaluates to + a header name only without a value, the header specified by the name + must be present in the request for this predicate to be true. If + the value evaluates to a header name/value pair, the header + specified by the name must be present in the request *and* the + regular expression specified as the value must match the header + value. Whether or not the value represents a header name or a + header name/value pair, the case of the header name is not + significant. + + This attribute can also be spelled as ``header``. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. + + The Matchdict ------------- diff --git a/docs/narr/views.rst b/docs/narr/views.rst index c8c779380..3979ec8f6 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -472,6 +472,49 @@ request_type ``request_method`` attribute instead for maximum forward compatibility. +xhr + + Thie value should be either ``True`` or ``False``. If this value is + specified and is ``True``, the :term:`request` must possess an + ``HTTP_X_REQUESTED_WITH`` (aka ``X-Requested-With``) header that has + the value ``XMLHttpRequest`` for this view to be found and called. + This is useful for detecting AJAX requests issued from jQuery, + Prototype and other Javascript libraries. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. + +accept + + The value of this attribute represents a match query for one or more + mimetypes in the ``Accept`` HTTP request header. If this value is + specified, it must be in one of the following forms: a mimetype + match token in the form ``text/plain``, a wildcard mimetype match + token in the form ``text/*`` or a match-all wildcard mimetype match + token in the form ``*/*``. If any of the forms matches the + ``Accept`` header of the request, this predicate will be true. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. + +header + + The value of this attribute represents an HTTP header name or a + header name/value pair. If the value contains a ``:`` (colon), it + will be considered a name/value pair (e.g. ``User-Agent:Mozilla/.*`` + or ``Host:localhost``). The *value* of an attribute that represent + a name/value pair should be a regular expression. If the value does + not contain a colon, the entire value will be considered to be the + header name (e.g. ``If-Modified-Since``). If the value evaluates to + a header name only without a value, the header specified by the name + must be present in the request for this predicate to be true. If + the value evaluates to a header name/value pair, the header + specified by the name must be present in the request *and* the + regular expression specified as the value must match the header + value. Whether or not the value represents a header name or a + header name/value pair, the case of the header name is not + significant. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. + .. _mapping_views_to_urls_using_a_decorator_section: View Configuration Using the ``@bfg_view`` Decorator @@ -484,10 +527,11 @@ functions with URLs instead of using :term:`ZCML` for the same purpose. ``repoze.bfg.view.bfg_view`` can be used to associate ``for``, ``name``, ``permission`` and ``request_method``, ``containment``, ``request_param`` and ``request_type``, ``attr``, -``renderer``, and ``wrapper`` information -- as done via the -equivalent ZCML -- with a function that acts as a :mod:`repoze.bfg` -view. All ZCML attributes (save for the ``view`` attribute) are -available in decorator form and mean precisely the same thing. +``renderer``, ``wrapper``, ``xhr``, ``accept``, and ``header`` +information -- as done via the equivalent ZCML -- with a function that +acts as a :mod:`repoze.bfg` view. All ZCML attributes (save for the +``view`` attribute) are available in decorator form and mean precisely +the same thing. To make :mod:`repoze.bfg` process your ``@bfg_view`` declarations, you *must* insert the following boilerplate into your application's @@ -582,6 +626,26 @@ If ``containment`` is supplied, the view will be invoked only if a location parent supplies the interface or class implied by the provided value. +If ``xhr`` is specified, it must be a boolean value. If the value is +``True``, the view will only be invoked if the request's +``X-Requested-With`` header has the value ``XMLHttpRequest``. + +If ``accept`` is specified, it must be a mimetype value. If +``accept`` is specified, the view will only be invoked if the +``Accept`` HTTP header matches the value requested. See the +description of ``accept`` in :ref:`the_view_zcml_directive` for +information about the allowable composition and matching behavior of +this value. + +If ``header`` is specified, it must be a header name or a +``headername:headervalue`` pair. If ``header`` is specified, and +possesses a value the view will only be invoked if an HTTP header +matches the value requested. If ``header`` is specified without a +value (a bare header name only), the view will only be invoked if the +HTTP header exists with any value in the request. See the description +of ``header`` in :ref:`the_view_zcml_directive` for information about +the allowable composition and matching behavior of this value. + View lookup ordering for views registered with the ``bfg_view`` decorator is the same as for those registered via ZCML. See :ref:`view_lookup_ordering` for more information. diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py index 7cbadd1e2..1f0fc44d8 100644 --- a/repoze/bfg/tests/test_zcml.py +++ b/repoze/bfg/tests/test_zcml.py @@ -40,7 +40,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -72,7 +72,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -109,7 +109,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -141,7 +141,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -181,7 +181,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -220,7 +220,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -260,7 +260,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -299,7 +299,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -350,7 +350,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -386,7 +386,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -415,7 +415,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IDummy, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -445,7 +445,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', Dummy, IView, None, None, None, None, None) + discrim = ('view', IFoo, '', Dummy, IView, None, None, None, None, + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -472,7 +473,7 @@ class TestViewDirective(unittest.TestCase): action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, 'GET', None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -502,7 +503,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(actions[0]['discriminator'], discrim) register = actions[0]['callable'] register() @@ -536,7 +537,7 @@ class TestViewDirective(unittest.TestCase): factory = sm.getUtility(IRouteRequest, 'foo') request_type = implementedBy(factory) discrim = ('view', IFoo, '', request_type, IView, None, None, None, - 'foo', None) + 'foo', None, False, None, None) self.assertEqual(action['discriminator'], discrim) the_view = sm.adapters.lookup((IFoo, request_type), IView, name='') request = factory({}) @@ -559,7 +560,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, 'POST', None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -586,7 +587,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, 'POST', None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -612,7 +613,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -639,7 +640,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -665,7 +666,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -692,7 +693,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -701,6 +702,235 @@ class TestViewDirective(unittest.TestCase): request.params = {'abc':'456'} self.assertRaises(NotFound, wrapper, None, request) + def test_with_xhr_true(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, xhr=True) + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, True, None, None) + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.is_xhr = True + self.assertEqual(wrapper(None, request), '123') + + def test_with_xhr_false(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + from repoze.bfg.exceptions import NotFound + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, xhr=True) + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, True, None, None) + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.is_xhr = False + self.assertRaises(NotFound, wrapper, None, request) + + def test_with_header_badregex(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + from zope.configuration.exceptions import ConfigurationError + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self.assertRaises(ConfigurationError, self._callFUT, + context, None, IFoo, + view=view, header='Host:a\\') + + def test_with_header_noval_match(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, header='Host') + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, False, None, 'Host') + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.headers = {'Host':'whatever'} + self.assertEqual(wrapper(None, request), '123') + + def test_with_header_noval_nomatch(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + from repoze.bfg.exceptions import NotFound + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, header='Host') + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, False, None, 'Host') + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.headers = {'NotHost':'whatever'} + self.assertRaises(NotFound, wrapper, None, request) + + def test_with_header_val_match(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, header=r'Host:\d') + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, False, None, r'Host:\d') + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.headers = {'Host':'1'} + self.assertEqual(wrapper(None, request), '123') + + def test_with_header_val_nomatch(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + from repoze.bfg.exceptions import NotFound + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, header=r'Host:\d') + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, False, None, r'Host:\d') + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.headers = {'Host':'abc'} + self.assertRaises(NotFound, wrapper, None, request) + + def test_with_accept_match(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, accept='text/xml') + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, False, 'text/xml', None) + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.accept = ['text/xml'] + self.assertEqual(wrapper(None, request), '123') + + def test_with_accept_nomatch(self): + from zope.component import getSiteManager + from zope.interface import Interface + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + from repoze.bfg.exceptions import NotFound + class IFoo(Interface): + pass + context = DummyContext() + view = lambda *arg: None + sm = getSiteManager() + def view(context, request): + return '123' + self._callFUT(context, None, IFoo, view=view, accept='text/xml') + actions = context.actions + self.assertEqual(len(actions), 1) + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None, False, 'text/xml', None) + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + request = DummyRequest() + request.accept = ['text/html'] + self.assertRaises(NotFound, wrapper, None, request) + def test_with_containment_true(self): from zope.component import getSiteManager from zope.interface import directlyProvides @@ -720,7 +950,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, IFoo, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -748,7 +978,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, IFoo, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -776,7 +1006,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -911,7 +1141,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -938,7 +1168,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] discrim = ('view', Foo, '', IRequest, IView, None, None, None, None, - None) + None, False, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -1318,17 +1548,9 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], None) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], None) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], None) - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', None, '', request_type, IView, None, None, None, + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) register = view_action['callable'] register() sm = getSiteManager() @@ -1367,17 +1589,9 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], IDummy) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], None) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], None) - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', IDummy, '', request_type, IView, None, None, None, + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') request = DummyRequest() self.assertEqual(wrapped(None, request), '123') @@ -1431,17 +1645,9 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], None) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], None) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], 'GET') - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', None, '', request_type, IView, None, None, 'GET', + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1476,17 +1682,9 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], None) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], None) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], 'GET') - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', None, '', request_type, IView, None, None, 'GET', + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1522,17 +1720,9 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], None) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], None) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], 'GET') - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', None, '', request_type, IView, None, None, 'GET', + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1567,17 +1757,9 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], None) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], None) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], 'GET') - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', None, '', request_type, IView, None, None, 'GET', + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1612,17 +1794,9 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], None) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], True) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], None) - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', None, '', request_type, IView, True, None, None, + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1657,17 +1831,232 @@ class TestRouteDirective(unittest.TestCase): request_factory = sm.getUtility(IRouteRequest, 'name') request_type = implementedBy(request_factory) view_discriminator = view_action['discriminator'] - self.assertEqual(len(view_discriminator), 10) - self.assertEqual(view_discriminator[0], 'view') - self.assertEqual(view_discriminator[1], None) - self.assertEqual(view_discriminator[2],'') - self.assertEqual(view_discriminator[3], request_type) - self.assertEqual(view_discriminator[4], IView) - self.assertEqual(view_discriminator[5], True) - self.assertEqual(view_discriminator[6], None) - self.assertEqual(view_discriminator[7], None) - self.assertEqual(view_discriminator[8], 'name') - self.assertEqual(view_discriminator[9], None) + discrim = ('view', None, '', request_type, IView, True, None, None, + 'name', None, False, None, None) + self.assertEqual(view_discriminator, discrim) + wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') + self.failUnless(wrapped) + + route_action = actions[1] + route_callable = route_action['callable'] + route_discriminator = route_action['discriminator'] + route_args = route_action['args'] + self.assertEqual(route_callable, connect_route) + self.assertEqual(len(route_discriminator), 2) + self.assertEqual(route_discriminator[0], 'route') + self.assertEqual(route_discriminator[1], 'name') + self.assertEqual(route_args, ('path', 'name', None)) + + def test_with_view_header(self): + from zope.component import getSiteManager + from zope.interface import implementedBy + from repoze.bfg.zcml import connect_route + from repoze.bfg.interfaces import IView + from repoze.bfg.interfaces import IRouteRequest + + context = DummyContext() + def view(context, request): + """ """ + self._callFUT(context, 'name', 'path', view=view, view_header='Host') + actions = context.actions + self.assertEqual(len(actions), 2) + + view_action = actions[0] + register = view_action['callable'] + register() + sm = getSiteManager() + request_factory = sm.getUtility(IRouteRequest, 'name') + request_type = implementedBy(request_factory) + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', request_type, IView, None, None, None, + 'name', None, False, None, 'Host') + self.assertEqual(view_discriminator, discrim) + wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') + self.failUnless(wrapped) + + route_action = actions[1] + route_callable = route_action['callable'] + route_discriminator = route_action['discriminator'] + route_args = route_action['args'] + self.assertEqual(route_callable, connect_route) + self.assertEqual(len(route_discriminator), 2) + self.assertEqual(route_discriminator[0], 'route') + self.assertEqual(route_discriminator[1], 'name') + self.assertEqual(route_args, ('path', 'name', None)) + + def test_with_view_header_alias(self): + from zope.component import getSiteManager + from zope.interface import implementedBy + from repoze.bfg.zcml import connect_route + from repoze.bfg.interfaces import IView + from repoze.bfg.interfaces import IRouteRequest + + context = DummyContext() + def view(context, request): + """ """ + self._callFUT(context, 'name', 'path', view=view, header='Host') + actions = context.actions + self.assertEqual(len(actions), 2) + + view_action = actions[0] + register = view_action['callable'] + register() + sm = getSiteManager() + request_factory = sm.getUtility(IRouteRequest, 'name') + request_type = implementedBy(request_factory) + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', request_type, IView, None, None, None, + 'name', None, False, None, 'Host') + self.assertEqual(view_discriminator, discrim) + wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') + self.failUnless(wrapped) + + route_action = actions[1] + route_callable = route_action['callable'] + route_discriminator = route_action['discriminator'] + route_args = route_action['args'] + self.assertEqual(route_callable, connect_route) + self.assertEqual(len(route_discriminator), 2) + self.assertEqual(route_discriminator[0], 'route') + self.assertEqual(route_discriminator[1], 'name') + self.assertEqual(route_args, ('path', 'name', None)) + + def test_with_view_xhr(self): + from zope.component import getSiteManager + from zope.interface import implementedBy + from repoze.bfg.zcml import connect_route + from repoze.bfg.interfaces import IView + from repoze.bfg.interfaces import IRouteRequest + + context = DummyContext() + def view(context, request): + """ """ + self._callFUT(context, 'name', 'path', view=view, view_xhr=True) + actions = context.actions + self.assertEqual(len(actions), 2) + + view_action = actions[0] + register = view_action['callable'] + register() + sm = getSiteManager() + request_factory = sm.getUtility(IRouteRequest, 'name') + request_type = implementedBy(request_factory) + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', request_type, IView, None, None, None, + 'name', None, True, None, None) + self.assertEqual(view_discriminator, discrim) + wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') + self.failUnless(wrapped) + + route_action = actions[1] + route_callable = route_action['callable'] + route_discriminator = route_action['discriminator'] + route_args = route_action['args'] + self.assertEqual(route_callable, connect_route) + self.assertEqual(len(route_discriminator), 2) + self.assertEqual(route_discriminator[0], 'route') + self.assertEqual(route_discriminator[1], 'name') + self.assertEqual(route_args, ('path', 'name', None)) + + def test_with_view_xhr_alias(self): + from zope.component import getSiteManager + from zope.interface import implementedBy + from repoze.bfg.zcml import connect_route + from repoze.bfg.interfaces import IView + from repoze.bfg.interfaces import IRouteRequest + + context = DummyContext() + def view(context, request): + """ """ + self._callFUT(context, 'name', 'path', view=view, xhr=True) + actions = context.actions + self.assertEqual(len(actions), 2) + + view_action = actions[0] + register = view_action['callable'] + register() + sm = getSiteManager() + request_factory = sm.getUtility(IRouteRequest, 'name') + request_type = implementedBy(request_factory) + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', request_type, IView, None, None, None, + 'name', None, True, None, None) + self.assertEqual(view_discriminator, discrim) + wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') + self.failUnless(wrapped) + + route_action = actions[1] + route_callable = route_action['callable'] + route_discriminator = route_action['discriminator'] + route_args = route_action['args'] + self.assertEqual(route_callable, connect_route) + self.assertEqual(len(route_discriminator), 2) + self.assertEqual(route_discriminator[0], 'route') + self.assertEqual(route_discriminator[1], 'name') + self.assertEqual(route_args, ('path', 'name', None)) + + def test_with_view_accept(self): + from zope.component import getSiteManager + from zope.interface import implementedBy + from repoze.bfg.zcml import connect_route + from repoze.bfg.interfaces import IView + from repoze.bfg.interfaces import IRouteRequest + + context = DummyContext() + def view(context, request): + """ """ + self._callFUT(context, 'name', 'path', view=view, + view_accept='text/xml') + actions = context.actions + self.assertEqual(len(actions), 2) + + view_action = actions[0] + register = view_action['callable'] + register() + sm = getSiteManager() + request_factory = sm.getUtility(IRouteRequest, 'name') + request_type = implementedBy(request_factory) + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', request_type, IView, None, None, None, + 'name', None, False, 'text/xml', None) + self.assertEqual(view_discriminator, discrim) + wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') + self.failUnless(wrapped) + + route_action = actions[1] + route_callable = route_action['callable'] + route_discriminator = route_action['discriminator'] + route_args = route_action['args'] + self.assertEqual(route_callable, connect_route) + self.assertEqual(len(route_discriminator), 2) + self.assertEqual(route_discriminator[0], 'route') + self.assertEqual(route_discriminator[1], 'name') + self.assertEqual(route_args, ('path', 'name', None)) + + def test_with_view_xhr_alias(self): + from zope.component import getSiteManager + from zope.interface import implementedBy + from repoze.bfg.zcml import connect_route + from repoze.bfg.interfaces import IView + from repoze.bfg.interfaces import IRouteRequest + + context = DummyContext() + def view(context, request): + """ """ + self._callFUT(context, 'name', 'path', view=view, accept='text/xml') + actions = context.actions + self.assertEqual(len(actions), 2) + + view_action = actions[0] + register = view_action['callable'] + register() + sm = getSiteManager() + request_factory = sm.getUtility(IRouteRequest, 'name') + request_type = implementedBy(request_factory) + view_discriminator = view_action['discriminator'] + discrim = ('view', None, '', request_type, IView, None, None, None, + 'name', None, False, 'text/xml', None) + self.assertEqual(view_discriminator, discrim) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -2001,7 +2390,8 @@ class TestBFGViewGrokker(unittest.TestCase): request_type=IRequest, route_name=None, request_method=None, request_param=None, containment=None, attr=None, renderer=None, - wrapper_viewname=None) + wrapper_viewname=None, xhr=False, header=None, + accept=None) obj.__bfg_view_settings__ = settings context = DummyContext() result = grokker.grok('name', obj, context=context) diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py index dba0f99b9..d6c47ec0f 100644 --- a/repoze/bfg/view.py +++ b/repoze/bfg/view.py @@ -240,7 +240,8 @@ class bfg_view(object): The following arguments are supported: ``for_``, ``permission``, ``name``, ``request_type``, ``route_name``, ``request_method``, - ``request_param``, and ``containment``. + ``request_param``, ``containment``, ``xhr``, ``accept``, and + ``header``. If ``for_`` is not supplied, the interface ``zope.interface.Interface`` (matching any context) is used. @@ -295,6 +296,27 @@ class bfg_view(object): denoting that the view 'matches' the current request only if any graph lineage node possesses this class or interface. + If ``xhr`` is specified, it must be a boolean value. If the value + is ``True``, the view will only be invoked if the request's + ``X-Requested-With`` header has the value ``XMLHttpRequest``. + + If ``accept`` is specified, it must be a mimetype value. If + ``accept`` is specified, the view will only be invoked if the + ``Accept`` HTTP header matches the value requested. See the + description of ``accept`` in :ref:`the_view_zcml_directive` for + information about the allowable composition and matching behavior + of this value. + + If ``header`` is specified, it must be a header name or a + ``headername:headervalue`` pair. If ``header`` is specified, and + possesses a value the view will only be invoked if an HTTP header + matches the value requested. If ``header`` is specified without a + value (a bare header name only), the view will only be invoked if + the HTTP header exists with any value in the request. See the + description of ``header`` in :ref:`the_view_zcml_directive` for + information about the allowable composition and matching behavior + of this value. + Any individual or all parameters can be omitted. The simplest bfg_view declaration then becomes:: @@ -354,7 +376,8 @@ class bfg_view(object): """ def __init__(self, name='', request_type=None, for_=None, permission=None, route_name=None, request_method=None, request_param=None, - containment=None, attr=None, renderer=None, wrapper=None): + containment=None, attr=None, renderer=None, wrapper=None, + xhr=False, accept=None, header=None): self.name = name self.request_type = request_type self.for_ = for_ @@ -366,6 +389,9 @@ class bfg_view(object): self.attr = attr self.renderer = renderer self.wrapper_viewname = wrapper + self.xhr = xhr + self.accept = accept + self.header = header def __call__(self, wrapped): wrapped.__bfg_view_settings__ = self.__dict__.copy() diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py index adb1f1f31..b0f5a2595 100644 --- a/repoze/bfg/zcml.py +++ b/repoze/bfg/zcml.py @@ -1,3 +1,4 @@ +import re import sys from zope.configuration import xmlconfig @@ -121,6 +122,26 @@ class IViewDirective(Interface): 'parameter exists which matches this string.'), required=False) + xhr = Bool( + title = (u'True if request has an X-Requested-With header with the ' + 'value "XMLHttpRequest"'), + description=(u'Useful for detecting AJAX requests issued from ' + 'jQuery, Protoype and other JavaScript libraries'), + required=False) + + accept = TextLine( + title = (u'Mimetype(s) that must be present in "Accept" HTTP header ' + 'for the view to match a request'), + description=(u'Accepts a mimetype match token in the form ' + '"text/plain", a wildcard mimetype match token in the ' + 'form "text/*" or a match-all wildcard mimetype match ' + 'token in the form "*/*".'), + required = False) + + header = TextLine( + title=u'Header name/value pair in the form "name=<regex>"', + description=u'Regular expression matching for header values', + required = False) def view( _context, @@ -136,6 +157,9 @@ def view( attr=None, renderer=None, wrapper=None, + xhr=False, + accept=None, + header=None, cacheable=True, # not used, here for b/w compat < 0.8 ): @@ -167,9 +191,6 @@ def view( if isinstance(request_type, basestring): request_type = _context.resolve(request_type) - predicates = [] - weight = sys.maxint - # Predicates are added to the predicate list in (presumed) # computation expense order. All predicates associated with a # view must evaluate true for the view to "match" a request. @@ -196,10 +217,19 @@ def view( # Views which do not have any predicates get a score of # sys.maxint, meaning that they will be tried very last. + predicates = [] + weight = sys.maxint + + if xhr: + def xhr_predicate(context, request): + return request.is_xhr + weight = weight - 10 + predicates.append(xhr_predicate) + if request_method is not None: def request_method_predicate(context, request): return request.method == request_method - weight = weight - 10 + weight = weight - 20 predicates.append(request_method_predicate) if request_param is not None: @@ -210,13 +240,35 @@ def view( if request_param_val is None: return request_param in request.params return request.params.get(request_param) == request_param_val - weight = weight - 20 + weight = weight - 30 predicates.append(request_param_predicate) + if header is not None: + header_name = header + header_val = None + if ':' in header: + header_name, header_val = header.split(':', 1) + try: + header_val = re.compile(header_val) + except re.error, why: + raise ConfigurationError(why[0]) + def header_predicate(context, request): + if header_val is None: + return header_name in request.headers + val = request.headers.get(header_name) + return header_val.match(val) is not None + predicates.append(header_predicate) + + if accept is not None: + def accept_predicate(context, request): + return accept in request.accept + weight = weight - 40 + predicates.append(accept_predicate) + if containment is not None: def containment_predicate(context, request): return find_interface(context, containment) is not None - weight = weight - 30 + weight = weight - 50 predicates.append(containment_predicate) # this will be == sys.maxint if no predicates @@ -272,7 +324,8 @@ def view( name, _context.info) _context.action( discriminator = ('view', for_, name, request_type, IView, containment, - request_param, request_method, route_name, attr), + request_param, request_method, route_name, attr, + xhr, accept, header), callable = register, args = (), ) @@ -482,6 +535,9 @@ class IRouteDirective(Interface): required=False) view_attr = TextLine(title=u'view_attr', required=False) view_renderer = TextLine(title=u'view_renderer', required=False) + view_header = TextLine(title=u'view_header', required=False) + view_accept = TextLine(title=u'view_accept', required=False) + view_xhr = Bool(title=u'view_xhr', required=False) # alias for "view_for" for_ = GlobalObject(title=u'for', required=False) # alias for "view_permission" @@ -500,6 +556,12 @@ class IRouteDirective(Interface): attr = TextLine(title=u'attr', required=False) # alias for "view_renderer" renderer = TextLine(title=u'renderer', required=False) + # alias for "view_header" + header = TextLine(title=u'header', required=False) + # alias for "view_accept" + accept = TextLine(title=u'accept', required=False) + # alias for "view_xhr" + xhr = Bool(title=u'xhr', required=False) class IRouteRequirementDirective(Interface): """ The interface for the ``requirement`` route subdirective """ @@ -513,7 +575,8 @@ def route(_context, name, path, view=None, view_for=None, request_method=None, view_request_method=None, request_param=None, view_request_param=None, containment=None, view_containment=None, attr=None, view_attr=None, renderer=None, - view_renderer=None): + view_renderer=None, header=None, view_header=None, accept=None, + view_accept=None, xhr=False, view_xhr=False): """ Handle ``route`` ZCML directives """ # the strange ordering of the request kw args above is for b/w @@ -526,6 +589,9 @@ def route(_context, name, path, view=None, view_for=None, containment = view_containment or containment attr = view_attr or attr renderer = view_renderer or renderer + header = view_header or header + accept = view_accept or accept + xhr = view_xhr or xhr sm = getSiteManager() @@ -545,7 +611,8 @@ def route(_context, name, path, view=None, view_for=None, _view(_context, permission=permission, for_=for_, view=view, name='', request_type=request_type, route_name=name, request_method=request_method, request_param=request_param, - containment=containment, attr=attr, renderer=renderer) + containment=containment, attr=attr, renderer=renderer, + header=header, accept=accept, xhr=xhr) _context.action( discriminator = ('route', name), @@ -641,12 +708,16 @@ class BFGViewGrokker(martian.InstanceGrokker): wrapper = settings['wrapper_viewname'] attr = settings['attr'] renderer = settings['renderer'] + xhr = settings['xhr'] + accept = settings['accept'] + header = settings['header'] context = kw['context'] view(context, permission=permission, for_=for_, view=obj, name=name, request_type=request_type, route_name=route_name, request_method=request_method, request_param=request_param, containment=containment, - attr=attr, renderer=renderer, wrapper=wrapper) + attr=attr, renderer=renderer, wrapper=wrapper, + xhr=xhr, accept=accept, header=header) return True return False |
