Fixed #22461 -- Added if-unmodified-since support to the condition decorator.
This commit is contained in:
parent
fae551d765
commit
b27db97b23
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
^^^^^
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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/')
|
||||
|
|
Loading…
Reference in New Issue