diff --git a/README.rst b/README.rst index ac74b79..f634b9a 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 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 ------------ diff --git a/djangocache.py b/djangocache.py index 6f24872..88d512b 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): @@ -20,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 @@ -53,6 +56,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 +73,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 @@ -116,7 +118,8 @@ class CacheMiddleware(cache_middleware.CacheMiddleware): 'HTTP_IF_MATCH': 'If-Match', } - def __init__(self, *args, **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): self.get_key_prefix = self.key_prefix @@ -130,14 +133,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 +151,20 @@ 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 = 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 + return None return response diff --git a/settings.py b/settings.py index ca32549..8a12be2 100644 --- a/settings.py +++ b/settings.py @@ -1,30 +1,2 @@ -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..1badf59 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()) @@ -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'), @@ -355,6 +361,128 @@ 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', + HTTP_PRAGMA='no-cache', + ) + 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']) + 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_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() @@ -545,3 +673,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='))