Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions django/middleware/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from django.core.cache import DEFAULT_CACHE_ALIAS, caches
from django.utils.cache import (
get_cache_key,
get_key_prefix,
get_max_age,
has_vary_header,
learn_cache_key,
Expand All @@ -72,7 +73,7 @@ def __init__(self, get_response):
super().__init__(get_response)
self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
self.page_timeout = None
self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
self.key_prefix = None
self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS

@property
Expand Down Expand Up @@ -118,8 +119,11 @@ def process_response(self, request, response):
return response
patch_response_headers(response, timeout)
if timeout and response.status_code == 200:
key_prefix = get_key_prefix(request, self.key_prefix)
if key_prefix is None:
return response
cache_key = learn_cache_key(
request, response, timeout, self.key_prefix, cache=self.cache
request, response, timeout, key_prefix, cache=self.cache
)
if hasattr(response, "render") and callable(response.render):
response.add_post_render_callback(
Expand All @@ -141,7 +145,7 @@ class FetchFromCacheMiddleware(MiddlewareMixin):

def __init__(self, get_response):
super().__init__(get_response)
self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
self.key_prefix = None
self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS

@property
Expand All @@ -157,17 +161,20 @@ def process_request(self, request):
request._cache_update_cache = False
return None # Don't bother checking the cache.

key_prefix = get_key_prefix(request, self.key_prefix)
if key_prefix is None:
request._cache_update_cache = False
return None

# try and get the cached GET response
cache_key = get_cache_key(request, self.key_prefix, "GET", cache=self.cache)
cache_key = get_cache_key(request, key_prefix, "GET", cache=self.cache)
if cache_key is None:
request._cache_update_cache = True
return None # No cache information available, need to rebuild.
response = self.cache.get(cache_key)
# if it wasn't found and we are looking for a HEAD, try looking just for that
if response is None and request.method == "HEAD":
cache_key = get_cache_key(
request, self.key_prefix, "HEAD", cache=self.cache
)
cache_key = get_cache_key(request, key_prefix, "HEAD", cache=self.cache)
response = self.cache.get(cache_key)

if response is None:
Expand Down
34 changes: 25 additions & 9 deletions django/utils/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from django.http import HttpResponse, HttpResponseNotModified
from django.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag
from django.utils.log import log_response
from django.utils.module_loading import import_string
from django.utils.regex_helper import _lazy_re_compile
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import get_language
Expand Down Expand Up @@ -384,18 +385,34 @@ def get_cache_key(request, key_prefix=None, method="GET", cache=None):
If there isn't a headerlist stored, return None, indicating that the page
needs to be rebuilt.
"""
if key_prefix is None:
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
cache_key = _generate_cache_header_key(key_prefix, request)
_key_prefix = get_key_prefix(request, key_prefix)

cache_key = _generate_cache_header_key(_key_prefix, request)
if cache is None:
cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
headerlist = cache.get(cache_key)
if headerlist is not None:
return _generate_cache_key(request, method, headerlist, key_prefix)
return _generate_cache_key(request, method, headerlist, _key_prefix)
else:
return None


def get_key_prefix(request, key_prefix):
"""
Returns the KEY_PREFIX to use for the cache. The key prefix can be defined by a
constant, a callable, or a dotted path to a function that returns the key prefix.
"""
_key_prefix = key_prefix
if _key_prefix is None:
_key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
if callable(_key_prefix):
return _key_prefix(request)
if isinstance(_key_prefix, str) and "." in _key_prefix:
key_prefix_callable = import_string(_key_prefix)
return key_prefix_callable(request)
return _key_prefix


def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cache=None):
"""
Learn what headers to take into account for some request URL from the
Expand All @@ -409,11 +426,10 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cach
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
_key_prefix = get_key_prefix(request, key_prefix)
if cache_timeout is None:
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
cache_key = _generate_cache_header_key(key_prefix, request)
cache_key = _generate_cache_header_key(_key_prefix, request)
if cache is None:
cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
if response.has_header("Vary"):
Expand All @@ -429,12 +445,12 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cach
headerlist.append("HTTP_" + header)
headerlist.sort()
cache.set(cache_key, headerlist, cache_timeout)
return _generate_cache_key(request, request.method, headerlist, key_prefix)
return _generate_cache_key(request, request.method, headerlist, _key_prefix)
else:
# if there is no Vary header, we still need a cache key
# for the request.build_absolute_uri()
cache.set(cache_key, [], cache_timeout)
return _generate_cache_key(request, request.method, [], key_prefix)
return _generate_cache_key(request, request.method, [], _key_prefix)


def _to_tuple(s):
Expand Down
37 changes: 1 addition & 36 deletions docs/intro/tutorial01.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,6 @@ work, see :ref:`troubleshooting-django-admin`.
``django`` (which will conflict with Django itself) or ``test`` (which
conflicts with a built-in Python package).

.. admonition:: Where should this code live?

If your background is in plain old PHP (with no use of modern frameworks),
you're probably used to putting code under the web server's document root
(in a place such as ``/var/www``). With Django, you don't do that. It's
not a good idea to put any of this Python code within your web server's
document root, because it risks the possibility that people may be able
to view your code over the web. That's not good for security.

Put your code in some directory **outside** of the document root, such as
:file:`/home/mycode`.

Let's look at what :djadmin:`startproject` created:

.. code-block:: text
Expand Down Expand Up @@ -163,30 +151,7 @@ Now that the server's running, visit http://127.0.0.1:8000/ with your web
browser. You'll see a "Congratulations!" page, with a rocket taking off.
It worked!

.. admonition:: Changing the port

By default, the :djadmin:`runserver` command starts the development server
on the internal IP at port 8000.

If you want to change the server's port, pass
it as a command-line argument. For instance, this command starts the server
on port 8080:

.. console::

$ python manage.py runserver 8080

If you want to change the server's IP, pass it along with the port. For
example, to listen on all available public IPs (which is useful if you are
running Vagrant or want to show off your work on other computers on the
network), use:

.. console::

$ python manage.py runserver 0.0.0.0:8000

Full docs for the development server can be found in the
:djadmin:`runserver` reference.
(To serve the site on a different port, see the :djadmin:`runserver` reference.)

.. admonition:: Automatic reloading of :djadmin:`runserver`

Expand Down
8 changes: 5 additions & 3 deletions docs/ref/settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,11 @@ The cache connection to use for the :ref:`cache middleware

Default: ``''`` (Empty string)

A string which will be prefixed to the cache keys generated by the :ref:`cache
middleware <the-per-site-cache>`. This prefix is combined with the
:setting:`KEY_PREFIX <CACHES-KEY_PREFIX>` setting; it does not replace it.
A string, a callable or a dotted path to a callable that returns a string - which
will be prefixed to the cache keys generated by the
:ref:`cache middleware <the-per-site-cache>`.
This prefix is combined with the :setting:`KEY_PREFIX <CACHES-KEY_PREFIX>` setting;
it does not replace it.

See :doc:`/topics/cache`.

Expand Down
11 changes: 10 additions & 1 deletion docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Minor features
* The default iteration count for the PBKDF2 password hasher is increased from
720,000 to 870,000.

* The default ``parallelism`` of the ``ScryptPasswordHasher`` is
* The default ``parallelism`` of the ``ScryptPasswordHasher`` is
increased from 1 to 5, to follow OWASP recommendations.

* :class:`~django.contrib.auth.forms.BaseUserCreationForm` and
Expand Down Expand Up @@ -194,6 +194,15 @@ Minor features
session engines now provide async API. The new asynchronous methods all have
``a`` prefixed names, e.g. ``aget()``, ``akeys()``, or ``acycle_key()``.

:mod:`django.contrib.syndication`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Cache
~~~~~

* :func:`~django.views.decorators.cache.cache_page` decorator now accepts a
callable for ``key_prefix``.

Database backends
~~~~~~~~~~~~~~~~~

Expand Down
30 changes: 30 additions & 0 deletions docs/topics/cache.txt
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,32 @@ setting for the middleware. It can be used like this::
@cache_page(60 * 15, key_prefix="site1")
def my_view(request): ...

The ``key_prefix`` may be either a string, a callable or a dotted path to a callable
which receives the request object and returns the key prefix. This allows you to
generate a key prefix dynamically, for example to enable per-user caching::

def per_user_cache_prefixer(request):
if request.user.is_authenticated:
return f"user_{request.user.id}"
else:
return "anon"


@cache_page(60 * 60, key_prefix=per_user_cache_prefixer)
def my_view(request): ...

The callable may also return ``None`` to skip caching::

def selective_cache_prefixer(request):
# skip caching if URL contains "nocache" query param
if "nocache" in request.GET:
return None
return "prefix1"


@cache_page(60 * 60 * 24, key_prefix=selective_cache_prefixer)
def my_view(request): ...

The ``key_prefix`` and ``cache`` arguments may be specified together. The
``key_prefix`` argument and the :setting:`KEY_PREFIX <CACHES-KEY_PREFIX>`
specified under :setting:`CACHES` will be concatenated.
Expand All @@ -713,6 +739,10 @@ Additionally, ``cache_page`` automatically sets ``Cache-Control`` and
``Expires`` headers in the response which affect :ref:`downstream caches
<downstream-caches>`.

.. versionchanged:: 5.1

Support for callable ``key_prefix`` was added.

Specifying per-view cache in the URLconf
----------------------------------------

Expand Down
5 changes: 5 additions & 0 deletions tests/cache/key_prefix_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DOTTED_PATH_KEY_PREFIX = "callable_prefix"


def cache_prefixer(request):
return DOTTED_PATH_KEY_PREFIX
Loading