Ticket #5791: 5791.diff

File 5791.diff, 9.6 KB (added by Maniac@…, 17 years ago)

Patch

  • django/views/decorators/http.py

     
    22Decorators for views based on HTTP headers.
    33"""
    44
     5from calendar import timegm
     6from datetime import timedelta
     7from email.Utils import formatdate
    58from django.utils.decorators import decorator_from_middleware
    69from django.middleware.http import ConditionalGetMiddleware
    7 from django.http import HttpResponseNotAllowed
     10from django.http import HttpResponseNotAllowed, HttpResponseNotModified
    811
    912conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
    1013
     
    3134require_GET.__doc__ = "Decorator to require that a view only accept the GET method."
    3235
    3336require_POST = require_http_methods(["POST"])
    34 require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
    35  No newline at end of file
     37require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
     38
     39
     40def _none(*args, **kwargs):
     41    return None
     42   
     43def condition(last_modified=_none, etag=_none):
     44    """
     45    Decorator to support conditional get for a view.  It takes as parameters
     46    user-defined functions that calculate etag and/or last modified time.
     47   
     48    Both functions are passed the same parameters as the view itself. "last_modified@
     49    should return a standard datetime value and "etag" should return a string.
     50   
     51    Usage with last_modified::
     52
     53        @condition(lambda r, obj_id: MyObject.objects.get(pk=obj_id).update_time)
     54        def my_object_view(request, obj_id):
     55            # ...
     56
     57    You can pass last_modified, etag or both of them (if you really need it).
     58    """
     59    def decorator(func):
     60        def inner(request, *args, **kwargs):
     61           
     62            # _call_* functions are used to format and memoize output from user supplied callables
     63            def _call_last_modified():
     64                result = []
     65                def caller():
     66                    if not result:
     67                        dt = last_modified(request, *args, **kwargs)
     68                        result.append(dt and (formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT'))
     69                    return result[0]
     70                return caller
     71            _last_modified = _call_last_modified()
     72           
     73            def _call_etag():
     74                result = []
     75                def caller():
     76                    if not result:
     77                        result.append(etag(request, *args, **kwargs))
     78                    return result[0]
     79                return caller
     80            _etag = _call_etag()
     81           
     82            if request.method not in ('GET', 'HEAD'):
     83                return func(request, *args, **kwargs)
     84            if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None)
     85            if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None)
     86            if if_none_match:
     87                if_none_match = [e.strip() for e in if_none_match.split(',')]
     88            not_modified = (if_modified_since or if_none_match) and \
     89                           (not if_modified_since or _last_modified() == if_modified_since) and \
     90                           (not if_none_match or _etag() in if_none_match)
     91            if not_modified:
     92                return HttpResponseNotModified()
     93            response = func(request, *args, **kwargs)
     94            if _last_modified() and not response.has_header('Last-Modified'):
     95                response['Last-Modified'] = _last_modified()
     96            if _etag() and not response.has_header('ETag'):
     97                response['ETag'] = _etag()
     98            return response
     99        return inner
     100    return decorator
     101 No newline at end of file
  • tests/regressiontests/conditional_get/views.py

     
     1# -*- coding:utf-8 -*-
     2from django.views.decorators.http import condition
     3from django.http import HttpResponse
     4
     5from models import FULL_RESPONSE, LAST_MODIFIED, ETAG
     6
     7@condition(lambda r: LAST_MODIFIED, lambda r: ETAG)
     8def index(request):
     9    return HttpResponse(FULL_RESPONSE)
     10
     11@condition(last_modified=lambda r: LAST_MODIFIED)
     12def last_modified(request):
     13    return HttpResponse(FULL_RESPONSE)
     14
     15@condition(etag=lambda r: ETAG)
     16def etag(request):
     17    return HttpResponse(FULL_RESPONSE)
     18 No newline at end of file
  • tests/regressiontests/conditional_get/models.py

     
     1# -*- coding:utf-8 -*-
     2from datetime import datetime, timedelta
     3from calendar import timegm
     4from django.test import TestCase
     5
     6FULL_RESPONSE = 'Test conditional get response'
     7LAST_MODIFIED = datetime(2007, 10, 21, 23, 21, 47)
     8LAST_MODIFIED_STR = 'Sun, 21 Oct 2007 23:21:47 GMT'
     9EXPIRED_LAST_MODIFIED_STR = 'Sat, 20 Oct 2007 23:21:47 GMT'
     10ETAG = '"b4246ffc4f62314ca13147c9d4f76974"'
     11EXPIRED_ETAG = '"7fae4cd4b0f81e7d2914700043aa8ed6"'
     12
     13class 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

     
     1from django.conf.urls.defaults import *
     2import views
     3
     4urlpatterns = patterns('',
     5    ('^$', views.index),
     6    ('^last_modified/$', views.last_modified),
     7    ('^etag/$', views.etag),
     8)
  • tests/urls.py

     
    1414   
    1515    # django built-in views
    1616    (r'^views/', include('regressiontests.views.urls')),
     17   
     18    # conditional get views
     19    (r'condition/', include('regressiontests.conditional_get.urls')),
    1720)
Back to Top