From 6cd90236350b6531122ddbb5a99874add09f3f15 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 23 Aug 2011 03:51:10 +0000 Subject: [PATCH] Fixed #15499 -- Ensure that cache control headers don't try to set public and private as a result of multiple calls to patch_cache_control with different arguments. Thanks to AndiDog for the report and patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@16657 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/utils/cache.py | 6 ++++++ docs/topics/cache.txt | 22 ++++++++++++++++++++++ tests/regressiontests/cache/tests.py | 28 +++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/django/utils/cache.py b/django/utils/cache.py index 81a8279120..45d8a95f66 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -66,6 +66,12 @@ def patch_cache_control(response, **kwargs): if 'max-age' in cc and 'max_age' in kwargs: kwargs['max_age'] = min(cc['max-age'], kwargs['max_age']) + # Allow overriding private caching and vice versa + if 'private' in cc and 'public' in kwargs: + del cc['private'] + elif 'public' in cc and 'private' in kwargs: + del cc['public'] + for (k, v) in kwargs.items(): cc[k.replace('_', '-')] = v cc = ', '.join([dictvalue(el) for el in cc.items()]) diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index a5e8a672f8..10b63b7b04 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -1059,6 +1059,28 @@ Django, use the ``cache_control`` view decorator. Example:: This decorator takes care of sending out the appropriate HTTP header behind the scenes. +Note that the cache control settings "private" and "public" are mutually +exclusive. The decorator ensures that the "public" directive is removed if +"private" should be set (and vice versa). An example use of the two directives +would be a blog site that offers both private and public entries. Public +entries may be cached on any shared cache. The following code uses +``patch_cache_control``, the manual way to modify the cache control header +(it is internally called by the ``cache_control`` decorator):: + + from django.views.decorators.cache import patch_cache_control + from django.views.decorators.vary import vary_on_cookie + + @vary_on_cookie + def list_blog_entries_view(request): + if request.user.is_anonymous(): + response = render_only_public_entries() + patch_cache_control(response, public=True) + else: + response = render_private_and_public_entries(request.user) + patch_cache_control(response, private=True) + + return response + There are a few other ways to control cache parameters. For example, HTTP allows applications to do the following: diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index 7eafe12917..fad60570c9 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -5,6 +5,7 @@ import hashlib import os +import re import tempfile import time import warnings @@ -19,7 +20,7 @@ from django.test import RequestFactory from django.test.utils import get_warnings_state, restore_warnings_state from django.utils import translation from django.utils import unittest -from django.utils.cache import patch_vary_headers, get_cache_key, learn_cache_key +from django.utils.cache import patch_vary_headers, get_cache_key, learn_cache_key, patch_cache_control from django.views.decorators.cache import cache_page from regressiontests.cache.models import Poll, expensive_calculation @@ -1003,6 +1004,31 @@ class CacheUtils(unittest.TestCase): learn_cache_key(request, response) self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.HEAD.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + def test_patch_cache_control(self): + tests = ( + # Initial Cache-Control, kwargs to patch_cache_control, expected Cache-Control parts + (None, {'private' : True}, set(['private'])), + + # Test whether private/public attributes are mutually exclusive + ('private', {'private' : True}, set(['private'])), + ('private', {'public' : True}, set(['public'])), + ('public', {'public' : True}, set(['public'])), + ('public', {'private' : True}, set(['private'])), + ('must-revalidate,max-age=60,private', {'public' : True}, set(['must-revalidate', 'max-age=60', 'public'])), + ('must-revalidate,max-age=60,public', {'private' : True}, set(['must-revalidate', 'max-age=60', 'private'])), + ('must-revalidate,max-age=60', {'public' : True}, set(['must-revalidate', 'max-age=60', 'public'])), + ) + + cc_delim_re = re.compile(r'\s*,\s*') + + for initial_cc, newheaders, expected_cc in tests: + response = HttpResponse() + if initial_cc is not None: + response['Cache-Control'] = initial_cc + patch_cache_control(response, **newheaders) + parts = set(cc_delim_re.split(response['Cache-Control'])) + self.assertEqual(parts, expected_cc) + class PrefixedCacheUtils(CacheUtils): def setUp(self): super(PrefixedCacheUtils, self).setUp()