[4.0.x] Fixed #33350 -- Reallowed using cache decorators with duck-typed HttpRequest.

Regression in 3fd82a6241.

Thanks Terence Honles for the report.
Backport of 40165eecc4 from main
This commit is contained in:
Mariusz Felisiak 2021-12-16 20:13:17 +01:00
parent 267a743bf2
commit c1d2e8b9b8
3 changed files with 47 additions and 5 deletions

View File

@ -1,6 +1,5 @@
from functools import wraps from functools import wraps
from django.http import HttpRequest
from django.middleware.cache import CacheMiddleware from django.middleware.cache import CacheMiddleware
from django.utils.cache import add_never_cache_headers, patch_cache_control from django.utils.cache import add_never_cache_headers, patch_cache_control
from django.utils.decorators import decorator_from_middleware_with_args from django.utils.decorators import decorator_from_middleware_with_args
@ -29,7 +28,8 @@ def cache_control(**kwargs):
def _cache_controller(viewfunc): def _cache_controller(viewfunc):
@wraps(viewfunc) @wraps(viewfunc)
def _cache_controlled(request, *args, **kw): def _cache_controlled(request, *args, **kw):
if not isinstance(request, HttpRequest): # Ensure argument looks like a request.
if not hasattr(request, 'META'):
raise TypeError( raise TypeError(
"cache_control didn't receive an HttpRequest. If you are " "cache_control didn't receive an HttpRequest. If you are "
"decorating a classmethod, be sure to use " "decorating a classmethod, be sure to use "
@ -48,7 +48,8 @@ def never_cache(view_func):
""" """
@wraps(view_func) @wraps(view_func)
def _wrapped_view_func(request, *args, **kwargs): def _wrapped_view_func(request, *args, **kwargs):
if not isinstance(request, HttpRequest): # Ensure argument looks like a request.
if not hasattr(request, 'META'):
raise TypeError( raise TypeError(
"never_cache didn't receive an HttpRequest. If you are " "never_cache didn't receive an HttpRequest. If you are "
"decorating a classmethod, be sure to use @method_decorator." "decorating a classmethod, be sure to use @method_decorator."

View File

@ -15,3 +15,7 @@ Bugfixes
* Fixed a bug in Django 4.0 that caused a crash on booleans with the * Fixed a bug in Django 4.0 that caused a crash on booleans with the
``RedisCache`` backend (:ticket:`33361`). ``RedisCache`` backend (:ticket:`33361`).
* Relaxed the check added in Django 4.0 to reallow use of a duck-typed
``HttpRequest`` in ``django.views.decorators.cache.cache_control()`` and
``never_cache()`` decorators (:ticket:`33350`).

View File

@ -493,6 +493,15 @@ class XFrameOptionsDecoratorsTests(TestCase):
self.assertIsNone(r.get('X-Frame-Options', None)) self.assertIsNone(r.get('X-Frame-Options', None))
class HttpRequestProxy:
def __init__(self, request):
self._request = request
def __getattr__(self, attr):
"""Proxy to the underlying HttpRequest object."""
return getattr(self._request, attr)
class NeverCacheDecoratorTest(SimpleTestCase): class NeverCacheDecoratorTest(SimpleTestCase):
def test_never_cache_decorator(self): def test_never_cache_decorator(self):
@never_cache @never_cache
@ -509,12 +518,27 @@ class NeverCacheDecoratorTest(SimpleTestCase):
@never_cache @never_cache
def a_view(self, request): def a_view(self, request):
return HttpResponse() return HttpResponse()
request = HttpRequest()
msg = ( msg = (
"never_cache didn't receive an HttpRequest. If you are decorating " "never_cache didn't receive an HttpRequest. If you are decorating "
"a classmethod, be sure to use @method_decorator." "a classmethod, be sure to use @method_decorator."
) )
with self.assertRaisesMessage(TypeError, msg): with self.assertRaisesMessage(TypeError, msg):
MyClass().a_view(HttpRequest()) MyClass().a_view(request)
with self.assertRaisesMessage(TypeError, msg):
MyClass().a_view(HttpRequestProxy(request))
def test_never_cache_decorator_http_request_proxy(self):
class MyClass:
@method_decorator(never_cache)
def a_view(self, request):
return HttpResponse()
request = HttpRequest()
response = MyClass().a_view(HttpRequestProxy(request))
self.assertIn('Cache-Control', response.headers)
self.assertIn('Expires', response.headers)
class CacheControlDecoratorTest(SimpleTestCase): class CacheControlDecoratorTest(SimpleTestCase):
@ -528,5 +552,18 @@ class CacheControlDecoratorTest(SimpleTestCase):
"cache_control didn't receive an HttpRequest. If you are " "cache_control didn't receive an HttpRequest. If you are "
"decorating a classmethod, be sure to use @method_decorator." "decorating a classmethod, be sure to use @method_decorator."
) )
request = HttpRequest()
with self.assertRaisesMessage(TypeError, msg): with self.assertRaisesMessage(TypeError, msg):
MyClass().a_view(HttpRequest()) MyClass().a_view(request)
with self.assertRaisesMessage(TypeError, msg):
MyClass().a_view(HttpRequestProxy(request))
def test_cache_control_decorator_http_request_proxy(self):
class MyClass:
@method_decorator(cache_control(a='b'))
def a_view(self, request):
return HttpResponse()
request = HttpRequest()
response = MyClass().a_view(HttpRequestProxy(request))
self.assertEqual(response.headers['Cache-Control'], 'a=b')