Browse Source

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

Thomas Tanner 10 years ago
parent
commit
b27db97b23

+ 35 - 29
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)

+ 3 - 0
docs/releases/1.8.txt

@@ -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
 ^^^^^
 

+ 13 - 3
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.
-

+ 60 - 0
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/')