diff options
| -rw-r--r-- | CHANGES.txt | 11 | ||||
| -rw-r--r-- | docs/api/view.rst | 3 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_view.py | 55 | ||||
| -rw-r--r-- | repoze/bfg/view.py | 35 |
4 files changed, 104 insertions, 0 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 981483e25..8a82bc086 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -13,6 +13,17 @@ Documentation Features -------- +- For behavior like Django's ``APPEND_SLASH=True``, use the + ``repoze.bfg.view.append_slash_notfound_view`` view as the Not Found + view in your application. When this view is the Not Found view + (indicating that no view was found), and any routes have been + defined in the configuration of your application, if the value of + ``PATH_INFO`` does not already end in a slash, and if the value of + ``PATH_INFO`` *plus* a slash matches any route's path, do an HTTP + redirect to the slash-appended PATH_INFO. Note that this will + *lose* ``POST`` data information (turning it into a GET), so you + shouldn't rely on this to redirect POST requests. + - Speed up ``repoze.bfg.location.lineage`` slightly. - Speed up ``repoze.bfg.encode.urlencode`` (nee' diff --git a/docs/api/view.rst b/docs/api/view.rst index 40c69d24b..e345a0015 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -19,3 +19,6 @@ .. autoclass:: static :members: + .. autofunction:: append_slash_notfound_view + + diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py index 32c9d391b..4705a16c4 100644 --- a/repoze/bfg/tests/test_view.py +++ b/repoze/bfg/tests/test_view.py @@ -441,6 +441,61 @@ class TestDefaultNotFoundView(unittest.TestCase): self.assertEqual(response.status, '404 Not Found') self.failUnless('<code>abc&123</code>' in response.body) +class AppendSlashNotFoundView(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, context, request): + from repoze.bfg.view import append_slash_notfound_view + return append_slash_notfound_view(context, request) + + def _registerMapper(self, match=True): + from repoze.bfg.interfaces import IRoutesMapper + class DummyRoute(object): + def __init__(self, val): + self.val = val + def match(self, path): + return self.val + class DummyMapper(object): + def __init__(self): + self.routelist = [ DummyRoute(match) ] + mapper = DummyMapper() + import zope.component + gsm = zope.component.getGlobalSiteManager() + gsm.registerUtility(mapper, IRoutesMapper) + return mapper + + def test_no_mapper(self): + request = DummyRequest({'PATH_INFO':'/abc'}) + context = DummyContext() + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + + def test_no_path(self): + self._registerMapper(True) + request = DummyRequest({}) + context = DummyContext() + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + + def test_mapper_path_already_slash_ending(self): + self._registerMapper(True) + request = DummyRequest({'PATH_INFO':'/abc/'}) + context = DummyContext() + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + + def test_matches(self): + self._registerMapper(True) + request = DummyRequest({'PATH_INFO':'/abc'}) + context = DummyContext() + response = self._callFUT(context, request) + self.assertEqual(response.status, '302 Found') + self.assertEqual(response.location, '/abc/') + class TestMultiView(unittest.TestCase): def _getTargetClass(self): from repoze.bfg.view import MultiView diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py index 91ccad57d..a4abf4947 100644 --- a/repoze/bfg/view.py +++ b/repoze/bfg/view.py @@ -14,6 +14,7 @@ if hasattr(mimetypes, 'init'): mimetypes.init() from webob import Response +from webob.exc import HTTPFound from paste.urlparser import StaticURLParser @@ -29,6 +30,7 @@ from repoze.bfg.interfaces import ILogger from repoze.bfg.interfaces import IMultiView from repoze.bfg.interfaces import IRendererFactory from repoze.bfg.interfaces import IResponseFactory +from repoze.bfg.interfaces import IRoutesMapper from repoze.bfg.interfaces import IView from repoze.bfg.exceptions import NotFound @@ -718,3 +720,36 @@ def authdebug_view(view, permission): decorate_view(wrapped_view, view) return wrapped_view + +def append_slash_notfound_view(context, request): + """For behavior like Django's ``APPEND_SLASH=True``, use this view + as the Not Found view in your application. + + When this view is the Not Found view (indicating that no view was + found), and any routes have been defined in the configuration of + your application, if the value of ``PATH_INFO`` does not already + end in a slash, and if the value of ``PATH_INFO`` *plus* a slash + matches any route's path, do an HTTP redirect to the + slash-appended PATH_INFO. Note that this will *lose* ``POST`` + data information (turning it into a GET), so you shouldn't rely on + this to redirect POST requests. + + Add the following to your application's ``configure.zcml`` to use + this view as the Not Found view:: + + <notfound + view="repoze.bfg.view.append_slash_notfound_view"/> + + See also :ref:`changing_the_notfound_view`. + + .. note:: This function is new as of :mod:`repoze.bfg` version 1.1. + + """ + path = request.environ.get('PATH_INFO', '/') + mapper = queryUtility(IRoutesMapper) + if mapper is not None and not path.endswith('/'): + slashpath = path + '/' + for route in mapper.routelist: + if route.match(slashpath) is not None: + return HTTPFound(location=slashpath) + return default_view(context, request, '404 Not Found') |
