From 8aa98af6bbebae4a6df2953716f07c6601f44259 Mon Sep 17 00:00:00 2001
From: Adrian Holovaty <adrian@holovaty.com>
Date: Sat, 8 Oct 2005 23:19:21 +0000
Subject: [PATCH] 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
---
 django/utils/cache.py | 155 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 155 insertions(+)
 create mode 100644 django/utils/cache.py

diff --git a/django/utils/cache.py b/django/utils/cache.py
new file mode 100644
index 0000000000..26b60c4040
--- /dev/null
+++ b/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)