diff options
| author | Malcolm Tredinnick <malcolm.tredinnick@gmail.com> | 2009-03-22 07:58:29 +0000 |
|---|---|---|
| committer | Malcolm Tredinnick <malcolm.tredinnick@gmail.com> | 2009-03-22 07:58:29 +0000 |
| commit | b203db6ec850fee9ad8f2e2c8873be986325572b (patch) | |
| tree | 349736a64d97e83f5817d79e7e12ad84973cdc0a /django | |
| parent | 5ac154e06568b9815e85b32f144ab4ee10190a61 (diff) | |
Fixed #5791 -- Added early-bailout support for views (ETags and Last-modified).
This provides support for views that can have their ETag and/or Last-modified
values computed much more quickly than the view itself. Supports all HTTP
verbs (not just GET).
Documentation and tests need a little more fleshing out (I'm not happy with the
documentation at the moment, since it's a bit backwards), but the functionality
is correct.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@10114 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
| -rw-r--r-- | django/utils/http.py | 23 | ||||
| -rw-r--r-- | django/views/decorators/http.py | 100 |
2 files changed, 121 insertions, 2 deletions
diff --git a/django/utils/http.py b/django/utils/http.py index 7d2af95c47..f0b1af9c58 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -1,9 +1,12 @@ +import re import urllib from email.Utils import formatdate from django.utils.encoding import smart_str, force_unicode from django.utils.functional import allow_lazy +ETAG_MATCH = re.compile(r'(?:W/)?"((?:\\.|[^"])*)"') + def urlquote(url, safe='/'): """ A version of Python's urllib.quote() function that can operate on unicode @@ -94,3 +97,23 @@ def int_to_base36(i): i = i % j factor -= 1 return ''.join(base36) + +def parse_etags(etag_str): + """ + Parses a string with one or several etags passed in If-None-Match and + If-Match headers by the rules in RFC 2616. Returns a list of etags + without surrounding double quotes (") and unescaped from \<CHAR>. + """ + etags = ETAG_MATCH.findall(etag_str) + if not etags: + # etag_str has wrong format, treat it as an opaque string then + return [etag_str] + etags = [e.decode('string_escape') for e in etags] + return etags + +def quote_etag(etag): + """ + Wraps a string in double quotes escaping contents as necesary. + """ + return '"%s"' % etag.replace('\\', '\\\\').replace('"', '\\"') + diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index dd4f90ea9c..ec4695367b 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -7,9 +7,15 @@ try: except ImportError: from django.utils.functional import wraps # Python 2.3, 2.4 fallback. +from calendar import timegm +from datetime import timedelta +from email.Utils import formatdate + from django.utils.decorators import decorator_from_middleware +from django.utils.http import parse_etags, quote_etag from django.middleware.http import ConditionalGetMiddleware -from django.http import HttpResponseNotAllowed +from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse + conditional_page = decorator_from_middleware(ConditionalGetMiddleware) @@ -36,4 +42,94 @@ require_GET = require_http_methods(["GET"]) require_GET.__doc__ = "Decorator to require that a view only accept the GET method." require_POST = require_http_methods(["POST"]) -require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
\ No newline at end of file +require_POST.__doc__ = "Decorator to require that a view only accept the POST method." + +def condition(etag_func=None, last_modified_func=None): + """ + Decorator to support conditional retrieval (or change) for a view + function. + + The parameters are callables to compute the ETag and last modified time for + the requested resource, respectively. The callables are passed the same + parameters as the view itself. The Etag function should return a string (or + None if the resource doesn't exist), whilst the last_modified function + should return a datetime object (or None if the resource doesn't exist). + + If both parameters are provided, all the preconditions must be met before + the view is processed. + + This decorator will either pass control to the wrapped view function or + return an HTTP 304 response (unmodified) or 412 response (preconditions + failed), depending upon the request method. + + Any behavior marked as "undefined" in the HTTP spec (e.g. If-none-match + plus If-modified-since headers) will result in the view function being + called. + """ + def decorator(func): + def inner(request, *args, **kwargs): + # Get HTTP request headers + if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE") + if_none_match = request.META.get("HTTP_IF_NONE_MATCH") + if_match = request.META.get("HTTP_IF_MATCH") + if if_none_match or if_match: + # There can be more than one ETag in the request, so we + # consider the list of values. + etags = parse_etags(if_none_match) + + # 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 = formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT' + else: + res_last_modified = None + else: + res_last_modified = None + + response = None + if not ((if_match and (if_modified_since or if_none_match)) or + (if_match and if_none_match)): + # We only get here if no undefined combinations of headers are + # specified. + if ((if_none_match and (res_etag in etags or + "*" in etags and res_etag)) and + (not if_modified_since or + res_last_modified == if_modified_since)): + if request.method in ("GET", "HEAD"): + response = HttpResponseNotModified() + else: + response = HttpResponse(status=412) + elif if_match and ((not res_etag and "*" in etags) or + (res_etag and res_etag not in etags)): + response = HttpResponse(status=412) + elif (not if_none_match and if_modified_since and + request.method == "GET" and + res_last_modified == if_modified_since): + response = HttpResponseNotModified() + + if response is None: + response = func(request, *args, **kwargs) + + # Set relevant headers on the response if they don't already exist. + if res_last_modified and not response.has_header('Last-Modified'): + response['Last-Modified'] = res_last_modified + if res_etag and not response.has_header('ETag'): + response['ETag'] = quote_etag(res_etag) + + return response + + return inner + return decorator + +# Shortcut decorators for common cases based on ETag or Last-Modified only +def etag(callable): + return condition(etag=callable) + +def last_modified(callable): + return condition(last_modified=callable) + |
