Repository: Pylons/pyramid

Issue:
Allow specifying the type of redirect for append_slash=True
It would be great to allow specifying the type of redirect to be used when using the `append_slash` parameter to `notfound_view_config`. Maybe the API could be `append_slash=True` gives you the current behavior, and `append_slash=HTTPMovedPermanently` for instance could be used to use `HTTPMovedPermanently` to do the redirect?



Discussion:
I'd be fine with that personally.  Do you think you could provide a PR with the impl and tests Donald?

Yup, just wanted to see if there was any interest before I did :)


Resolving patch:
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -1703,7 +1703,11 @@ def notfound(request):
         Pyramid will return the result of the view callable provided as
         ``view``, as normal.
 
+        If ``append_slash`` implements IResponse then that will be used as the
+        response class instead of the default of ``HTTPFound``.
+
         .. versionadded:: 1.3
+        .. versionchanged:: 1.6
         """
         for arg in ('name', 'permission', 'context', 'for_', 'http_cache'):
             if arg in predicates:
@@ -1737,7 +1741,12 @@ def notfound(request):
         settings.update(predicates)
         if append_slash:
             view = self._derive_view(view, attr=attr, renderer=renderer)
-            view = AppendSlashNotFoundViewFactory(view)
+            if IResponse.implementedBy(append_slash):
+                view = AppendSlashNotFoundViewFactory(
+                    view, redirect_class=append_slash,
+                )
+            else:
+                view = AppendSlashNotFoundViewFactory(view)
             settings['view'] = view
         else:
             settings['attr'] = attr
diff --git a/pyramid/view.py b/pyramid/view.py
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -252,10 +252,11 @@ def notfound_view(context, request): return HTTPNotFound('nope')
     .. deprecated:: 1.3
 
     """
-    def __init__(self, notfound_view=None):
+    def __init__(self, notfound_view=None, redirect_class=HTTPFound):
         if notfound_view is None:
             notfound_view = default_exceptionresponse_view
         self.notfound_view = notfound_view
+        self.redirect_class = redirect_class
 
     def __call__(self, context, request):
         path = decode_path_info(request.environ['PATH_INFO'] or '/')
@@ -268,7 +269,7 @@ def __call__(self, context, request):
                     qs = request.query_string
                     if qs:
                         qs = '?' + qs
-                    return HTTPFound(location=request.path+'/'+qs)
+                    return self.redirect_class(location=request.path+'/'+qs)
         return self.notfound_view(context, request)
 
 append_slash_notfound_view = AppendSlashNotFoundViewFactory()


Test patch:
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1941,7 +1941,7 @@ def test_add_notfound_view_append_slash(self):
         from pyramid.renderers import null_renderer
         from zope.interface import implementedBy
         from pyramid.interfaces import IRequest
-        from pyramid.httpexceptions import HTTPNotFound
+        from pyramid.httpexceptions import HTTPFound, HTTPNotFound
         config = self._makeOne(autocommit=True)
         config.add_route('foo', '/foo/')
         def view(request): return Response('OK')
@@ -1954,6 +1954,30 @@ def view(request): return Response('OK')
                                      ctx_iface=implementedBy(HTTPNotFound),
                                      request_iface=IRequest)
         result = view(None, request)
+        self.assertTrue(isinstance(result, HTTPFound))
+        self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2')
+
+    def test_add_notfound_view_append_slash_custom_response(self):
+        from pyramid.response import Response
+        from pyramid.renderers import null_renderer
+        from zope.interface import implementedBy
+        from pyramid.interfaces import IRequest
+        from pyramid.httpexceptions import HTTPMovedPermanently, HTTPNotFound
+        config = self._makeOne(autocommit=True)
+        config.add_route('foo', '/foo/')
+        def view(request): return Response('OK')
+        config.add_notfound_view(
+            view, renderer=null_renderer,append_slash=HTTPMovedPermanently
+        )
+        request = self._makeRequest(config)
+        request.environ['PATH_INFO'] = '/foo'
+        request.query_string = 'a=1&b=2'
+        request.path = '/scriptname/foo'
+        view = self._getViewCallable(config,
+                                     ctx_iface=implementedBy(HTTPNotFound),
+                                     request_iface=IRequest)
+        result = view(None, request)
+        self.assertTrue(isinstance(result, HTTPMovedPermanently))
         self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2')
 
     def test_add_notfound_view_with_view_defaults(self):