Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added django.utils.cache, from Hugo's #580 patch. Refs #580.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@808 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 8aa98af6bbebae4a6df2953716f07c6601f44259 1 parent 71564b4
@adrianholovaty adrianholovaty authored
Showing with 155 additions and 0 deletions.
  1. +155 −0 django/utils/cache.py
View
155 django/utils/cache.py
@@ -0,0 +1,155 @@
+"""
+This module contains helper functions and decorators for controlling caching.
+It does so by managing the "Vary" header of responses. It includes functions
+to patch the header of response objects directly and decorators that change
+functions to do that header-patching themselves.
+
+For information on the Vary header, see:
+
+ http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
+
+Essentially, the "Vary" HTTP header defines which headers a cache should take
+into account when building its cache key. Requests with the same path but
+different header content for headers named in "Vary" need to get different
+cache keys to prevent delivery of wrong content.
+
+A example: i18n middleware would need to distinguish caches by the
+"Accept-language" header.
+"""
+
+import datetime, md5, re
+from django.conf import settings
+from django.core.cache import cache
+
+vary_delim_re = re.compile(r',\s*')
+
+def patch_response_headers(response, cache_timeout=None):
+ """
+ Adds some useful headers to the given HttpResponse object:
+ ETag, Last-Modified, Expires and Cache-Control
+
+ Each header is only added if it isn't already set.
+
+ cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
+ by default.
+ """
+ if cache_timeout is None:
+ cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
+ now = datetime.datetime.utcnow()
+ expires = now + datetime.timedelta(0, cache_timeout)
+ if not response.has_header('ETag'):
+ response['ETag'] = md5.new(response.content).hexdigest()
+ if not response.has_header('Last-Modified'):
+ response['Last-Modified'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT')
+ if not response.has_header('Expires'):
+ response['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
+ if not response.has_header('Cache-Control'):
+ response['Cache-Control'] = 'max-age=%d' % cache_timeout
+
+def patch_vary_headers(response, newheaders):
+ """
+ Adds (or updates) the "Vary" header in the given HttpResponse object.
+ newheaders is a list of header names that should be in "Vary". Existing
+ headers in "Vary" aren't removed.
+ """
+ # Note that we need to keep the original order intact, because cache
+ # implementations may rely on the order of the Vary contents in, say,
+ # computing an MD5 hash.
+ vary = []
+ if response.has_header('Vary'):
+ vary = vary_delim_re.split(response['Vary'])
+ oldheaders = dict([(el.lower(), 1) for el in vary])
+ for newheader in newheaders:
+ if not newheader.lower() in oldheaders:
+ vary.append(newheader)
+ response['Vary'] = ', '.join(vary)
+
+def vary_on_headers(*headers):
+ """
+ A view decorator that adds the specified headers to the Vary header of the
+ response. Usage:
+
+ @vary_on_headers('Cookie', 'Accept-language')
+ def index(request):
+ ...
+
+ Note that the header names are not case-sensitive.
+ """
+ def decorator(func):
+ def inner_func(*args, **kwargs):
+ response = func(*args, **kwargs)
+ patch_vary_headers(response, headers)
+ return response
+ return inner_func
+ return decorator
+
+def vary_on_cookie(func):
+ """
+ A view decorator that adds "Cookie" to the Vary header of a response. This
+ indicates that a page's contents depends on cookies. Usage:
+
+ @vary_on_cookie
+ def index(request):
+ ...
+ """
+ def inner_func(*args, **kwargs):
+ response = func(*args, **kwargs)
+ patch_vary_headers(response, ('Cookie',))
+ return response
+ return inner_func
+
+def _generate_cache_key(request, headerlist, key_prefix):
+ "Returns a cache key from the headers given in the header list."
+ ctx = md5.new()
+ for header in headerlist:
+ value = request.META.get(header, None)
+ if value is not None:
+ ctx.update(value)
+ return 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, ctx.hexdigest())
+
+def get_cache_key(request, key_prefix=None):
+ """
+ Returns a cache key based on the request path. It can be used in the
+ request phase because it pulls the list of headers to take into account
+ from the global path registry and uses those to build a cache key to check
+ against.
+
+ If there is no headerlist stored, the page needs to be rebuilt, so this
+ function returns None.
+ """
+ if key_prefix is None:
+ key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
+ cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path)
+ headerlist = cache.get(cache_key, None)
+ if headerlist is not None:
+ return _generate_cache_key(request, headerlist, key_prefix)
+ else:
+ return None
+
+def learn_cache_key(request, response, cache_timeout=None, key_prefix=None):
+ """
+ Learns what headers to take into account for some request path from the
+ response object. It stores those headers in a global path registry so that
+ later access to that path will know what headers to take into account
+ without building the response object itself. The headers are named in the
+ Vary header of the response, but we want to prevent response generation.
+
+ The list of headers to use for cache key generation is stored in the same
+ cache as the pages themselves. If the cache ages some data out of the
+ cache, this just means that we have to build the response once to get at
+ the Vary header and so at the list of headers to use for the cache key.
+ """
+ if key_prefix is None:
+ key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
+ if cache_timeout is None:
+ cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
+ cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path)
+ if response.has_header('Vary'):
+ headerlist = ['HTTP_'+header.upper().replace('-', '_') for header in vary_delim_re.split(response['Vary'])]
+ cache.set(cache_key, headerlist, cache_timeout)
+ return _generate_cache_key(request, headerlist, key_prefix)
+ else:
+ # if there is no Vary header, we still need a cache key
+ # for the request.path
+ cache.set(cache_key, [], cache_timeout)
+ return _generate_cache_key(request, [], key_prefix)

0 comments on commit 8aa98af

Please sign in to comment.
Something went wrong with that request. Please try again.