diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-01-06 18:53:17 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-01-06 18:53:17 +0000 |
| commit | d20d094da95456e7939f9e77cc51f71d2d4561db (patch) | |
| tree | 5b1b8fb4aa5549599b39249f8ebafbc390e3a7d4 | |
| parent | d9423b6f9adbce7f8fdc9153644bd88be9a5b6c8 (diff) | |
| download | pyramid-d20d094da95456e7939f9e77cc51f71d2d4561db.tar.gz pyramid-d20d094da95456e7939f9e77cc51f71d2d4561db.tar.bz2 pyramid-d20d094da95456e7939f9e77cc51f71d2d4561db.zip | |
- A ``static`` helper class was added to the ``repoze.bfg.views``
module. Instances of this class are willing to act as BFG views
which return static resources using files on disk. See the
:mod:`repoze.bfg.view` docs for more info.
| -rw-r--r-- | CHANGES.txt | 5 | ||||
| -rw-r--r-- | docs/api/view.rst | 3 | ||||
| -rw-r--r-- | docs/narr/views.rst | 80 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_view.py | 49 | ||||
| -rw-r--r-- | repoze/bfg/view.py | 41 | ||||
| -rw-r--r-- | repoze/bfg/wsgi.py | 10 |
6 files changed, 135 insertions, 53 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 12fe3d911..205a8bd5a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,6 +17,11 @@ Next Release Features + - A ``static`` helper class was added to the ``repoze.bfg.views`` + module. Instances of this class are willing to act as BFG views + which return static resources using files on disk. See the + :mod:`repoze.bfg.view` docs for more info. + - The ``repoze.bfg.url.model_url`` API (nee' ``repoze.bfg.traversal.model_url``) now accepts and honors a keyword argument named ``query``. The value of this argument diff --git a/docs/api/view.rst b/docs/api/view.rst index b57ea97c4..ac705c4c3 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -15,3 +15,6 @@ .. autofunction:: view_execution_permitted + .. autoclass:: static + :members: + diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 80f08b175..8cb00767a 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -374,40 +374,25 @@ includes other response types for Unauthorized, etc. Serving Static Resources Using a View ------------------------------------- -Using a view is the preferred way to serve static resources (like -JavaScript and CSS files) within :mod:`repoze.bfg`. To create a view -that is capable of serving static resources from a directory that is -mounted at the URL path ``/static``, first create a ``static_view`` -view function in a file in your application named ``static.py`` (this -name is arbitrary, it just needs to match the ZCML registration for -the view): +Using the :mod:repoze.bfg.view ``static`` helper class is the +preferred way to serve static resources (like JavaScript and CSS +files) within :mod:`repoze.bfg`. This class creates a callable that +is capable acting as a :mod:`repoze.bfg` view which serves static +resources from a directory. For instance, to serve files within a +directory located on your filesystem at ``/path/to/static/dir`` +mounted at the URL path ``/static`` in your application, create an +instance of :mod:`repoze.bfg.view` 's ``static`` class inside a +``static.py`` file in your application root as below. .. code-block:: python :linenos: - from paste import urlparser - from repoze.bfg.wsgi import wsgiapp + from repoze.bfg.view import static + static_view = static('/path/to/static/dir') - static_dir = '/path/to/static/dir' - static = urlparser.StaticURLParser(static_dir, cache_max_age=3600) - - @wsgiapp - def static_view(environ, start_response): - return static(environ, start_response) - -This view uses the Paste class ``paste.urlparser.StaticURLParser`` to -do the actual serving of content. This class is a WSGI application; -we wrap it into a BFG view by using the ``@wsgiapp`` decorator (see -:ref:`wsgi_module` for the documentation for ``@wsgiapp``). See `the -Paste documentation for urlparser -<http://pythonpaste.org/modules/urlparser.html>`_ for more information -about ``urlparser.StaticURLParser``. - -Put your static files (JS, etc) on your filesystem in the directory -represented as ``/path/to/static/dir``, then wire it up to be -accessible as ``/static`` using ZCML in your application's -``configure.zcml`` against either the class or interface that -represents your root object. +Subsequently, wire this view up to be accessible as ``/static`` using +ZCML in your application's ``configure.zcml`` against either the class +or interface that represents your root object. .. code-block:: xml :linenos: @@ -425,18 +410,31 @@ application's root object is an instance. ``static`` to be accessible as the static view against any model. This will also allow ``/static/foo.js`` to work, but it will allow for ``/anything/static/foo.js`` too, as long as ``anything`` itself - is resolved. - -After this is done, you should be able to view your static files via -URLs prefixed with ``/static/``, for instance ``/static/foo.js``. - -To ensure that model objects contained in the root don't "shadow" your -static view (model objects take precedence during traversal), or to -ensure that your root object's ``__getitem__`` is never called when a -static resource is requested, you can refer to your static resources -as registered above in URLs as, e.g. ``/@@static/foo.js``. This is -completely equivalent to ``/static/foo.js``. See -:ref:`traversal_chapter` for information about "goggles" (``@@``). + is resolveable. + +Now put your static files (JS, etc) on your filesystem in the +directory represented as ``/path/to/static/dir``. After this is done, +you should be able to view the static files in this directory via a +browser at URLs prefixed with ``/static/``, for instance +``/static/foo.js`` will return the file +``/path/to/static/dir/foo.js``. The static directory may contain +subdirectories recursively, and any subdirectories may hold files; +these will be resolved by the static view as you would expect. + +.. note:: To ensure that model objects contained in the root don't + "shadow" your static view (model objects take precedence during + traversal), or to ensure that your root object's ``__getitem__`` is + never called when a static resource is requested, you can refer to + your static resources as registered above in URLs as, + e.g. ``/@@static/foo.js``. This is completely equivalent to + ``/static/foo.js``. See :ref:`traversal_chapter` for information + about "goggles" (``@@``). + +.. note:: Under the hood, the ``repoze.bfg.view.static`` class employs + the ``urlparser.StaticURLParser`` WSGI application to serve static + files. See `the Paste documentation for urlparser + <http://pythonpaste.org/modules/urlparser.html>`_ for more + information about ``urlparser.StaticURLParser``. diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py index 46687e904..389c00cc5 100644 --- a/repoze/bfg/tests/test_view.py +++ b/repoze/bfg/tests/test_view.py @@ -1,13 +1,13 @@ import unittest -from zope.component.testing import PlacelessSetup +from zope.testing.cleanup import cleanUp -class BaseTest(PlacelessSetup): +class BaseTest(object): def setUp(self): - PlacelessSetup.setUp(self) + cleanUp() def tearDown(self): - PlacelessSetup.tearDown(self) + cleanUp() def _registerView(self, app, name, *for_): import zope.component @@ -30,6 +30,7 @@ class BaseTest(PlacelessSetup): def _makeEnviron(self, **extras): environ = { 'wsgi.url_scheme':'http', + 'wsgi.version':(1,0), 'SERVER_NAME':'localhost', 'SERVER_PORT':'8080', 'REQUEST_METHOD':'GET', @@ -372,12 +373,12 @@ class TestIsResponse(unittest.TestCase): f = self._getFUT() self.assertEqual(f(response), False) -class TestViewExecutionPermitted(unittest.TestCase, PlacelessSetup): +class TestViewExecutionPermitted(unittest.TestCase): def setUp(self): - PlacelessSetup.setUp(self) + cleanUp() def tearDown(self): - PlacelessSetup.tearDown(self) + cleanUp() def _callFUT(self, *arg, **kw): from repoze.bfg.view import view_execution_permitted @@ -433,6 +434,40 @@ class TestViewExecutionPermitted(unittest.TestCase, PlacelessSetup): result = self._callFUT(context, request, '') self.failUnless(result is True) +class TestStaticView(unittest.TestCase, BaseTest): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _getTargetClass(self): + from repoze.bfg.view import static + return static + + def _getStaticDir(self): + import os + here = os.path.abspath(os.path.normpath(os.path.dirname(__file__))) + fixtureapp = os.path.join(here, 'fixtureapp') + return fixtureapp + + def _makeOne(self): + static_dir = self._getStaticDir() + return self._getTargetClass()(static_dir) + + def test_it(self): + view = self._makeOne() + context = DummyContext() + request = DummyRequest() + request.subpath = ['__init__.py'] + request.environ = self._makeEnviron() + response = view(context, request) + result = ''.join(list(response.app_iter)) + static_dir = self._getStaticDir() + import os + filedata = open(os.path.join(static_dir, '__init__.py')).read() + self.assertEqual(result, filedata) + class DummyContext: pass diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py index ae4f304f0..3729fe4e5 100644 --- a/repoze/bfg/view.py +++ b/repoze/bfg/view.py @@ -1,3 +1,6 @@ +from paste.urlparser import StaticURLParser +from webob import Response + from zope.component import queryMultiAdapter from zope.component import queryUtility @@ -120,3 +123,41 @@ def is_response(ob): return True return False +class static(object): + """ An instance of this class is a callable which can act as a BFG + view; this view will serve static files from a directory on disk + based on the ``root_dir`` you provide to its constructor. The + directory may contain subdirectories (recursively); the static + view implementation will descend into these directories as + necessary based on the components of the URL in order to resolve a + path into a response.""" + + def __init__(self, root_dir, cache_max_age=3600): + """ Pass the absolute filesystem path to the directory + containing static files directory as ``root_dir``, + ``cache_max_age`` influences the Expires and Max-Age caching + headers (default is 3600 seconds or five minutes).""" + self.app = StaticURLParser(root_dir, cache_max_age=cache_max_age) + + def __call__(self, context, request): + subpath = '/'.join(request.subpath) + caught = [] + def catch_start_response(status, headers, exc_info=None): + caught[:] = (status, headers, exc_info) + ecopy = request.environ.copy() + # Fix up PATH_INFO to get rid of everything but the "subpath" + # (the actual path to the file relative to the root dir). + # Zero out SCRIPT_NAME for good measure. + ecopy['PATH_INFO'] = '/' + subpath + ecopy['SCRIPT_NAME'] = '' + body = self.app(ecopy, catch_start_response) + if caught: + status, headers, exc_info = caught + response = Response() + response.app_iter = body + response.status = status + response.headerlist = headers + return response + else: + raise RuntimeError('WSGI start_response not called') + diff --git a/repoze/bfg/wsgi.py b/repoze/bfg/wsgi.py index 2a23fe7aa..149273c27 100644 --- a/repoze/bfg/wsgi.py +++ b/repoze/bfg/wsgi.py @@ -19,11 +19,11 @@ def wsgiapp(wrapped): Allows the following view declaration to be made:: - <bfg:view - view=".views.hello_world" - name="hello_world.txt" - context="*" - /> + <view + view=".views.hello_world" + name="hello_world.txt" + context="*" + /> The wsgiapp decorator will convert the result of the WSGI application to a Response and return it to repoze.bfg as if the |
