summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-01-06 18:53:17 +0000
committerChris McDonough <chrism@agendaless.com>2009-01-06 18:53:17 +0000
commitd20d094da95456e7939f9e77cc51f71d2d4561db (patch)
tree5b1b8fb4aa5549599b39249f8ebafbc390e3a7d4
parentd9423b6f9adbce7f8fdc9153644bd88be9a5b6c8 (diff)
downloadpyramid-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.txt5
-rw-r--r--docs/api/view.rst3
-rw-r--r--docs/narr/views.rst80
-rw-r--r--repoze/bfg/tests/test_view.py49
-rw-r--r--repoze/bfg/view.py41
-rw-r--r--repoze/bfg/wsgi.py10
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