Fixed #32124 -- Added per-view opt-out for APPEND_SLASH behavior.
This commit is contained in:
parent
3418092238
commit
ad11f5b8c9
|
@ -67,10 +67,11 @@ class CommonMiddleware(MiddlewareMixin):
|
||||||
"""
|
"""
|
||||||
if settings.APPEND_SLASH and not request.path_info.endswith('/'):
|
if settings.APPEND_SLASH and not request.path_info.endswith('/'):
|
||||||
urlconf = getattr(request, 'urlconf', None)
|
urlconf = getattr(request, 'urlconf', None)
|
||||||
return (
|
if not is_valid_path(request.path_info, urlconf):
|
||||||
not is_valid_path(request.path_info, urlconf) and
|
match = is_valid_path('%s/' % request.path_info, urlconf)
|
||||||
is_valid_path('%s/' % request.path_info, urlconf)
|
if match:
|
||||||
)
|
view = match.func
|
||||||
|
return getattr(view, 'should_append_slash', True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_full_path_with_slash(self, request):
|
def get_full_path_with_slash(self, request):
|
||||||
|
|
|
@ -145,13 +145,12 @@ def get_urlconf(default=None):
|
||||||
|
|
||||||
def is_valid_path(path, urlconf=None):
|
def is_valid_path(path, urlconf=None):
|
||||||
"""
|
"""
|
||||||
Return True if the given path resolves against the default URL resolver,
|
Return the ResolverMatch if the given path resolves against the default URL
|
||||||
False otherwise. This is a convenience method to make working with "is
|
resolver, False otherwise. This is a convenience method to make working
|
||||||
this a match?" cases easier, avoiding try...except blocks.
|
with "is this a match?" cases easier, avoiding try...except blocks.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
resolve(path, urlconf)
|
return resolve(path, urlconf)
|
||||||
return True
|
|
||||||
except Resolver404:
|
except Resolver404:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def no_append_slash(view_func):
|
||||||
|
"""
|
||||||
|
Mark a view function as excluded from CommonMiddleware's APPEND_SLASH
|
||||||
|
redirection.
|
||||||
|
"""
|
||||||
|
# view_func.should_append_slash = False would also work, but decorators are
|
||||||
|
# nicer if they don't have side effects, so return a new function.
|
||||||
|
def wrapped_view(*args, **kwargs):
|
||||||
|
return view_func(*args, **kwargs)
|
||||||
|
wrapped_view.should_append_slash = False
|
||||||
|
return wraps(view_func)(wrapped_view)
|
|
@ -61,6 +61,22 @@ Adds a few conveniences for perfectionists:
|
||||||
indexer would treat them as separate URLs -- so it's best practice to
|
indexer would treat them as separate URLs -- so it's best practice to
|
||||||
normalize URLs.
|
normalize URLs.
|
||||||
|
|
||||||
|
If necessary, individual views may be excluded from the ``APPEND_SLASH``
|
||||||
|
behavior using the :func:`~django.views.decorators.common.no_append_slash`
|
||||||
|
decorator::
|
||||||
|
|
||||||
|
from django.views.decorators.common import no_append_slash
|
||||||
|
|
||||||
|
@no_append_slash
|
||||||
|
def sensitive_fbv(request, *args, **kwargs):
|
||||||
|
"""View to be excluded from APPEND_SLASH."""
|
||||||
|
return HttpResponse()
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
Support for the :func:`~django.views.decorators.common.no_append_slash`
|
||||||
|
decorator was added.
|
||||||
|
|
||||||
* Sets the ``Content-Length`` header for non-streaming responses.
|
* Sets the ``Content-Length`` header for non-streaming responses.
|
||||||
|
|
||||||
.. attribute:: CommonMiddleware.response_redirect_class
|
.. attribute:: CommonMiddleware.response_redirect_class
|
||||||
|
|
|
@ -185,6 +185,13 @@ CSRF
|
||||||
|
|
||||||
* ...
|
* ...
|
||||||
|
|
||||||
|
Decorators
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
* The new :func:`~django.views.decorators.common.no_append_slash` decorator
|
||||||
|
allows individual views to be excluded from :setting:`APPEND_SLASH` URL
|
||||||
|
normalization.
|
||||||
|
|
||||||
Email
|
Email
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
|
|
|
@ -121,3 +121,18 @@ client-side caching.
|
||||||
This decorator adds a ``Cache-Control: max-age=0, no-cache, no-store,
|
This decorator adds a ``Cache-Control: max-age=0, no-cache, no-store,
|
||||||
must-revalidate, private`` header to a response to indicate that a page
|
must-revalidate, private`` header to a response to indicate that a page
|
||||||
should never be cached.
|
should never be cached.
|
||||||
|
|
||||||
|
.. module:: django.views.decorators.common
|
||||||
|
|
||||||
|
Common
|
||||||
|
======
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
The decorators in :mod:`django.views.decorators.common` allow per-view
|
||||||
|
customization of :class:`~django.middleware.common.CommonMiddleware` behavior.
|
||||||
|
|
||||||
|
.. function:: no_append_slash()
|
||||||
|
|
||||||
|
This decorator allows individual views to be excluded from
|
||||||
|
:setting:`APPEND_SLASH` URL normalization.
|
||||||
|
|
|
@ -127,6 +127,17 @@ class CommonMiddlewareTest(SimpleTestCase):
|
||||||
request = self.rf.get('/slash')
|
request = self.rf.get('/slash')
|
||||||
self.assertEqual(CommonMiddleware(get_response_404)(request).status_code, 404)
|
self.assertEqual(CommonMiddleware(get_response_404)(request).status_code, 404)
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=True)
|
||||||
|
def test_append_slash_opt_out(self):
|
||||||
|
"""
|
||||||
|
Views marked with @no_append_slash should be left alone.
|
||||||
|
"""
|
||||||
|
request = self.rf.get('/sensitive_fbv')
|
||||||
|
self.assertEqual(CommonMiddleware(get_response_404)(request).status_code, 404)
|
||||||
|
|
||||||
|
request = self.rf.get('/sensitive_cbv')
|
||||||
|
self.assertEqual(CommonMiddleware(get_response_404)(request).status_code, 404)
|
||||||
|
|
||||||
@override_settings(APPEND_SLASH=True)
|
@override_settings(APPEND_SLASH=True)
|
||||||
def test_append_slash_quoted(self):
|
def test_append_slash_quoted(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -8,4 +8,7 @@ urlpatterns = [
|
||||||
path('needsquoting#/', views.empty_view),
|
path('needsquoting#/', views.empty_view),
|
||||||
# Accepts paths with two leading slashes.
|
# Accepts paths with two leading slashes.
|
||||||
re_path(r'^(.+)/security/$', views.empty_view),
|
re_path(r'^(.+)/security/$', views.empty_view),
|
||||||
|
# Should not append slash.
|
||||||
|
path('sensitive_fbv/', views.sensitive_fbv),
|
||||||
|
path('sensitive_cbv/', views.SensitiveCBV.as_view()),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.common import no_append_slash
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
|
|
||||||
def empty_view(request, *args, **kwargs):
|
def empty_view(request, *args, **kwargs):
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
|
@no_append_slash
|
||||||
|
def sensitive_fbv(request, *args, **kwargs):
|
||||||
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(no_append_slash, name='dispatch')
|
||||||
|
class SensitiveCBV(View):
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
return HttpResponse()
|
||||||
|
|
Loading…
Reference in New Issue