Ticket #5791: 5791.4.diff
File 5791.4.diff, 12.9 KB (added by , 16 years ago) |
---|
-
django/views/decorators/http.py
=== modified file 'django/views/decorators/http.py'
7 7 except ImportError: 8 8 from django.utils.functional import wraps # Python 2.3, 2.4 fallback. 9 9 10 from calendar import timegm 11 from datetime import timedelta 12 from email.Utils import formatdate 10 13 from django.utils.decorators import decorator_from_middleware 11 14 from django.middleware.http import ConditionalGetMiddleware 12 from django.http import HttpResponseNotAllowed 15 from django.http import HttpResponseNotAllowed, HttpResponseNotModified 13 16 14 17 conditional_page = decorator_from_middleware(ConditionalGetMiddleware) 15 18 … … 36 39 require_GET.__doc__ = "Decorator to require that a view only accept the GET method." 37 40 38 41 require_POST = require_http_methods(["POST"]) 39 require_POST.__doc__ = "Decorator to require that a view only accept the POST method." 40 No newline at end of file 42 require_POST.__doc__ = "Decorator to require that a view only accept the POST method." 43 44 def condition(etag=None, last_modified=None): 45 """ 46 Decorator to support conditional get for a view. It takes as parameters 47 user-defined functions that calculate etag and/or last modified time. 48 49 Both functions are passed the same parameters as the view itself. "last_modified" 50 should return a standard datetime value and "etag" should return a string. 51 52 Example usage with last_modified:: 53 54 @condition(last_modified=lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time) 55 def my_object_view(request, obj_id): 56 # ... 57 58 You can pass last_modified, etag or both of them (if you really need it). 59 """ 60 def decorator(func): 61 def inner(request, *args, **kwargs): 62 if request.method not in ('GET', 'HEAD'): 63 return func(request, *args, **kwargs) 64 65 # Get HTTP request headers 66 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) 67 if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None) 68 if if_none_match: 69 if_none_match = [e.strip() for e in if_none_match.split(',')] 70 71 # Get and convert user-defined values 72 if last_modified is not None: 73 dt = last_modified(request, *args, **kwargs) 74 last_modified_value = dt and (formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT') 75 else: 76 last_modified_value = None 77 78 if etag is not None: 79 etag_value = etag(request, *args, **kwargs) 80 else: 81 etag_value = None 82 83 # Calculate "not modified" condition 84 not_modified = (if_modified_since or if_none_match) and \ 85 (not if_modified_since or last_modified_value == if_modified_since) and \ 86 (not if_none_match or etag_value in if_none_match) 87 88 # Create appropriate response 89 if not_modified: 90 response = HttpResponseNotModified() 91 else: 92 response = func(request, *args, **kwargs) 93 94 # Set relevant headers for response 95 if last_modified_value and not response.has_header('Last-Modified'): 96 response['Last-Modified'] = last_modified_value 97 if etag_value and not response.has_header('ETag'): 98 response['ETag'] = etag_value 99 100 return response 101 return inner 102 return decorator 103 104 # Shortcut decorators for common cases based on ETag or Last-Modified only 105 def etag(callable): 106 return condition(etag=callable) 107 108 def last_modified(callable): 109 return condition(last_modified=callable) 110 No newline at end of file -
docs/index.txt
=== modified file 'docs/index.txt'
81 81 * :ref:`Admin site <ref-contrib-admin>` 82 82 * :ref:`Authentication <topics-auth>` 83 83 * :ref:`Cache system <topics-cache>` 84 * :ref:`Conditional get <topics-conditional-get>` 84 85 * :ref:`Comments <ref-contrib-comments-index>` 85 86 * :ref:`Content types <ref-contrib-contenttypes>` 86 87 * :ref:`Cross Site Request Forgery protection <ref-contrib-csrf>` -
docs/topics/conditional-get.txt
=== added file 'docs/topics/conditional-get.txt'
1 ======================== 2 Per-view conditional get 3 ======================== 4 5 `Conditional get`_ is a feature of HTTP that allows you to omit sending a response 6 if the client signals in a request that it has a cached copy of reponse. To control 7 whether the cached copy is expired the client may use time of its last modification 8 or an "entity tag" - a short value that changes whenever the response itself changes 9 (typically it's simply a hash over the response). 10 11 .. _`Conditional get`: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3 12 13 Django allows simple usage of this feature with ``ConditionalGetMiddleware`` and 14 ``CommonMiddleware`` (see `middleware documentation`_ for their usage). But, while 15 simple, they both have limitations: 16 17 * they are applied gloablly to all views in your project 18 * they don't spare you from generating the response itself, which may be expensive 19 20 .. _`middleware documentation`: ../middleware/ 21 22 Decorators 23 ========== 24 25 When you need more fine-grained control you may use per-view conditional get 26 decorators. 27 28 Decorators ``etag`` and ``last_modified`` accept a user-defined function that 29 takes the same parameters as the view itself. ``last_modified`` should return a 30 standard datetime value and ``etag`` should return a string. 31 32 Example usage of ``last_modified``:: 33 34 @last_modified(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time) 35 def my_object_view(request, obj_id): 36 # Expensive generation of response with MyObject instance 37 38 In this example we're taking advantage of the fact that MyObject instance stores 39 the time when it was last updated in a field. 40 41 Usage of ``etag`` is similar. 42 43 HTTP allows to use both "ETag" and "Last-Modified" headers in your response. Then 44 a response is considered not modified only if the client sends both headers back and 45 they're both equal to the response headers. This means that you can't just chain 46 decorators on your view: 47 48 @etag(etag_func) 49 @last_modified(last_modified_func) 50 def my_view(request): 51 # ... 52 53 Because the first decorator doesn't know anything about the second and can answer 54 that the response is not modified even if the second decorators would think otherwise. 55 In this case you should use a more general decorator - ``condition`` that accepts 56 two functions at once: 57 58 @condition(etag_func, last_modified_func) 59 def my_view(request): 60 # ... -
tests/regressiontests/conditional_get/__init__.py
=== added directory 'tests/regressiontests/conditional_get' === added file 'tests/regressiontests/conditional_get/__init__.py'
1 # -*- coding:utf-8 -*- -
tests/regressiontests/conditional_get/models.py
=== added file 'tests/regressiontests/conditional_get/models.py'
1 # -*- coding:utf-8 -*- 2 from datetime import datetime, timedelta 3 from calendar import timegm 4 from django.test import TestCase 5 6 FULL_RESPONSE = 'Test conditional get response' 7 LAST_MODIFIED = datetime(2007, 10, 21, 23, 21, 47) 8 LAST_MODIFIED_STR = 'Sun, 21 Oct 2007 23:21:47 GMT' 9 EXPIRED_LAST_MODIFIED_STR = 'Sat, 20 Oct 2007 23:21:47 GMT' 10 ETAG = '"b4246ffc4f62314ca13147c9d4f76974"' 11 EXPIRED_ETAG = '"7fae4cd4b0f81e7d2914700043aa8ed6"' 12 13 class ConditionalGet(TestCase): 14 def assertFullResponse(self, response, check_last_modified=True, check_etag=True): 15 self.assertEquals(response.status_code, 200) 16 self.assertEquals(response.content, FULL_RESPONSE) 17 if check_last_modified: 18 self.assertEquals(response['Last-Modified'], LAST_MODIFIED_STR) 19 if check_etag: 20 self.assertEquals(response['ETag'], ETAG) 21 22 def assertNotModified(self, response): 23 self.assertEquals(response.status_code, 304) 24 self.assertEquals(response.content, '') 25 26 def testWithoutConditions(self): 27 response = self.client.get('/condition/') 28 self.assertFullResponse(response) 29 30 def testIfModifiedSince(self): 31 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 32 response = self.client.get('/condition/') 33 self.assertNotModified(response) 34 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR 35 response = self.client.get('/condition/') 36 self.assertFullResponse(response) 37 38 def testIfNoneMatch(self): 39 self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG 40 response = self.client.get('/condition/') 41 self.assertNotModified(response) 42 self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG 43 response = self.client.get('/condition/') 44 self.assertFullResponse(response) 45 46 # Several etags in If-None-Match is a bit exotic but why not? 47 self.client.defaults['HTTP_IF_NONE_MATCH'] = ', '.join([ETAG, EXPIRED_ETAG]) 48 response = self.client.get('/condition/') 49 self.assertNotModified(response) 50 51 def testBothHeaders(self): 52 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 53 self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG 54 response = self.client.get('/condition/') 55 self.assertNotModified(response) 56 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR 57 self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG 58 response = self.client.get('/condition/') 59 self.assertFullResponse(response) 60 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 61 self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG 62 response = self.client.get('/condition/') 63 self.assertFullResponse(response) 64 65 def testSingleConditions(self): 66 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR 67 response = self.client.get('/condition/last_modified/') 68 self.assertNotModified(response) 69 response = self.client.get('/condition/etag/') 70 self.assertFullResponse(response, check_last_modified=False) 71 72 del self.client.defaults['HTTP_IF_MODIFIED_SINCE'] 73 self.client.defaults['HTTP_IF_NONE_MATCH'] = ETAG 74 response = self.client.get('/condition/etag/') 75 self.assertNotModified(response) 76 response = self.client.get('/condition/last_modified/') 77 self.assertFullResponse(response, check_etag=False) 78 79 self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR 80 self.client.defaults['HTTP_IF_NONE_MATCH'] = EXPIRED_ETAG 81 response = self.client.get('/condition/last_modified/') 82 self.assertFullResponse(response, check_etag=False) 83 response = self.client.get('/condition/etag/') 84 self.assertFullResponse(response, check_last_modified=False) 85 No newline at end of file -
tests/regressiontests/conditional_get/urls.py
=== added file 'tests/regressiontests/conditional_get/urls.py'
1 from django.conf.urls.defaults import * 2 import views 3 4 urlpatterns = patterns('', 5 ('^$', views.index), 6 ('^last_modified/$', views.last_modified), 7 ('^etag/$', views.etag), 8 ) -
tests/regressiontests/conditional_get/views.py
=== added file 'tests/regressiontests/conditional_get/views.py'
1 # -*- coding:utf-8 -*- 2 from django.views.decorators.http import condition 3 from django.http import HttpResponse 4 5 from models import FULL_RESPONSE, LAST_MODIFIED, ETAG 6 7 @condition(lambda r: ETAG, lambda r: LAST_MODIFIED) 8 def index(request): 9 return HttpResponse(FULL_RESPONSE) 10 11 @condition(last_modified=lambda r: LAST_MODIFIED) 12 def last_modified(request): 13 return HttpResponse(FULL_RESPONSE) 14 15 @condition(etag=lambda r: ETAG) 16 def etag(request): 17 return HttpResponse(FULL_RESPONSE) 18 No newline at end of file -
tests/urls.py
=== modified file 'tests/urls.py'
29 29 30 30 # test urlconf for syndication tests 31 31 (r'^syndication/', include('regressiontests.syndication.urls')), 32 33 # conditional get views 34 (r'condition/', include('regressiontests.conditional_get.urls')), 32 35 )