diff options
| -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 " |
