summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt11
-rw-r--r--docs/api/view.rst3
-rw-r--r--repoze/bfg/tests/test_view.py55
-rw-r--r--repoze/bfg/view.py35
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&amp;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')