diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-09-14 04:33:38 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-09-14 04:33:38 +0000 |
| commit | 04e182bbc0c077afcd921f0df4231020549dc217 (patch) | |
| tree | a994d2508aaac376b76e04dd4335129b6391dca1 | |
| parent | f587c76deac60c0a328975dcc4641d0f85984e63 (diff) | |
| download | pyramid-04e182bbc0c077afcd921f0df4231020549dc217.tar.gz pyramid-04e182bbc0c077afcd921f0df4231020549dc217.tar.bz2 pyramid-04e182bbc0c077afcd921f0df4231020549dc217.zip | |
- A ZCML ``view`` directive (and the associated ``bfg_view``
decorator) can now accept an "attr" value. If an "attr" value is
supplied, it is considered a method named of the view object to be
called when the response is required. This is typically only good
for views that are classes or instances (not so useful for
functions, as functions typically have no methods other than
``__call__``).
- A ZCML ``view`` directive (and the associated ``bfg_view``
decorator) can now accept a "template" value. If a "template" value
is supplied, and the view callable returns a dictionary, the
associated template is rendered with the dictionary as keyword
arguments.
| -rw-r--r-- | CHANGES.txt | 17 | ||||
| -rw-r--r-- | docs/narr/views.rst | 185 | ||||
| -rw-r--r-- | repoze/bfg/chameleon_text.py | 43 | ||||
| -rw-r--r-- | repoze/bfg/chameleon_zpt.py | 7 | ||||
| -rw-r--r-- | repoze/bfg/includes/configure.zcml | 7 | ||||
| -rw-r--r-- | repoze/bfg/includes/meta.zcml | 6 | ||||
| -rw-r--r-- | repoze/bfg/templating.py | 43 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_chameleon_text.py | 7 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_templating.py | 31 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_view.py | 264 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_zcml.py | 149 | ||||
| -rw-r--r-- | repoze/bfg/view.py | 106 | ||||
| -rw-r--r-- | repoze/bfg/zcml.py | 44 |
13 files changed, 805 insertions, 104 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 858770da7..65ed57635 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,20 @@ +Next release +============ + +- A ZCML ``view`` directive (and the associated ``bfg_view`` + decorator) can now accept an "attr" value. If an "attr" value is + supplied, it is considered a method named of the view object to be + called when the response is required. This is typically only good + for views that are classes or instances (not so useful for + functions, as functions typically have no methods other than + ``__call__``). + +- A ZCML ``view`` directive (and the associated ``bfg_view`` + decorator) can now accept a "template" value. If a "template" value + is supplied, and the view callable returns a dictionary, the + associated template is rendered with the dictionary as keyword + arguments. + 1.1a1 (2009-09-06) ================== diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 28fe643d8..794c36d20 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -247,6 +247,34 @@ permission call the view. See :ref:`view_security_section` for more information about view security and permissions. +attr + + The view machinery defaults to using the ``__call__`` method of the + view callable (or the function itself, if the view callable is a + funcion) to obtain a response dictionary. The ``attr`` value allows + you to vary the method attribute used to obtain the response. For + example, if your view was a class, and the class has a method named + ``index`` and you wanted to use this method instead of the class' + ``__call__`` method to return the response, you'd say + ``attr="index"`` in the page ZCML definition. This is most useful + when the page definition is a class. + +template + + This is a string implying a path to a filesystem template. Although + a path is usually just a simple relative pathname + (e.g. ``templates/foo.pt``, implying that the template is in the + "templates" directory relative to the directory in which the ZCML + file is defined), a path can be absolute, starting with a slash on + UNIX or a drive letter prefix on Windows. The path can alternately + be a :term:`resource` "specification" in the form + ``some.dotted.package_name:relative/path``, making it possible to + address template resources which live in a separate package. The + ``template`` attribute is optional. If it is not defined, no + template is assoicated with the view. See + :ref:`views_with_templates` for more information about view + templates. + request_method This value can either be one of the strings 'GET', 'POST', 'PUT', @@ -427,6 +455,13 @@ All arguments to ``bfg_view`` are optional. If ``name`` is not supplied, the empty string is used (implying the default view). +If ``attr`` is not supplied, ``None`` is used (implying the function +itself if the view is a function, or the ``__call__`` callable +attribute if the view is a class). + +If ``template`` is not supplied, ``None`` is used (meaning that no +template is associated with this view). + If ``request_type`` is not supplied, the value ``None`` is used, implying any request type. Otherwise, this should be a class or interface. @@ -515,6 +550,156 @@ decorator syntactic sugar), if you wish: my_view = bfg_view()(MyView) +.. _views_with_templates: + +Views That Have a ``template`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using a ``view`` with an associated ``template`` attribute differs +from using a ``view`` without an associated ``template`` in a number +of important ways: + +- When the ``template`` attribute is used, the BFG view machinery + finds and renders the template internally, unlike a view without an + associated ``template``, which, if it needs to render a template, + must find and render the template by itself. + +- When a ``template`` attribute is used, the may return a Response + object *or* a Python dictionary. This is unlike a BFG ``view`` + without an associated template, which must always return a Response + object. If a BFG view without an associated template returns a + dictionary, an error will result at rendering time. + +- If the view callable with an associated template returns a Python + dictionary, the named template will be passed the dictionary as its + keyword arguments, and the view implementation will return the + resulting rendered template in a response to the user. The callable + object (whatever object was used to define the ``view``) will be + automatically inserted into the set of keyword arguments passed to + the template as the ``view`` keyword. If the view callable was a + class, the ``view`` keyword will be an instance of that class. Also + inserted into the keywords passed to the template are + ``template_name`` (the name of the template, which may be a full + path or a package-relative name, typically the full string used in + the ``template`` atttribute of the directive), ``context`` (the + context of the view used to render the template), and ``request`` + (the request passed to the view used to render the template). None + of these default names are available to a template when the view + directive has no associated ``template`` attribute; the developer is + responsible for inserting them herself. + +- If the ``view`` callable associated with a ``view`` directive + returns a Response object (an object with the attributes ``status``, + ``headerlist`` and ``app_iter``), any template associated with the + ``page`` declaration is ignored, and the response is passed back to + BFG. For example, if your page callable returns an ``HTTPFound`` + response, no template rendering will be performed: + + .. code-block:: python + :linenos: + + from webob.exc import HTTPFound + return HTTPFound(location='http://example.com') # templating avoided + +Several keyword names in a dictionary return value of a view callable +are treated specially by :mod:`repoze.bfg`. These values are passed +through to the template during rendering, but they also influence the +response returned to the user separate from any template rendering. +Page callables should set these values into the dictionary they return +to influence response attributes. + +content_type_ + + Defines the content-type of the resulting response, + e.g. ``text/xml``. + +headerlist_ + + A sequence of tuples describing cookie values that should be set in + the response, e.g. ``[('Set-Cookie', 'abc=123'), ('X-My-Header', + 'foo')]``. + +status_ + + A WSGI-style status code (e.g. ``200 OK``) describing the status of + the response. + +charset_ + + The character set (e.g. ``UTF-8``) of the response. + +cache_for_ + + A value in seconds which will influence ``Cache-Control`` and + ``Expires`` headers in the returned response. The same can also be + achieved by returning various values in the headerlist, this is + purely a convenience. + +View Template Filename Extension Mappings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When the ``template`` attribute of a view directive is used, a +filename extension based mapping is consulted to determine which +templating renderer implementation to use. By default, a single +filename-extension-to-renderer mapping is used: any template name with +a filename extension of ".pt" is assumed to be rendered via a +Chameleon ZPT template. + +If a template renderer cannot be recognized by the extension of a +template, it will be assumed that a Chameleon text renderer should be +used to render the template. + +Adding and Overriding Template Filename Extension Mappings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Additonal declarations can be made which override a default +file-extension-to-renderer mapping or add a new +file-extension-to-renderer mapping. This is accomplished via one or +more separate ZCML directives. + +For example, to add Jinja2 rendering (after installing the +repoze.bfg.jinja2" package), whereby filenames that end in ``.jinja`` +are rendered by a Jinja2 renderer:: + + <template_renderer + extension=".jinja" + renderer="my.package.MyJinja2Renderer"/> + +To override the default mapping in which files with a ``.pt`` +extension are rendered via a Chameleon ZPT page template renderer, use +a variation on the following:: + + <template_renderer + extension=".pt" + renderer="my.package.pt_renderer"/> + +By default, when a template extension is unrecognized, the Chameleon +text templating engine is assumed. You can override the default +renderer by creating a directive which has no ``extension``:: + + <template_renderer + renderer="my.package.default_renderer"/> + +A renderer must be a class that has the following interface: + +.. code-block:: python + :linenos: + + class TemplateRendererFactory: + def __init__(self, path, auto_reload=False): + """ Constructor """ + + def implementation(self): + """ Return the object that the underlying templating system + uses to render the template; it is typically a callable that + accepts arbitrary keyword arguments and returns a string or + unicode object """ + + def __call__(self, **kw): + """ Call a the template implementation with the keywords + passed in as arguments and return the result (a string or + unicode object) """ + .. _using_model_interfaces: Using Model Interfaces diff --git a/repoze/bfg/chameleon_text.py b/repoze/bfg/chameleon_text.py index 453328d9c..f482d795f 100644 --- a/repoze/bfg/chameleon_text.py +++ b/repoze/bfg/chameleon_text.py @@ -2,50 +2,11 @@ from webob import Response from zope.component import queryUtility -from zope.interface import classProvides -from zope.interface import implements - from repoze.bfg.interfaces import IResponseFactory -from repoze.bfg.interfaces import ITemplateRenderer -from repoze.bfg.interfaces import ITemplateRendererFactory -from repoze.bfg.interfaces import ISettings from repoze.bfg.templating import renderer_from_cache - -from chameleon.core.template import TemplateFile -from chameleon.zpt.language import Parser - -class TextTemplateFile(TemplateFile): - default_parser = Parser() - - def __init__(self, filename, parser=None, format=None, - doctype=None, **kwargs): - if parser is None: - parser = self.default_parser - super(TextTemplateFile, self).__init__(filename, parser, format, - doctype, **kwargs) - -class TextTemplateRenderer(object): - classProvides(ITemplateRendererFactory) - implements(ITemplateRenderer) - - def __init__(self, path, auto_reload=False): - self.template = TextTemplateFile( - path, - format='text', - auto_reload=auto_reload - ) - - def implementation(self): - return self.template - - def __call__(self, **kw): - return self.template(**kw) - -def _auto_reload(): - settings = queryUtility(ISettings) - auto_reload = settings and settings.reload_templates - return auto_reload +from repoze.bfg.templating import TextTemplateRenderer +from repoze.bfg.templating import _auto_reload def get_renderer(path): """ Return a callable ``ITemplateRenderer`` object representing a diff --git a/repoze/bfg/chameleon_zpt.py b/repoze/bfg/chameleon_zpt.py index f4f899879..c66a69e66 100644 --- a/repoze/bfg/chameleon_zpt.py +++ b/repoze/bfg/chameleon_zpt.py @@ -8,9 +8,9 @@ from zope.interface import implements from repoze.bfg.interfaces import IResponseFactory from repoze.bfg.interfaces import ITemplateRenderer from repoze.bfg.interfaces import ITemplateRendererFactory -from repoze.bfg.interfaces import ISettings from repoze.bfg.templating import renderer_from_cache +from repoze.bfg.templating import _auto_reload from chameleon.zpt.template import PageTemplateFile class ZPTTemplateRenderer(object): @@ -26,11 +26,6 @@ class ZPTTemplateRenderer(object): def __call__(self, **kw): return self.template(**kw) -def _auto_reload(): - settings = queryUtility(ISettings) - auto_reload = settings and settings.reload_templates - return auto_reload - def get_renderer(path): """ Return a callable ``ITemplateRenderer`` object representing a ``chameleon.zpt`` template at the package-relative path (may also diff --git a/repoze/bfg/includes/configure.zcml b/repoze/bfg/includes/configure.zcml index 7e2036476..be8a69543 100644 --- a/repoze/bfg/includes/configure.zcml +++ b/repoze/bfg/includes/configure.zcml @@ -4,6 +4,8 @@ <!-- traversal adapters --> + <include file="meta.zcml" /> + <adapter factory="repoze.bfg.traversal.ModelGraphTraverser" provides="repoze.bfg.interfaces.ITraverserFactory" @@ -16,6 +18,9 @@ for="* repoze.bfg.interfaces.IRequest" /> - <include file="meta.zcml" /> + <template_renderer + renderer="repoze.bfg.chameleon_zpt.ZPTTemplateRenderer" + extension=".pt" + /> </configure> diff --git a/repoze/bfg/includes/meta.zcml b/repoze/bfg/includes/meta.zcml index 69e2eabc0..8195101be 100644 --- a/repoze/bfg/includes/meta.zcml +++ b/repoze/bfg/includes/meta.zcml @@ -70,6 +70,12 @@ handler="repoze.bfg.zcml.aclauthorizationpolicy" /> + <meta:directive + name="template_renderer" + schema="repoze.bfg.zcml.ITemplateRendererDirective" + handler="repoze.bfg.zcml.template_renderer" + /> + </meta:directives> </configure> diff --git a/repoze/bfg/templating.py b/repoze/bfg/templating.py index 1f242b306..2ec049ca5 100644 --- a/repoze/bfg/templating.py +++ b/repoze/bfg/templating.py @@ -4,10 +4,42 @@ import pkg_resources from zope.component import queryUtility from zope.component import getSiteManager +from zope.interface import classProvides +from zope.interface import implements + +from chameleon.core.template import TemplateFile +from chameleon.zpt.language import Parser + +from repoze.bfg.interfaces import ISettings from repoze.bfg.interfaces import ITemplateRenderer +from repoze.bfg.interfaces import ITemplateRendererFactory from repoze.bfg.path import caller_package from repoze.bfg.settings import get_settings +class TextTemplateFile(TemplateFile): + default_parser = Parser() + + def __init__(self, filename, parser=None, format=None, doctype=None, + **kwargs): + if parser is None: + parser = self.default_parser + super(TextTemplateFile, self).__init__(filename, parser, format, + doctype, **kwargs) + +class TextTemplateRenderer(object): + classProvides(ITemplateRendererFactory) + implements(ITemplateRenderer) + + def __init__(self, path, auto_reload=False): + self.template = TextTemplateFile(path, format='text', + auto_reload=auto_reload) + + def implementation(self): + return self.template + + def __call__(self, **kw): + return self.template(**kw) + def renderer_from_cache(path, factory, level=3, **kw): if os.path.isabs(path): # 'path' is an absolute filename (not common and largely only @@ -50,3 +82,14 @@ def renderer_from_cache(path, factory, level=3, **kw): return renderer +def renderer_from_path(path, level=4, **kw): + extension = os.path.splitext(path)[1] + factory = queryUtility(ITemplateRendererFactory, name=extension, + default=TextTemplateRenderer) + return renderer_from_cache(path, factory, level, **kw) + +def _auto_reload(): + settings = queryUtility(ISettings) + auto_reload = settings and settings['reload_templates'] + return auto_reload + diff --git a/repoze/bfg/tests/test_chameleon_text.py b/repoze/bfg/tests/test_chameleon_text.py index 74d348d14..139a77072 100644 --- a/repoze/bfg/tests/test_chameleon_text.py +++ b/repoze/bfg/tests/test_chameleon_text.py @@ -5,6 +5,13 @@ from repoze.bfg.testing import cleanUp class Base: def setUp(self): cleanUp() + import os + try: + # avoid spew from chameleon logger? + os.unlink(self._getTemplatePath('minimal.txt.py')) + except: + pass + def tearDown(self): cleanUp() diff --git a/repoze/bfg/tests/test_templating.py b/repoze/bfg/tests/test_templating.py index 6b317fb7d..c3894cce4 100644 --- a/repoze/bfg/tests/test_templating.py +++ b/repoze/bfg/tests/test_templating.py @@ -142,6 +142,37 @@ class TestRendererFromCache(unittest.TestCase): self.assertNotEqual(queryUtility(ITemplateRenderer, name=spec), None) +class TestRendererFromPath(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, path, level=4, **kw): + from repoze.bfg.templating import renderer_from_path + return renderer_from_path(path, level, **kw) + + def test_with_default(self): + from repoze.bfg.templating import TextTemplateRenderer + import os + here = os.path.dirname(os.path.abspath(__file__)) + fixture = os.path.join(here, 'fixtures/minimal.txt') + result = self._callFUT(fixture) + self.assertEqual(result.__class__, TextTemplateRenderer) + + def test_with_nondefault(self): + from repoze.bfg.interfaces import ITemplateRendererFactory + import os + here = os.path.dirname(os.path.abspath(__file__)) + fixture = os.path.join(here, 'fixtures/minimal.pt') + renderer = {} + def factory(path, **kw): + return renderer + testing.registerUtility(factory, ITemplateRendererFactory, name='.pt') + result = self._callFUT(fixture) + self.assertEqual(result, renderer) + class DummyFactory: def __init__(self, renderer): self.renderer = renderer diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py index f496ebb36..5639e9799 100644 --- a/repoze/bfg/tests/test_view.py +++ b/repoze/bfg/tests/test_view.py @@ -620,7 +620,7 @@ class TestMultiView(unittest.TestCase): response = mv.__call_permissive__(context, request) self.assertEqual(response, expected_response) -class TestMapView(unittest.TestCase): +class Test_map_view(unittest.TestCase): def setUp(self): cleanUp() @@ -636,7 +636,22 @@ class TestMapView(unittest.TestCase): return 'OK' result = self._callFUT(view) self.failUnless(result is view) - self.assertEqual(view(None, None), 'OK') + self.assertEqual(result(None, None), 'OK') + + def test_view_as_function_with_attr(self): + def view(context, request): + """ """ + result = self._callFUT(view, attr='__name__') + self.failIf(result is view) + self.assertRaises(TypeError, result, None, None) + + def test_view_as_function_with_attr_and_template(self): + def view(context, request): + """ """ + result = self._callFUT(view, attr='__name__', + template='fixtures/minimal.txt') + self.failIf(result is view) + self.assertRaises(TypeError, result, None, None) def test_view_as_function_requestonly(self): def view(request): @@ -648,6 +663,16 @@ class TestMapView(unittest.TestCase): self.assertEqual(view.__name__, result.__name__) self.assertEqual(result(None, None), 'OK') + def test_view_as_function_requestonly_with_attr(self): + def view(request): + """ """ + result = self._callFUT(view, attr='__name__') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertRaises(TypeError, result, None, None) + def test_view_as_newstyle_class_context_and_request(self): class view(object): def __init__(self, context, request): @@ -660,10 +685,38 @@ class TestMapView(unittest.TestCase): self.assertEqual(view.__doc__, result.__doc__) self.assertEqual(view.__name__, result.__name__) self.assertEqual(result(None, None), 'OK') + + def test_view_as_newstyle_class_context_and_request_with_attr(self): + class view(object): + def __init__(self, context, request): + pass + def index(self): + return 'OK' + result = self._callFUT(view, attr='index') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None), 'OK') + + def test_view_as_newstyle_class_context_and_request_with_attr_and_template( + self): + class view(object): + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self._callFUT(view, attr='index', + template='repoze.bfg.tests:fixtures/minimal.txt') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None).body, 'Hello.\n') def test_view_as_newstyle_class_requestonly(self): class view(object): - def __init__(self, context, request): + def __init__(self, request): pass def __call__(self): return 'OK' @@ -674,6 +727,33 @@ class TestMapView(unittest.TestCase): self.assertEqual(view.__name__, result.__name__) self.assertEqual(result(None, None), 'OK') + def test_view_as_newstyle_class_requestonly_with_attr(self): + class view(object): + def __init__(self, request): + pass + def index(self): + return 'OK' + result = self._callFUT(view, attr='index') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None), 'OK') + + def test_view_as_newstyle_class_requestonly_with_attr_and_template(self): + class view(object): + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self._callFUT(view, attr='index', + template='repoze.bfg.tests:fixtures/minimal.txt') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None).body, 'Hello.\n') + def test_view_as_oldstyle_class_context_and_request(self): class view: def __init__(self, context, request): @@ -686,11 +766,39 @@ class TestMapView(unittest.TestCase): self.assertEqual(view.__doc__, result.__doc__) self.assertEqual(view.__name__, result.__name__) self.assertEqual(result(None, None), 'OK') - - def test_view_as_oldstyle_class_requestonly(self): + + def test_view_as_oldstyle_class_context_and_request_with_attr(self): class view: def __init__(self, context, request): pass + def index(self): + return 'OK' + result = self._callFUT(view, attr='index') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None), 'OK') + + def test_view_as_oldstyle_class_context_and_request_with_attr_and_template( + self): + class view: + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self._callFUT(view, attr='index', + template='repoze.bfg.tests:fixtures/minimal.txt') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None).body, 'Hello.\n') + + def test_view_as_oldstyle_class_requestonly(self): + class view: + def __init__(self, request): + pass def __call__(self): return 'OK' result = self._callFUT(view) @@ -700,6 +808,33 @@ class TestMapView(unittest.TestCase): self.assertEqual(view.__name__, result.__name__) self.assertEqual(result(None, None), 'OK') + def test_view_as_oldstyle_class_requestonly_with_attr(self): + class view: + def __init__(self, request): + pass + def index(self): + return 'OK' + result = self._callFUT(view, attr='index') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None), 'OK') + + def test_view_as_oldstyle_class_requestonly_with_attr_and_template(self): + class view: + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self._callFUT(view, attr='index', + template='repoze.bfg.tests:fixtures/minimal.txt') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result(None, None).body, 'Hello.\n') + def test_view_as_instance_context_and_request(self): class View: def __call__(self, context, request): @@ -709,6 +844,25 @@ class TestMapView(unittest.TestCase): self.failUnless(result is view) self.assertEqual(result(None, None), 'OK') + def test_view_as_instance_context_and_request_and_attr(self): + class View: + def index(self, context, request): + return 'OK' + view = View() + result = self._callFUT(view, attr='index') + self.failIf(result is view) + self.assertEqual(result(None, None), 'OK') + + def test_view_as_instance_context_and_request_attr_and_template(self): + class View: + def index(self, context, request): + return {'a':'1'} + view = View() + result = self._callFUT(view, attr='index', + template='repoze.bfg.tests:fixtures/minimal.txt') + self.failIf(result is view) + self.assertEqual(result(None, None).body, 'Hello.\n') + def test_view_as_instance_requestonly(self): class View: def __call__(self, request): @@ -721,6 +875,41 @@ class TestMapView(unittest.TestCase): self.failUnless('instance' in result.__name__) self.assertEqual(result(None, None), 'OK') + def test_view_as_instance_requestonly_with_attr(self): + class View: + def index(self, request): + return 'OK' + view = View() + result = self._callFUT(view, attr='index') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.failUnless('instance' in result.__name__) + self.assertEqual(result(None, None), 'OK') + + def test_view_as_instance_requestonly_with_attr_and_template(self): + class View: + def index(self, request): + return {'a':'1'} + view = View() + result = self._callFUT(view, attr='index', + template='repoze.bfg.tests:fixtures/minimal.txt') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.failUnless('instance' in result.__name__) + self.assertEqual(result(None, None).body, 'Hello.\n') + + def test_view_templateonly(self): + def view(context, request): + return {'a':'1'} + result = self._callFUT(view, + template='repoze.bfg.tests:fixtures/minimal.txt') + self.failIf(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(result(None, None).body, 'Hello.\n') + class TestRequestOnly(unittest.TestCase): def _callFUT(self, arg): from repoze.bfg.view import requestonly @@ -923,6 +1112,71 @@ class TestDecorateView(unittest.TestCase): self.failUnless(view1.__predicated__.im_func is view2.__predicated__.im_func) +class Test_templated_response(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, template_name, response, view=None, + context=None, request=None, auto_reload=False): + from repoze.bfg.view import templated_response + return templated_response(template_name, response, view, context, + request, auto_reload) + + def test_is_response(self): + response = DummyResponse() + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result, response) + + def test_is_not_valid_dict(self): + response = None + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result, response) + + def test_valid_dict(self): + response = {'a':'1'} + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result.body, 'Hello.\n') + + def test_with_content_type(self): + response = {'a':'1', 'content_type_':'text/nonsense'} + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result.content_type, 'text/nonsense') + + def test_with_headerlist(self): + response = {'a':'1', 'headerlist_':[('a', '1'), ('b', '2')]} + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result.headerlist, + [('Content-Type', 'text/html; charset=UTF-8'), + ('Content-Length', '7'), + ('a', '1'), + ('b', '2')]) + + def test_with_status(self): + response = {'a':'1', 'status_':'406 You Lose'} + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result.status, '406 You Lose') + + def test_with_charset(self): + response = {'a':'1', 'charset_':'UTF-16'} + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result.charset, 'UTF-16') + + def test_with_cache_for(self): + response = {'a':'1', 'cache_for_':100} + result = self._callFUT( + 'repoze.bfg.tests:fixtures/minimal.txt', response) + self.assertEqual(result.cache_control.max_age, 100) + class DummyContext: pass diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py index 9e8c70cbd..e54bd108f 100644 --- a/repoze/bfg/tests/test_zcml.py +++ b/repoze/bfg/tests/test_zcml.py @@ -1,3 +1,7 @@ +import logging + +logging.basicConfig() + import unittest from repoze.bfg.testing import cleanUp @@ -35,7 +39,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -66,7 +71,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -102,7 +108,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -133,7 +140,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -172,7 +180,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -210,7 +219,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -249,7 +259,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -287,7 +298,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -303,6 +315,45 @@ class TestViewDirective(unittest.TestCase): perm = sm.adapters.lookup((IFoo, IRequest), IViewPermission, name='') self.assertEqual(perm, None) + def test_with_reltemplate(self): + from zope.interface import Interface + from zope.component import getSiteManager + from repoze.bfg.interfaces import IRequest + from repoze.bfg.interfaces import IView + from repoze.bfg.interfaces import IViewPermission + + import repoze.bfg.tests + + context = DummyContext(repoze.bfg.tests) + class IFoo(Interface): + pass + class view(object): + def __init__(self, context, request): + self.request = request + self.context = context + + def __call__(self): + return {'a':'1'} + + import os + fixture = 'fixtures/minimal.txt' + self._callFUT(context, 'repoze.view', IFoo, view=view, template=fixture) + actions = context.actions + self.assertEqual(len(actions), 1) + + action = actions[0] + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) + self.assertEqual(action['discriminator'], discrim) + register = action['callable'] + register() + sm = getSiteManager() + wrapper = sm.adapters.lookup((IFoo, IRequest), IView, name='') + self.assertEqual(wrapper.__module__, view.__module__) + self.assertEqual(wrapper.__name__, view.__name__) + self.assertEqual(wrapper.__doc__, view.__doc__) + result = wrapper(None, None) + self.assertEqual(result.body, 'Hello.\n') def test_request_type_asinterface(self): from zope.component import getSiteManager @@ -322,7 +373,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IDummy, IView, None, None, None, None) + discrim = ('view', IFoo, '', IDummy, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -352,7 +404,7 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', Dummy, IView, None, None, None, None) + discrim = ('view', IFoo, '', Dummy, IView, None, None, None, None, None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -378,7 +430,8 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, 'GET', None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, 'GET', None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -407,7 +460,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(actions[0]['discriminator'], discrim) register = actions[0]['callable'] register() @@ -441,7 +495,7 @@ class TestViewDirective(unittest.TestCase): factory = sm.getUtility(IRouteRequest, 'foo') request_type = implementedBy(factory) discrim = ('view', IFoo, '', request_type, IView, None, None, None, - 'foo') + 'foo', None) self.assertEqual(action['discriminator'], discrim) the_view = sm.adapters.lookup((IFoo, request_type), IView, name='') request = factory({}) @@ -463,7 +517,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, 'POST', None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, 'POST', None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -489,7 +544,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, 'POST', None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, 'POST', None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -514,7 +570,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -540,7 +597,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -565,7 +623,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -591,7 +650,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, 'abc', None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -618,7 +678,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, IFoo, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, IFoo, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -645,7 +706,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, IFoo, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, IFoo, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -673,7 +735,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -715,7 +778,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None) + discrim = ('view', IFoo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -741,7 +805,8 @@ class TestViewDirective(unittest.TestCase): actions = context.actions self.assertEqual(len(actions), 1) action = actions[0] - discrim = ('view', Foo, '', IRequest, IView, None, None, None, None) + discrim = ('view', Foo, '', IRequest, IView, None, None, None, None, + None) self.assertEqual(action['discriminator'], discrim) register = action['callable'] register() @@ -1110,7 +1175,7 @@ class TestDeriveView(unittest.TestCase): def test_view_with_debug_authorization_no_authpol(self): def view(context, request): return 'OK' - self._registerSettings(debug_authorization=True) + self._registerSettings(debug_authorization=True, reload_templates=True) logger = self._registerLogger() result = self._callFUT(view, permission='view') self.assertEqual(view.__module__, result.__module__) @@ -1130,7 +1195,7 @@ class TestDeriveView(unittest.TestCase): def test_view_with_debug_authorization_no_permission(self): def view(context, request): return 'OK' - self._registerSettings(debug_authorization=True) + self._registerSettings(debug_authorization=True, reload_templates=True) self._registerSecurityPolicy(True) logger = self._registerLogger() result = self._callFUT(view) @@ -1151,7 +1216,7 @@ class TestDeriveView(unittest.TestCase): def test_view_with_debug_authorization_permission_authpol_permitted(self): def view(context, request): return 'OK' - self._registerSettings(debug_authorization=True) + self._registerSettings(debug_authorization=True, reload_templates=True) logger = self._registerLogger() self._registerSecurityPolicy(True) result = self._callFUT(view, permission='view') @@ -1172,7 +1237,7 @@ class TestDeriveView(unittest.TestCase): from repoze.bfg.security import Unauthorized def view(context, request): """ """ - self._registerSettings(debug_authorization=True) + self._registerSettings(debug_authorization=True, reload_templates=True) logger = self._registerLogger() self._registerSecurityPolicy(False) result = self._callFUT(view, permission='view') @@ -1192,7 +1257,7 @@ class TestDeriveView(unittest.TestCase): def test_view_with_debug_authorization_permission_authpol_denied2(self): def view(context, request): """ """ - self._registerSettings(debug_authorization=True) + self._registerSettings(debug_authorization=True, reload_templates=True) logger = self._registerLogger() self._registerSecurityPolicy(False) result = self._callFUT(view, permission='view') @@ -1330,7 +1395,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], None) self.assertEqual(view_discriminator[2],'') @@ -1340,6 +1405,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], None) self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) register = view_action['callable'] register() sm = getSiteManager() @@ -1378,7 +1444,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], IDummy) self.assertEqual(view_discriminator[2],'') @@ -1388,6 +1454,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], None) self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') request = DummyRequest() self.assertEqual(wrapped(None, request), '123') @@ -1441,7 +1508,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], None) self.assertEqual(view_discriminator[2],'') @@ -1451,6 +1518,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], 'GET') self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1485,7 +1553,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], None) self.assertEqual(view_discriminator[2],'') @@ -1495,6 +1563,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], 'GET') self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1530,7 +1599,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], None) self.assertEqual(view_discriminator[2],'') @@ -1540,6 +1609,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], 'GET') self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1574,7 +1644,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], None) self.assertEqual(view_discriminator[2],'') @@ -1584,6 +1654,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], 'GET') self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1618,7 +1689,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], None) self.assertEqual(view_discriminator[2],'') @@ -1628,6 +1699,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], None) self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -1662,7 +1734,7 @@ 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), 9) + self.assertEqual(len(view_discriminator), 10) self.assertEqual(view_discriminator[0], 'view') self.assertEqual(view_discriminator[1], None) self.assertEqual(view_discriminator[2],'') @@ -1672,6 +1744,7 @@ class TestRouteDirective(unittest.TestCase): self.assertEqual(view_discriminator[6], None) self.assertEqual(view_discriminator[7], None) self.assertEqual(view_discriminator[8], 'name') + self.assertEqual(view_discriminator[9], None) wrapped = sm.adapters.lookup((IDummy, request_type), IView, name='') self.failUnless(wrapped) @@ -2010,6 +2083,8 @@ class TestBFGViewFunctionGrokker(unittest.TestCase): obj.__request_method__ = None obj.__request_param__ = None obj.__containment__ = None + obj.__attr__ = None + obj.__template__ = None context = DummyContext() result = grokker.grok('name', obj, context=context) self.assertEqual(result, True) diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py index 8e6d1c7fe..794c7fa65 100644 --- a/repoze/bfg/view.py +++ b/repoze/bfg/view.py @@ -33,6 +33,9 @@ from repoze.bfg.path import caller_package from repoze.bfg.static import PackageURLParser +from repoze.bfg.templating import renderer_from_path +from repoze.bfg.templating import _auto_reload + deprecated('view_execution_permitted', "('from repoze.bfg.view import view_execution_permitted' was " "deprecated as of repoze.bfg 1.0; instead use 'from " @@ -229,6 +232,13 @@ class bfg_view(object): If ``name`` is not supplied, the empty string is used (implying the default view name). + If ``attr`` is not supplied, ``None`` is used (implying the + function itself if the view is a function, or the ``__call__`` + callable attribute if the view is a class). + + If ``template`` is not supplied, ``None`` is used (meaning that no + template is associated with this view). + If ``request_type`` is not supplied, the interface ``repoze.bfg.interfaces.IRequest`` is used, implying the standard request interface type. @@ -322,7 +332,7 @@ 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): + containment=None, attr=None, template=None): self.name = name self.request_type = request_type self.for_ = for_ @@ -331,9 +341,11 @@ class bfg_view(object): self.request_method = request_method self.request_param = request_param self.containment = containment + self.attr = attr + self.template = template def __call__(self, wrapped): - _bfg_view = map_view(wrapped) + _bfg_view = map_view(wrapped, self.attr, self.template) _bfg_view.__is_bfg_view__ = True _bfg_view.__permission__ = self.permission _bfg_view.__for__ = self.for_ @@ -413,8 +425,41 @@ class MultiView(object): continue raise NotFound(self.name) -def map_view(view): +def templated_response(template_name, response, view, context, request, + auto_reload=False): + if is_response(response): + return response + renderer = renderer_from_path(template_name, auto_reload=auto_reload) + kw = {'view':view, 'template_name':template_name, 'context':context, + 'request':request} + try: + kw.update(response) + except TypeError: + return response + result = renderer(**kw) + response_factory = queryUtility(IResponseFactory, default=Response) + response = response_factory(result) + content_type = kw.get('content_type_', None) + if content_type is not None: + response.content_type = content_type + headerlist = kw.get('headerlist_', None) + if headerlist is not None: + for k, v in headerlist: + response.headers.add(k, v) + status = kw.get('status_', None) + if status is not None: + response.status = status + charset = kw.get('charset_', None) + if charset is not None: + response.charset = charset + cache_for = kw.get('cache_for_', None) + if cache_for is not None: + response.cache_expires = cache_for + return response + +def map_view(view, attr=None, template=None): wrapped_view = view + auto_reload = _auto_reload() if inspect.isclass(view): # If the object we've located is a class, turn it into a @@ -423,33 +468,74 @@ def map_view(view): # position arguments, then immediately invoke the __call__ # method of the instance with no arguments; __call__ should # return an IResponse). - if requestonly(view): + if requestonly(view, attr): # its __init__ accepts only a single request argument, # instead of both context and request def _bfg_class_requestonly_view(context, request): inst = view(request) - return inst() + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + if template: + response = templated_response(template, response, inst, + context, request, auto_reload) + return response wrapped_view = _bfg_class_requestonly_view else: # its __init__ accepts both context and request def _bfg_class_view(context, request): inst = view(context, request) - return inst() + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + if template: + response = templated_response(template, response, inst, + context, request, auto_reload) + return response wrapped_view = _bfg_class_view - elif requestonly(view): + elif requestonly(view, attr): # its __call__ accepts only a single request argument, # instead of both context and request def _bfg_requestonly_view(context, request): - return view(request) + if attr is None: + response = view(request) + else: + response = getattr(view, attr)(request) + + if template: + response = templated_response(template, response, view, + context, request, auto_reload) + return response wrapped_view = _bfg_requestonly_view + elif attr: + def _bfg_attr_view(context, request): + response = getattr(view, attr)(context, request) + if template: + response = templated_response(template, response, view, + context, request, auto_reload) + return response + wrapped_view = _bfg_attr_view + + elif template: + def _templated_view(context, request): + response = view(context, request) + response = templated_response(template, response, view, + context, request, auto_reload) + return response + wrapped_view = _templated_view + decorate_view(wrapped_view, view) return wrapped_view -def requestonly(class_or_callable): +def requestonly(class_or_callable, attr=None): """ Return true of the class or callable accepts only a request argument, as opposed to something that accepts context, request """ + if attr is None: + attr = '__call__' if inspect.isfunction(class_or_callable): fn = class_or_callable elif inspect.isclass(class_or_callable): @@ -459,7 +545,7 @@ def requestonly(class_or_callable): return False else: try: - fn = class_or_callable.__call__ + fn = getattr(class_or_callable, attr) except AttributeError: return False diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py index f584d17fb..3a2a24819 100644 --- a/repoze/bfg/zcml.py +++ b/repoze/bfg/zcml.py @@ -39,6 +39,7 @@ from repoze.bfg.interfaces import ILogger from repoze.bfg.interfaces import IPackageOverrides from repoze.bfg.interfaces import IRequest from repoze.bfg.interfaces import IRouteRequest +from repoze.bfg.interfaces import ITemplateRendererFactory from repoze.bfg.path import package_name @@ -81,6 +82,8 @@ def view( request_method=None, request_param=None, containment=None, + attr=None, + template=None, cacheable=True, # not used, here for b/w compat < 0.8 ): @@ -164,8 +167,14 @@ def view( else: score = sys.maxint + if template and (not ':' in template) and (not os.path.isabs(template)): + # if it's not a package:relative/name and it's not an + # /absolute/path it's a relative/path; this means its relative + # to the package in which the ZCML file is defined. + template = '%s:%s' % (package_name(_context.resolve('.')), template) + def register(): - derived_view = derive_view(view, permission, predicates) + derived_view = derive_view(view, permission, predicates, attr, template) r_for_ = for_ r_request_type = request_type if r_for_ is None: @@ -210,7 +219,7 @@ def view( name, _context.info) _context.action( discriminator = ('view', for_, name, request_type, IView, containment, - request_param, request_method, route_name), + request_param, request_method, route_name, attr), callable = register, args = (), ) @@ -234,8 +243,9 @@ def notfound(_context, view): def forbidden(_context, view): view_utility(_context, view, IForbiddenView) -def derive_view(original_view, permission=None, predicates=()): - mapped_view = map_view(original_view) +def derive_view(original_view, permission=None, predicates=(), attr=None, + template=None): + mapped_view = map_view(original_view, attr, template) secured_view = secure_view(mapped_view, permission) debug_view = authdebug_view(secured_view, permission) derived_view = predicate_wrap(debug_view, predicates) @@ -530,6 +540,22 @@ def connect_route(path, name, factory): mapper = getUtility(IRoutesMapper) mapper.connect(path, name, factory) +class ITemplateRendererDirective(Interface): + renderer = GlobalObject( + title=u'ITemplateRendererFactory implementation', + required=True) + + extension = TextLine( + title=u'Filename extension (e.g. ".pt")', + required=False) + +def template_renderer(_context, renderer, extension=''): + # renderer factories must be registered eagerly so they can be + # found by the view machinery + sm = getSiteManager() + sm.registerUtility(renderer, ITemplateRendererFactory, name=extension) + _context.action(discriminator=(ITemplateRendererFactory, extension)) + class IStaticDirective(Interface): name = TextLine( title=u"The URL prefix of the static view", @@ -591,6 +617,16 @@ class IViewDirective(Interface): required=False, ) + attr = TextLine( + title=u'The callable attribute of the view object(default is __call__)', + description=u'', + required=False) + + template = TextLine( + title=u'The template asssociated with the view', + description=u'', + required=False) + request_type = TextLine( title=u"The request type string or dotted name interface for the view", description=(u"The view will be called if the interface represented by " |
