Fixed #22461 -- Added if-unmodified-since support to the condition decorator.

This commit is contained in:
Thomas Tanner 2014-12-20 22:14:46 +01:00 committed by Tim Graham
parent fae551d765
commit b27db97b23
4 changed files with 111 additions and 32 deletions

View File

@ -52,6 +52,16 @@ require_safe = require_http_methods(["GET", "HEAD"])
require_safe.__doc__ = "Decorator to require that a view only accept safe methods: GET and HEAD."
def _precondition_failed(request):
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': 412,
'request': request
},
)
return HttpResponse(status=412)
def condition(etag_func=None, last_modified_func=None):
"""
Decorator to support conditional retrieval (or change) for a view
@ -81,8 +91,12 @@ def condition(etag_func=None, last_modified_func=None):
if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
if if_modified_since:
if_modified_since = parse_http_date_safe(if_modified_since)
if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
if if_unmodified_since:
if_unmodified_since = parse_http_date_safe(if_unmodified_since)
if_none_match = request.META.get("HTTP_IF_NONE_MATCH")
if_match = request.META.get("HTTP_IF_MATCH")
etags = []
if if_none_match or if_match:
# There can be more than one ETag in the request, so we
# consider the list of values.
@ -97,21 +111,19 @@ def condition(etag_func=None, last_modified_func=None):
if_match = None
# Compute values (if any) for the requested resource.
if etag_func:
res_etag = etag_func(request, *args, **kwargs)
else:
res_etag = None
def get_last_modified():
if last_modified_func:
dt = last_modified_func(request, *args, **kwargs)
if dt:
res_last_modified = timegm(dt.utctimetuple())
else:
res_last_modified = None
else:
res_last_modified = None
return timegm(dt.utctimetuple())
res_etag = etag_func(request, *args, **kwargs) if etag_func else None
res_last_modified = get_last_modified()
response = None
if not ((if_match and (if_modified_since or if_none_match)) or
if not ((if_match and if_modified_since) or
(if_none_match and if_unmodified_since) or
(if_modified_since and if_unmodified_since) or
(if_match and if_none_match)):
# We only get here if no undefined combinations of headers are
# specified.
@ -123,26 +135,20 @@ def condition(etag_func=None, last_modified_func=None):
if request.method in ("GET", "HEAD"):
response = HttpResponseNotModified()
else:
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': 412,
'request': request
}
)
response = HttpResponse(status=412)
elif if_match and ((not res_etag and "*" in etags) or
(res_etag and res_etag not in etags)):
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': 412,
'request': request
}
)
response = HttpResponse(status=412)
response = _precondition_failed(request)
elif (if_match and ((not res_etag and "*" in etags) or
(res_etag and res_etag not in etags) or
(res_last_modified and if_unmodified_since and
res_last_modified > if_unmodified_since))):
response = _precondition_failed(request)
elif (not if_none_match and request.method in ("GET", "HEAD") and
res_last_modified and if_modified_since and
res_last_modified <= if_modified_since):
response = HttpResponseNotModified()
elif (not if_match and
res_last_modified and if_unmodified_since and
res_last_modified > if_unmodified_since):
response = _precondition_failed(request)
if response is None:
response = func(request, *args, **kwargs)

View File

@ -528,6 +528,9 @@ Requests and Responses
<django.http.HttpResponse.setdefault>` method allows setting a header unless
it has already been set.
* The :func:`~django.views.decorators.http.condition` decorator for
conditional view processing now supports the ``If-unmodified-since`` header.
Tests
^^^^^

View File

@ -15,18 +15,29 @@ or you can rely on the :class:`~django.middleware.common.CommonMiddleware`
middleware to set the ``ETag`` header.
When the client next requests the same resource, it might send along a header
such as `If-modified-since`_, containing the date of the last modification
time it was sent, or `If-none-match`_, containing the ``ETag`` it was sent.
such as either `If-modified-since`_ or `If-unmodified-since`_, containing the
date of the last modification time it was sent, or either `If-match`_ or
`If-none-match`_, containing the last ``ETag`` it was sent.
If the current version of the page matches the ``ETag`` sent by the client, or
if the resource has not been modified, a 304 status code can be sent back,
instead of a full response, telling the client that nothing has changed.
Depending on the header, if the page has been modified or does not match the
``ETag`` sent by the client, a 412 status code (Precondition Failed) may be
returned.
.. _If-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
.. _If-none-match: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
.. _If-modified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
.. _If-unmodified-since: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28
When you need more fine-grained control you may use per-view conditional
processing functions.
.. versionchanged:: 1.8
Support for the ``If-unmodified-since`` header was added to conditional
view processing.
.. _conditional-decorators:
The ``condition`` decorator
@ -194,4 +205,3 @@ view takes a while to generate the content, you should consider using the
fairly quickly, stick to using the middleware and the amount of network
traffic sent back to the clients will still be reduced if the view hasn't
changed.

View File

@ -49,6 +49,20 @@ class ConditionalGet(TestCase):
response = self.client.get('/condition/')
self.assertFullResponse(response)
def test_if_unmodified_since(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_NEWER_STR
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_INVALID_STR
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
def test_if_none_match(self):
self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/')
@ -71,6 +85,7 @@ class ConditionalGet(TestCase):
self.assertEqual(response.status_code, 412)
def test_both_headers(self):
# see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/')
@ -86,6 +101,32 @@ class ConditionalGet(TestCase):
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG
response = self.client.get('/condition/')
self.assertFullResponse(response)
def test_both_headers_2(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/')
self.assertFullResponse(response)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG
response = self.client.get('/condition/')
self.assertEqual(response.status_code, 412)
def test_single_condition_1(self):
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified/')
@ -124,6 +165,25 @@ class ConditionalGet(TestCase):
response = self.client.get('/condition/last_modified2/')
self.assertFullResponse(response, check_etag=False)
def test_single_condition_7(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified/')
self.assertEqual(response.status_code, 412)
response = self.client.get('/condition/etag/')
self.assertFullResponse(response, check_last_modified=False)
def test_single_condition_8(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified/')
self.assertFullResponse(response, check_etag=False)
def test_single_condition_9(self):
self.client.defaults['HTTP_IF_UNMODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR
response = self.client.get('/condition/last_modified2/')
self.assertEqual(response.status_code, 412)
response = self.client.get('/condition/etag2/')
self.assertFullResponse(response, check_last_modified=False)
def test_single_condition_head(self):
self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR
response = self.client.head('/condition/')