Skip to content

Commit

Permalink
Merge d318f47 into 45a295f
Browse files Browse the repository at this point in the history
  • Loading branch information
renskiy committed Oct 17, 2017
2 parents 45a295f + d318f47 commit f577fb3
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 44 deletions.
9 changes: 7 additions & 2 deletions README.rst
Expand Up @@ -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
------------

Expand Down
34 changes: 21 additions & 13 deletions 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
Expand All @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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

Expand Down
28 changes: 0 additions & 28 deletions 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
134 changes: 133 additions & 1 deletion tests.py
Expand Up @@ -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())

Expand All @@ -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()
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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='))

0 comments on commit f577fb3

Please sign in to comment.