From 40165eecc40f9e223702a41a0cb0958515bb1f82 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 16 Dec 2021 20:13:17 +0100 Subject: [PATCH] Fixed #33350 -- Reallowed using cache decorators with duck-typed HttpRequest. Regression in 3fd82a62415e748002435e7bad06b5017507777c. Thanks Terence Honles for the report. --- django/views/decorators/cache.py | 7 +++--- docs/releases/4.0.1.txt | 4 ++++ tests/decorators/tests.py | 41 ++++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index fdc5917738..417f3614f8 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -1,6 +1,5 @@ from functools import wraps -from django.http import HttpRequest from django.middleware.cache import CacheMiddleware from django.utils.cache import add_never_cache_headers, patch_cache_control from django.utils.decorators import decorator_from_middleware_with_args @@ -29,7 +28,8 @@ def cache_control(**kwargs): def _cache_controller(viewfunc): @wraps(viewfunc) def _cache_controlled(request, *args, **kw): - if not isinstance(request, HttpRequest): + # Ensure argument looks like a request. + if not hasattr(request, 'META'): raise TypeError( "cache_control didn't receive an HttpRequest. If you are " "decorating a classmethod, be sure to use " @@ -48,7 +48,8 @@ def never_cache(view_func): """ @wraps(view_func) 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( "never_cache didn't receive an HttpRequest. If you are " "decorating a classmethod, be sure to use @method_decorator." diff --git a/docs/releases/4.0.1.txt b/docs/releases/4.0.1.txt index 08e5b206bb..50c84bfe17 100644 --- a/docs/releases/4.0.1.txt +++ b/docs/releases/4.0.1.txt @@ -15,3 +15,7 @@ Bugfixes * Fixed a bug in Django 4.0 that caused a crash on booleans with the ``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`). diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index de0edaa787..b9d16f9a1b 100644 --- a/tests/decorators/tests.py +++ b/tests/decorators/tests.py @@ -493,6 +493,15 @@ class XFrameOptionsDecoratorsTests(TestCase): 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): @mock.patch('time.time') @@ -525,12 +534,27 @@ class NeverCacheDecoratorTest(SimpleTestCase): @never_cache def a_view(self, request): return HttpResponse() + + request = HttpRequest() msg = ( "never_cache didn't receive an HttpRequest. If you are decorating " "a classmethod, be sure to use @method_decorator." ) 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): @@ -544,5 +568,18 @@ class CacheControlDecoratorTest(SimpleTestCase): "cache_control didn't receive an HttpRequest. If you are " "decorating a classmethod, be sure to use @method_decorator." ) + request = HttpRequest() 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')