diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index bfb68295d1..990c3770db 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -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 - 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 + def get_last_modified(): + if last_modified_func: + dt = last_modified_func(request, *args, **kwargs) + if dt: + 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) diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 26fc2c17a2..7f8005088a 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -528,6 +528,9 @@ Requests and Responses ` 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 ^^^^^ diff --git a/docs/topics/conditional-view-processing.txt b/docs/topics/conditional-view-processing.txt index 83b454a4aa..77080266de 100644 --- a/docs/topics/conditional-view-processing.txt +++ b/docs/topics/conditional-view-processing.txt @@ -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. - diff --git a/tests/conditional_processing/tests.py b/tests/conditional_processing/tests.py index 72dd1dd48c..f679996efd 100644 --- a/tests/conditional_processing/tests.py +++ b/tests/conditional_processing/tests.py @@ -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/')