Ticket #17305: diff (2)

File diff (2), 23.1 KB (added by subsume, 4 years ago)
Line 
1diff --git a/django/middleware/cache.py b/django/middleware/cache.py
2index 34bf0ca..049238e 100644
3--- a/django/middleware/cache.py
4+++ b/django/middleware/cache.py
5@@ -48,12 +48,72 @@ More details about how the caching works:
6 
7 """
8 
9+import hashlib
10+
11+from django.utils.encoding import iri_to_uri
12 from django.conf import settings
13 from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS
14-from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age
15+from django.utils.cache import patch_response_headers, get_max_age, cc_delim_re
16+from django.utils.timezone import get_current_timezone_name
17+from django.utils.translation import get_language
18 
19+class TwoPartCacheMiddlewareBase(object):
20+    @classmethod
21+    def get_cache_key(cls, request, key_prefix=None, method='GET', cache=None):
22+        """
23+        Returns a cache key based on the request path and query. It can be used
24+        in the request phase because it pulls the list of headers to take into
25+        account from the global path registry and uses those to build a cache key
26+        to check against.
27 
28-class UpdateCacheMiddleware(object):
29+        If there is no headerlist stored, the page needs to be rebuilt, so this
30+        function returns None.
31+        """
32+        if key_prefix is None:
33+            key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
34+        cache_key = cls._generate_cache_header_key(key_prefix, request)
35+        if cache is None:
36+            cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS)
37+        headerlist = cache.get(cache_key, None)
38+        if headerlist is not None:
39+            return cls._generate_cache_key(request, method, headerlist, key_prefix)
40+        else:
41+            return None
42+
43+    @classmethod
44+    def _i18n_cache_key_suffix(cls, request, cache_key):
45+        """If necessary, adds the current locale or time zone to the cache key."""
46+        if settings.USE_I18N or settings.USE_L10N:
47+            # first check if LocaleMiddleware or another middleware added
48+            # LANGUAGE_CODE to request, then fall back to the active language
49+            # which in turn can also fall back to settings.LANGUAGE_CODE
50+            cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
51+        if settings.USE_TZ:
52+            cache_key += '.%s' % get_current_timezone_name()
53+        return cache_key
54+
55+    @classmethod
56+    def _generate_cache_key(cls, request, method, headerlist, key_prefix):
57+        """Returns a cache key from the headers given in the header list."""
58+        ctx = hashlib.md5()
59+        for header in headerlist:
60+            value = request.META.get(header, None)
61+            if value is not None:
62+                ctx.update(value)
63+        path = hashlib.md5(iri_to_uri(request.get_full_path()))
64+        cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
65+            key_prefix, method, path.hexdigest(), ctx.hexdigest())
66+        return cls._i18n_cache_key_suffix(request, cache_key)
67+
68+    @classmethod
69+    def _generate_cache_header_key(cls, key_prefix, request):
70+        """Returns a cache key for the header cache."""
71+        path = hashlib.md5(iri_to_uri(request.get_full_path()))
72+        cache_key = 'views.decorators.cache.cache_header.%s.%s' % (
73+            key_prefix, path.hexdigest())
74+        return cls._i18n_cache_key_suffix(request, cache_key)
75+
76+class UpdateCacheMiddleware(TwoPartCacheMiddlewareBase):
77     """
78     Response-phase cache middleware that updates the cache if the response is
79     cacheable.
80@@ -88,6 +148,38 @@ class UpdateCacheMiddleware(object):
81                 return False
82         return True
83 
84+    @classmethod
85+    def learn_cache_key(cls, request, response, cache_timeout=None, key_prefix=None, cache=None):
86+        """
87+        Learns what headers to take into account for some request path from the
88+        response object. It stores those headers in a global path registry so that
89+        later access to that path will know what headers to take into account
90+        without building the response object itself. The headers are named in the
91+        Vary header of the response, but we want to prevent response generation.
92+
93+        The list of headers to use for cache key generation is stored in the same
94+        cache as the pages themselves. If the cache ages some data out of the
95+        cache, this just means that we have to build the response once to get at
96+        the Vary header and so at the list of headers to use for the cache key.
97+        """
98+        if key_prefix is None:
99+            key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
100+        if cache_timeout is None:
101+            cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
102+        cache_key = cls._generate_cache_header_key(key_prefix, request)
103+        if cache is None:
104+            cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS)
105+        if response.has_header('Vary'):
106+            headerlist = ['HTTP_'+header.upper().replace('-', '_')
107+                          for header in cc_delim_re.split(response['Vary'])]
108+            cache.set(cache_key, headerlist, cache_timeout)
109+            return cls._generate_cache_key(request, request.method, headerlist, key_prefix)
110+        else:
111+            # if there is no Vary header, we still need a cache key
112+            # for the request.get_full_path()
113+            cache.set(cache_key, [], cache_timeout)
114+            return cls._generate_cache_key(request, request.method, [], key_prefix)
115+
116     def process_response(self, request, response):
117         """Sets the cache, if needed."""
118         if not self._should_update_cache(request, response):
119@@ -106,7 +198,7 @@ class UpdateCacheMiddleware(object):
120             return response
121         patch_response_headers(response, timeout)
122         if timeout:
123-            cache_key = learn_cache_key(request, response, timeout, self.key_prefix, cache=self.cache)
124+            cache_key = self.learn_cache_key(request, response, timeout, self.key_prefix, cache=self.cache)
125             if hasattr(response, 'render') and callable(response.render):
126                 response.add_post_render_callback(
127                     lambda r: self.cache.set(cache_key, r, timeout)
128@@ -115,7 +207,7 @@ class UpdateCacheMiddleware(object):
129                 self.cache.set(cache_key, response, timeout)
130         return response
131 
132-class FetchFromCacheMiddleware(object):
133+class FetchFromCacheMiddleware(TwoPartCacheMiddlewareBase):
134     """
135     Request-phase cache middleware that fetches a page from the cache.
136 
137@@ -140,14 +232,14 @@ class FetchFromCacheMiddleware(object):
138             return None # Don't bother checking the cache.
139 
140         # try and get the cached GET response
141-        cache_key = get_cache_key(request, self.key_prefix, 'GET', cache=self.cache)
142+        cache_key = self.get_cache_key(request, self.key_prefix, 'GET', cache=self.cache)
143         if cache_key is None:
144             request._cache_update_cache = True
145             return None # No cache information available, need to rebuild.
146         response = self.cache.get(cache_key, None)
147         # if it wasn't found and we are looking for a HEAD, try looking just for that
148         if response is None and request.method == 'HEAD':
149-            cache_key = get_cache_key(request, self.key_prefix, 'HEAD', cache=self.cache)
150+            cache_key = self.get_cache_key(request, self.key_prefix, 'HEAD', cache=self.cache)
151             response = self.cache.get(cache_key, None)
152 
153         if response is None:
154diff --git a/django/utils/cache.py b/django/utils/cache.py
155index 1015c2f..9448aae 100644
156--- a/django/utils/cache.py
157+++ b/django/utils/cache.py
158@@ -23,10 +23,8 @@ import time
159 
160 from django.conf import settings
161 from django.core.cache import get_cache
162-from django.utils.encoding import smart_str, iri_to_uri
163+from django.utils.encoding import smart_str
164 from django.utils.http import http_date
165-from django.utils.timezone import get_current_timezone_name
166-from django.utils.translation import get_language
167 
168 cc_delim_re = re.compile(r'\s*,\s*')
169 
170@@ -122,12 +120,6 @@ def patch_response_headers(response, cache_timeout=None):
171         response['Expires'] = http_date(time.time() + cache_timeout)
172     patch_cache_control(response, max_age=cache_timeout)
173 
174-def add_never_cache_headers(response):
175-    """
176-    Adds headers to a response to indicate that a page should never be cached.
177-    """
178-    patch_response_headers(response, cache_timeout=-1)
179-
180 def patch_vary_headers(response, newheaders):
181     """
182     Adds (or updates) the "Vary" header in the given HttpResponse object.
183@@ -157,88 +149,6 @@ def has_vary_header(response, header_query):
184     existing_headers = set([header.lower() for header in vary_headers])
185     return header_query.lower() in existing_headers
186 
187-def _i18n_cache_key_suffix(request, cache_key):
188-    """If necessary, adds the current locale or time zone to the cache key."""
189-    if settings.USE_I18N or settings.USE_L10N:
190-        # first check if LocaleMiddleware or another middleware added
191-        # LANGUAGE_CODE to request, then fall back to the active language
192-        # which in turn can also fall back to settings.LANGUAGE_CODE
193-        cache_key += '.%s' % getattr(request, 'LANGUAGE_CODE', get_language())
194-    if settings.USE_TZ:
195-        cache_key += '.%s' % get_current_timezone_name()
196-    return cache_key
197-
198-def _generate_cache_key(request, method, headerlist, key_prefix):
199-    """Returns a cache key from the headers given in the header list."""
200-    ctx = hashlib.md5()
201-    for header in headerlist:
202-        value = request.META.get(header, None)
203-        if value is not None:
204-            ctx.update(value)
205-    path = hashlib.md5(iri_to_uri(request.get_full_path()))
206-    cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
207-        key_prefix, method, path.hexdigest(), ctx.hexdigest())
208-    return _i18n_cache_key_suffix(request, cache_key)
209-
210-def _generate_cache_header_key(key_prefix, request):
211-    """Returns a cache key for the header cache."""
212-    path = hashlib.md5(iri_to_uri(request.get_full_path()))
213-    cache_key = 'views.decorators.cache.cache_header.%s.%s' % (
214-        key_prefix, path.hexdigest())
215-    return _i18n_cache_key_suffix(request, cache_key)
216-
217-def get_cache_key(request, key_prefix=None, method='GET', cache=None):
218-    """
219-    Returns a cache key based on the request path and query. It can be used
220-    in the request phase because it pulls the list of headers to take into
221-    account from the global path registry and uses those to build a cache key
222-    to check against.
223-
224-    If there is no headerlist stored, the page needs to be rebuilt, so this
225-    function returns None.
226-    """
227-    if key_prefix is None:
228-        key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
229-    cache_key = _generate_cache_header_key(key_prefix, request)
230-    if cache is None:
231-        cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS)
232-    headerlist = cache.get(cache_key, None)
233-    if headerlist is not None:
234-        return _generate_cache_key(request, method, headerlist, key_prefix)
235-    else:
236-        return None
237-
238-def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cache=None):
239-    """
240-    Learns what headers to take into account for some request path from the
241-    response object. It stores those headers in a global path registry so that
242-    later access to that path will know what headers to take into account
243-    without building the response object itself. The headers are named in the
244-    Vary header of the response, but we want to prevent response generation.
245-
246-    The list of headers to use for cache key generation is stored in the same
247-    cache as the pages themselves. If the cache ages some data out of the
248-    cache, this just means that we have to build the response once to get at
249-    the Vary header and so at the list of headers to use for the cache key.
250-    """
251-    if key_prefix is None:
252-        key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
253-    if cache_timeout is None:
254-        cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
255-    cache_key = _generate_cache_header_key(key_prefix, request)
256-    if cache is None:
257-        cache = get_cache(settings.CACHE_MIDDLEWARE_ALIAS)
258-    if response.has_header('Vary'):
259-        headerlist = ['HTTP_'+header.upper().replace('-', '_')
260-                      for header in cc_delim_re.split(response['Vary'])]
261-        cache.set(cache_key, headerlist, cache_timeout)
262-        return _generate_cache_key(request, request.method, headerlist, key_prefix)
263-    else:
264-        # if there is no Vary header, we still need a cache key
265-        # for the request.get_full_path()
266-        cache.set(cache_key, [], cache_timeout)
267-        return _generate_cache_key(request, request.method, [], key_prefix)
268-
269 
270 def _to_tuple(s):
271     t = s.split('=',1)
272diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py
273index a39cc54..2847e05 100644
274--- a/django/views/decorators/cache.py
275+++ b/django/views/decorators/cache.py
276@@ -1,6 +1,6 @@
277 from functools import wraps
278 from django.utils.decorators import decorator_from_middleware_with_args, available_attrs
279-from django.utils.cache import patch_cache_control, add_never_cache_headers
280+from django.utils.cache import patch_cache_control, patch_response_headers
281 from django.middleware.cache import CacheMiddleware
282 
283 
284@@ -86,6 +86,6 @@ def never_cache(view_func):
285     @wraps(view_func, assigned=available_attrs(view_func))
286     def _wrapped_view_func(request, *args, **kwargs):
287         response = view_func(request, *args, **kwargs)
288-        add_never_cache_headers(response)
289+        patch_response_headers(response, cache_timeout=0)
290         return response
291     return _wrapped_view_func
292diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
293index 307588c..ea13421 100644
294--- a/tests/regressiontests/cache/tests.py
295+++ b/tests/regressiontests/cache/tests.py
296@@ -19,15 +19,15 @@ from django.core.cache.backends.base import (CacheKeyWarning,
297 from django.db import router
298 from django.http import HttpResponse, HttpRequest, QueryDict
299 from django.middleware.cache import (FetchFromCacheMiddleware,
300-    UpdateCacheMiddleware, CacheMiddleware)
301+    UpdateCacheMiddleware, CacheMiddleware, TwoPartCacheMiddlewareBase)
302 from django.template import Template
303 from django.template.response import TemplateResponse
304 from django.test import TestCase, TransactionTestCase, RequestFactory
305 from django.test.utils import (get_warnings_state, restore_warnings_state,
306     override_settings)
307 from django.utils import timezone, translation, unittest
308-from django.utils.cache import (patch_vary_headers, get_cache_key,
309-    learn_cache_key, patch_cache_control, patch_response_headers)
310+from django.utils.cache import (patch_vary_headers,
311+    patch_cache_control, patch_response_headers)
312 from django.views.decorators.cache import cache_page
313 
314 from .models import Poll, expensive_calculation
315@@ -998,22 +998,11 @@ class CacheUtils(TestCase):
316     """TestCase for django.utils.cache functions."""
317 
318     def setUp(self):
319-        self.path = '/cache/test/'
320         self.cache = get_cache('default')
321 
322     def tearDown(self):
323         self.cache.clear()
324 
325-    def _get_request(self, path, method='GET'):
326-        request = HttpRequest()
327-        request.META = {
328-            'SERVER_NAME': 'testserver',
329-            'SERVER_PORT': 80,
330-        }
331-        request.method = method
332-        request.path = request.path_info = "/cache/%s" % path
333-        return request
334-
335     def test_patch_vary_headers(self):
336         headers = (
337             # Initial vary, new headers, resulting vary.
338@@ -1034,37 +1023,6 @@ class CacheUtils(TestCase):
339             patch_vary_headers(response, newheaders)
340             self.assertEqual(response['Vary'], resulting_vary)
341 
342-    def test_get_cache_key(self):
343-        request = self._get_request(self.path)
344-        response = HttpResponse()
345-        key_prefix = 'localprefix'
346-        # Expect None if no headers have been set yet.
347-        self.assertEqual(get_cache_key(request), None)
348-        # Set headers to an empty list.
349-        learn_cache_key(request, response)
350-        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
351-        # Verify that a specified key_prefix is taken into account.
352-        learn_cache_key(request, response, key_prefix=key_prefix)
353-        self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
354-
355-    def test_get_cache_key_with_query(self):
356-        request = self._get_request(self.path + '?test=1')
357-        response = HttpResponse()
358-        # Expect None if no headers have been set yet.
359-        self.assertEqual(get_cache_key(request), None)
360-        # Set headers to an empty list.
361-        learn_cache_key(request, response)
362-        # Verify that the querystring is taken into account.
363-        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e')
364-
365-    def test_learn_cache_key(self):
366-        request = self._get_request(self.path, 'HEAD')
367-        response = HttpResponse()
368-        response['Vary'] = 'Pony'
369-        # Make sure that the Vary header is added to the key hash
370-        learn_cache_key(request, response)
371-        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
372-
373     def test_patch_cache_control(self):
374         tests = (
375             # Initial Cache-Control, kwargs to patch_cache_control, expected Cache-Control parts
376@@ -1342,6 +1300,7 @@ class CacheMiddlewareTest(TestCase):
377 
378     def setUp(self):
379         self.factory = RequestFactory()
380+        self.path = '/cache/test/'
381         self.default_cache = get_cache('default')
382         self.other_cache = get_cache('other')
383 
384@@ -1381,6 +1340,48 @@ class CacheMiddlewareTest(TestCase):
385         self.assertEqual(as_view_decorator_with_custom.cache_alias, 'other')
386         self.assertEqual(as_view_decorator_with_custom.cache_anonymous_only, True)
387 
388+    def _get_request(self, path, method='GET'):
389+        request = HttpRequest()
390+        request.META = {
391+            'SERVER_NAME': 'testserver',
392+            'SERVER_PORT': 80,
393+        }
394+        request.method = method
395+        request.path = request.path_info = "/cache/%s" % path
396+        return request
397+
398+    def test_get_cache_key(self):
399+        request = self._get_request(self.path)
400+        response = HttpResponse()
401+        key_prefix = 'localprefix'
402+        # Expect None if no headers have been set yet.
403+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None)
404+        # Set headers to an empty list.
405+        UpdateCacheMiddleware.learn_cache_key(request, response)
406+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
407+        # Verify that a specified key_prefix is taken into account.
408+        UpdateCacheMiddleware.learn_cache_key(request, response, key_prefix=key_prefix)
409+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
410+
411+    def test_get_cache_key_with_query(self):
412+        request = self._get_request(self.path + '?test=1')
413+        response = HttpResponse()
414+        # Expect None if no headers have been set yet.
415+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None)
416+        # Set headers to an empty list.
417+        UpdateCacheMiddleware.learn_cache_key(request, response)
418+        # Verify that the querystring is taken into account.
419+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e')
420+
421+    def test_learn_cache_key(self):
422+        request = self._get_request(self.path, 'HEAD')
423+        response = HttpResponse()
424+        response['Vary'] = 'Pony'
425+        # Make sure that the Vary header is added to the key hash
426+        UpdateCacheMiddleware.learn_cache_key(request, response)
427+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
428+
429+
430     def test_middleware(self):
431         middleware = CacheMiddleware()
432         prefix_middleware = CacheMiddleware(key_prefix='prefix1')
433@@ -1624,23 +1625,23 @@ class TestWithTemplateResponse(TestCase):
434         response = TemplateResponse(HttpResponse(), Template("This is a test"))
435         key_prefix = 'localprefix'
436         # Expect None if no headers have been set yet.
437-        self.assertEqual(get_cache_key(request), None)
438+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None)
439         # Set headers to an empty list.
440-        learn_cache_key(request, response)
441-        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
442+        UpdateCacheMiddleware.learn_cache_key(request, response)
443+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
444         # Verify that a specified key_prefix is taken into account.
445-        learn_cache_key(request, response, key_prefix=key_prefix)
446-        self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
447+        UpdateCacheMiddleware.learn_cache_key(request, response, key_prefix=key_prefix)
448+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.GET.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e')
449 
450     def test_get_cache_key_with_query(self):
451         request = self._get_request(self.path + '?test=1')
452         response = TemplateResponse(HttpResponse(), Template("This is a test"))
453         # Expect None if no headers have been set yet.
454-        self.assertEqual(get_cache_key(request), None)
455+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), None)
456         # Set headers to an empty list.
457-        learn_cache_key(request, response)
458+        UpdateCacheMiddleware.learn_cache_key(request, response)
459         # Verify that the querystring is taken into account.
460-        self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e')
461+        self.assertEqual(TwoPartCacheMiddlewareBase.get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.GET.bd889c5a59603af44333ed21504db3cd.d41d8cd98f00b204e9800998ecf8427e')
462 
463     @override_settings(USE_ETAGS=False)
464     def test_without_etag(self):
Back to Top