summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorMalcolm Tredinnick <malcolm.tredinnick@gmail.com>2009-03-22 07:58:29 +0000
committerMalcolm Tredinnick <malcolm.tredinnick@gmail.com>2009-03-22 07:58:29 +0000
commitb203db6ec850fee9ad8f2e2c8873be986325572b (patch)
tree349736a64d97e83f5817d79e7e12ad84973cdc0a /django
parent5ac154e06568b9815e85b32f144ab4ee10190a61 (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.py23
-rw-r--r--django/views/decorators/http.py100
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)
+