From f6633dd140a8b70671d9beab773f0d242bd7e42e Mon Sep 17 00:00:00 2001 From: Rinat Khabibiev Date: Tue, 17 Oct 2017 10:14:25 +0300 Subject: [PATCH 1/7] client can provide max cache age --- README.rst | 9 +++++-- djangocache.py | 27 +++++++++++--------- settings.py | 30 ---------------------- tests.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 45 deletions(-) delete mode 100644 settings.py diff --git a/README.rst b/README.rst index ac74b79..0b3d95c 100644 --- a/README.rst +++ b/README.rst @@ -47,15 +47,20 @@ If you planning to use :code:`cache_page` among with :code:`last_modified` and/o from django.views.decorators.http import last_modified, etag - def etag_generator(request): + def etag_generator(request, *args, **kwargs): return 'ETag!!' @cache_page(cache_timeout=600) @etag(etag_generator) - def view(request): + def view(request, *args, **kwargs): pass +Django Settings +--------------- + +``DJANGOCACHE_CACHE_MIN_AGE`` - used to limit minimal age of cache. Default is 0, meaning that client can ask server to skip cache by providing header ``Cache-Control: max-age=0``. + Installation ------------ diff --git a/djangocache.py b/djangocache.py index 6f24872..f2d57d9 100644 --- a/djangocache.py +++ b/djangocache.py @@ -1,6 +1,7 @@ import contextlib import time +from django.conf import settings from django.core.cache.backends.dummy import DummyCache from django.middleware import cache as cache_middleware from django.utils import http, cache, decorators @@ -10,7 +11,7 @@ dummy_cache = DummyCache('dummy_host', {}) # https://tools.ietf.org/html/rfc7232#section-4.1 -rfc7232_headers = ['ETag', 'Vary', 'Cache-Control', 'Expires', 'Content-Location'] +rfc7232_headers = ['ETag', 'Vary', 'Cache-Control', 'Expires', 'Content-Location', 'Date', 'Last-Modified'] def cache_page(**kwargs): @@ -53,6 +54,7 @@ def get_cache_max_age(cache_control): def get_conditional_response(request, response=None): if not (response and hasattr(cache, 'get_conditional_response')): + # Django 1.8 does not have such method, can't do anything return response last_modified = response.get('Last-Modified') conditional_response = cache.get_conditional_response( @@ -69,8 +71,6 @@ def get_conditional_response(request, response=None): } for header, value in headers.items(): conditional_response[header] = value - if last_modified: - conditional_response['Last-Modified'] = last_modified return conditional_response @@ -130,14 +130,6 @@ def get_key_prefix(self, request, *args, **kwargs): return self.key_prefix def process_request(self, request): - if request.method not in ('GET', 'HEAD'): - return None - - cache_max_age = get_cache_max_age(request.META.get('HTTP_CACHE_CONTROL')) - if cache_max_age == 0: - request._cache_update_cache = True - return None - request._cache_key_prefix = key_prefix = self.get_key_prefix( request, *request.resolver_match.args, @@ -156,7 +148,18 @@ def process_request(self, request): if max_age: expires = http.parse_http_date(response['Expires']) timeout = expires - int(time.time()) - response['Age'] = max_age - timeout + response['Age'] = age = max_age - timeout + + # check cache age limit provided by client + age_limit = get_cache_max_age(request.META.get('HTTP_CACHE_CONTROL')) + if age_limit is None and request.META.get('HTTP_PRAGMA') == 'no-cache': + age_limit = 0 + if age_limit is not None: + min_age = getattr(settings, 'DJANGOCACHE_CACHE_MIN_AGE', 0) + age_limit = max(min_age, age_limit) + if age > age_limit: + request._cache_update_cache = True + return None return response diff --git a/settings.py b/settings.py deleted file mode 100644 index ca32549..0000000 --- a/settings.py +++ /dev/null @@ -1,30 +0,0 @@ -import os - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'secret_key' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - -CACHE_MIDDLEWARE_SECONDS = 600 - -USE_ETAGS = True - -INSTALLED_APPS = [ - 'djangocache', -] - -MIDDLEWARE_CLASSES = [] - - -# Internationalization -# https://docs.djangoproject.com/en/1.9/topics/i18n/ - -TIME_ZONE = 'UTC' - -USE_TZ = True diff --git a/tests.py b/tests.py index 9c2410f..aa55fd6 100644 --- a/tests.py +++ b/tests.py @@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse from django.views.decorators.http import last_modified, etag -from djangocache import cache_page +from djangocache import cache_page, get_cache_max_age mocked_response = mock.Mock(side_effect=lambda: http.HttpResponse()) @@ -355,6 +355,67 @@ def test_static(self): self.assertIn('Cache-Control', response) self.assertEqual('Mon, 18 Jul 2016 10:05:00 GMT', response['Expires']) self.assertEqual('max-age=86400', response['Cache-Control']) + mocked_response.reset_mock() + + # Sun, 17 Jul 2016 10:10:00 GMT + with mock.patch.object(time, 'time', return_value=1468750200): + response = client.get( + reverse('static'), + HTTP_PRAGMA='no-cache', + ) + mocked_response.assert_called_once() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertEqual('Mon, 18 Jul 2016 10:10:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) + mocked_response.reset_mock() + + # Sun, 17 Jul 2016 10:15:00 GMT + with mock.patch.object(time, 'time', return_value=1468750500): + response = client.get( + reverse('static'), + HTTP_CACHE_CONTROL='max-age=600', + ) + mocked_response.assert_not_called() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertIn('Age', response) + self.assertEqual('Mon, 18 Jul 2016 10:10:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) + self.assertEqual('300', response['Age']) + mocked_response.reset_mock() + + with test.utils.override_settings(DJANGOCACHE_CACHE_MIN_AGE=600): + response = client.get( + reverse('static'), + HTTP_CACHE_CONTROL='max-age=200', + ) + mocked_response.assert_not_called() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertIn('Age', response) + self.assertEqual('Mon, 18 Jul 2016 10:10:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) + self.assertEqual('300', response['Age']) + mocked_response.reset_mock() + + response = client.get( + reverse('static'), + HTTP_CACHE_CONTROL='max-age=200', + ) + mocked_response.assert_called_once() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertEqual('Mon, 18 Jul 2016 10:15:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) def test_with_last_modified(self): client = test.Client() @@ -545,3 +606,7 @@ def test_no_cache(self): self.assertNotIn('Last-Modified', response) self.assertNotIn('Expires', response) self.assertNotIn('Cache-Control', response) + + def test_get_cache_max_age_returns_none_on_wrong_or_empty_result(self): + self.assertIsNone(get_cache_max_age('max-age=a')) + self.assertIsNone(get_cache_max_age('max-age=')) From 9a3969545612a44cc3459404ad79b78314db17fc Mon Sep 17 00:00:00 2001 From: Rinat Khabibiev Date: Tue, 17 Oct 2017 10:20:51 +0300 Subject: [PATCH 2/7] updated DJANGOCACHE_CACHE_MIN_AGE description --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0b3d95c..f634b9a 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,7 @@ If you planning to use :code:`cache_page` among with :code:`last_modified` and/o Django Settings --------------- -``DJANGOCACHE_CACHE_MIN_AGE`` - used to limit minimal age of cache. Default is 0, meaning that client can ask server to skip cache by providing header ``Cache-Control: max-age=0``. +``DJANGOCACHE_CACHE_MIN_AGE`` - used to set minimal age of cache. Default is 0, meaning that client can ask server to skip cache by providing header ``Cache-Control: max-age=0``. Installation ------------ From 27628093e9ace3c31b7ff57863e5bee3c69a5997 Mon Sep 17 00:00:00 2001 From: Rinat Khabibiev Date: Tue, 17 Oct 2017 10:25:42 +0300 Subject: [PATCH 3/7] dummy settings --- settings.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 settings.py diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..e69de29 From f0cca73f1a81ed6ba9462944fd256368af28fbac Mon Sep 17 00:00:00 2001 From: Rinat Khabibiev Date: Tue, 17 Oct 2017 10:27:57 +0300 Subject: [PATCH 4/7] dummy settings --- settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/settings.py b/settings.py index e69de29..8a12be2 100644 --- a/settings.py +++ b/settings.py @@ -0,0 +1,2 @@ +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'secret_key' From fcad79de1e0403aa022f3cb2a4d4d737770cea38 Mon Sep 17 00:00:00 2001 From: Rinat Khabibiev Date: Tue, 17 Oct 2017 13:43:20 +0300 Subject: [PATCH 5/7] skip cache if age reached age_limit --- djangocache.py | 2 +- tests.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/djangocache.py b/djangocache.py index f2d57d9..aef81ed 100644 --- a/djangocache.py +++ b/djangocache.py @@ -157,7 +157,7 @@ def process_request(self, request): if age_limit is not None: min_age = getattr(settings, 'DJANGOCACHE_CACHE_MIN_AGE', 0) age_limit = max(min_age, age_limit) - if age > age_limit: + if age >= age_limit: request._cache_update_cache = True return None diff --git a/tests.py b/tests.py index aa55fd6..8d821a3 100644 --- a/tests.py +++ b/tests.py @@ -377,6 +377,7 @@ def test_static(self): response = client.get( reverse('static'), HTTP_CACHE_CONTROL='max-age=600', + HTTP_PRAGMA='no-cache', ) mocked_response.assert_not_called() self.assertNotIn('ETag', response) @@ -416,6 +417,19 @@ def test_static(self): self.assertIn('Cache-Control', response) self.assertEqual('Mon, 18 Jul 2016 10:15:00 GMT', response['Expires']) self.assertEqual('max-age=86400', response['Cache-Control']) + mocked_response.reset_mock() + + response = client.get( + reverse('static'), + HTTP_CACHE_CONTROL='max-age=0', + ) + mocked_response.assert_called_once() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertEqual('Mon, 18 Jul 2016 10:15:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) def test_with_last_modified(self): client = test.Client() From 66ec2510f2b790532f34ff92e54ffcaaad2e15ae Mon Sep 17 00:00:00 2001 From: Rinat Khabibiev Date: Tue, 17 Oct 2017 14:20:53 +0300 Subject: [PATCH 6/7] cache_page's cache_min_age optional argument --- djangocache.py | 9 +++++++-- tests.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/djangocache.py b/djangocache.py index aef81ed..ebfd296 100644 --- a/djangocache.py +++ b/djangocache.py @@ -21,10 +21,12 @@ def cache_page(**kwargs): cache_timeout = kwargs.get('cache_timeout') cache_alias = kwargs.get('cache_alias') key_prefix = kwargs.get('key_prefix') + cache_min_age = kwargs.get('cache_min_age') decorator = decorators.decorator_from_middleware_with_args(CacheMiddleware)( cache_timeout=cache_timeout, cache_alias=cache_alias, key_prefix=key_prefix, + cache_min_age=cache_min_age, ) return decorator @@ -116,7 +118,8 @@ class CacheMiddleware(cache_middleware.CacheMiddleware): 'HTTP_IF_MATCH': 'If-Match', } - def __init__(self, *args, **kwargs): + def __init__(self, *args, cache_min_age=None, **kwargs): + self.cache_min_age = cache_min_age super(CacheMiddleware, self).__init__(*args, **kwargs) if callable(self.key_prefix): self.get_key_prefix = self.key_prefix @@ -155,7 +158,9 @@ def process_request(self, request): if age_limit is None and request.META.get('HTTP_PRAGMA') == 'no-cache': age_limit = 0 if age_limit is not None: - min_age = getattr(settings, 'DJANGOCACHE_CACHE_MIN_AGE', 0) + min_age = self.cache_min_age + if min_age is None: + min_age = getattr(settings, 'DJANGOCACHE_CACHE_MIN_AGE', 0) age_limit = max(min_age, age_limit) if age >= age_limit: request._cache_update_cache = True diff --git a/tests.py b/tests.py index 8d821a3..1badf59 100644 --- a/tests.py +++ b/tests.py @@ -22,6 +22,11 @@ def static(request): return mocked_response() +@cache_page(cache_timeout=24 * 60 * 60, cache_min_age=600) +def static2(request): + return mocked_response() + + @cache_page() def default(request): return mocked_response() @@ -62,6 +67,7 @@ def process_response(self, request, response): return response urlpatterns = [ + urls.url(r'static2', static2, name='static2'), urls.url(r'static', static, name='static'), urls.url(r'default', default, name='default'), urls.url(r'no_cache', no_cache, name='no_cache'), @@ -431,6 +437,53 @@ def test_static(self): self.assertEqual('Mon, 18 Jul 2016 10:15:00 GMT', response['Expires']) self.assertEqual('max-age=86400', response['Cache-Control']) + def test_static2(self): + client = test.Client() + + # Sun, 17 Jul 2016 10:00:00 GMT + with mock.patch.object(time, 'time', return_value=1468749600): + response = client.get(reverse('static2')) + mocked_response.assert_called_once() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertEqual('Mon, 18 Jul 2016 10:00:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) + mocked_response.reset_mock() + + # Sun, 17 Jul 2016 10:05:00 GMT + with mock.patch.object(time, 'time', return_value=1468749900): + response = client.get( + reverse('static2'), + HTTP_CACHE_CONTROL='max-age=300', + ) + mocked_response.assert_not_called() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertIn('Age', response) + self.assertEqual('Mon, 18 Jul 2016 10:00:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) + self.assertEqual('300', response['Age']) + mocked_response.reset_mock() + + # Sun, 17 Jul 2016 10:10:00 GMT + with mock.patch.object(time, 'time', return_value=1468750200): + response = client.get( + reverse('static'), + HTTP_CACHE_CONTROL='max-age=300', + ) + mocked_response.assert_called_once() + self.assertNotIn('ETag', response) + self.assertNotIn('Last-Modified', response) + self.assertIn('Expires', response) + self.assertIn('Cache-Control', response) + self.assertEqual('Mon, 18 Jul 2016 10:10:00 GMT', response['Expires']) + self.assertEqual('max-age=86400', response['Cache-Control']) + mocked_response.reset_mock() + def test_with_last_modified(self): client = test.Client() From d318f476cf65af26ced770725a9a53600745bdcf Mon Sep 17 00:00:00 2001 From: Rinat Khabibiev Date: Tue, 17 Oct 2017 17:30:05 +0300 Subject: [PATCH 7/7] fixed 2.7 syntax --- djangocache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangocache.py b/djangocache.py index ebfd296..88d512b 100644 --- a/djangocache.py +++ b/djangocache.py @@ -118,7 +118,7 @@ class CacheMiddleware(cache_middleware.CacheMiddleware): 'HTTP_IF_MATCH': 'If-Match', } - def __init__(self, *args, cache_min_age=None, **kwargs): + def __init__(self, cache_min_age=None, *args, **kwargs): self.cache_min_age = cache_min_age super(CacheMiddleware, self).__init__(*args, **kwargs) if callable(self.key_prefix):