mirror of
https://github.com/django/django.git
synced 2025-11-19 03:08:59 +00:00
Merge fc1c08f7b8 into 1ce6e78dd4
This commit is contained in:
commit
130c1b73d8
4 changed files with 215 additions and 1 deletions
|
|
@ -21,7 +21,8 @@ from hashlib import md5
|
|||
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from django.http import HttpResponse, HttpResponseNotModified
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseNotModified
|
||||
from django.test import RequestFactory
|
||||
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.regex_helper import _lazy_re_compile
|
||||
|
|
@ -443,3 +444,51 @@ def _to_tuple(s):
|
|||
if len(t) == 2:
|
||||
return t[0].lower(), t[1]
|
||||
return t[0].lower(), True
|
||||
|
||||
|
||||
def invalidate_view_cache(
|
||||
request, key_prefix=None, cache=None, invalidate_whole_prefix=False
|
||||
):
|
||||
"""
|
||||
Delete a cached page based on either a relative URL (type: any)
|
||||
or a request object (type: django.http.HttpRequest).
|
||||
|
||||
The cache key is reconstructed in two steps:
|
||||
1. A headers cache key is built using the absolute URL,
|
||||
key prefix, and locale code.
|
||||
2. A response cache key is then built using the absolute URL,
|
||||
key prefix, HTTP method, and recovered headers.
|
||||
|
||||
invalidate_whole_prefix: If True, the function will
|
||||
attempt to invalidate all cached entries that share the given key_prefix
|
||||
(i.e. all pages cached under that prefix). The exact scope and method of
|
||||
prefix invalidation depends on the cache backend capabilities.
|
||||
|
||||
The ``key_prefix`` must match the value used in the ``cache_page``
|
||||
decorator for the corresponding view.
|
||||
"""
|
||||
if not isinstance(request, HttpRequest):
|
||||
factory = RequestFactory()
|
||||
request = factory.get(request)
|
||||
|
||||
if cache is None:
|
||||
cache = caches[settings.CACHE_MIDDLEWARE_ALIAS]
|
||||
|
||||
if invalidate_whole_prefix:
|
||||
cache_class = f"{cache.__class__.__module__}.{cache.__class__.__name__}"
|
||||
if cache_class != "django_redis.cache.RedisCache":
|
||||
raise ValueError("invalidate_view_cache only supports RedisCache backend")
|
||||
try:
|
||||
# Get all keys matching the prefix
|
||||
keys = cache.iter_keys(f"*{key_prefix}*")
|
||||
# Get all `cache_header` keys to delete associated entries
|
||||
keys = cache.iter_keys(f"*{next(keys).split('.')[6]}*")
|
||||
except StopIteration:
|
||||
return 0
|
||||
return cache.delete_many(keys)
|
||||
|
||||
cache_key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
|
||||
if cache_key is None:
|
||||
return 0
|
||||
|
||||
return cache.delete(cache_key)
|
||||
|
|
|
|||
|
|
@ -82,3 +82,43 @@ def never_cache(view_func):
|
|||
return response
|
||||
|
||||
return wraps(view_func)(_view_wrapper)
|
||||
|
||||
|
||||
def cache_page_per_item(timeout, key_prefix=None):
|
||||
"""Decorator that applies cache_page with a dynamic key_prefix
|
||||
based on item id. The item id is expected to be passed as
|
||||
'pk' or 'id' keyword argument to the view. If key_prefix
|
||||
is not provided, the view name will be used as base for the key prefix.
|
||||
Example usage:
|
||||
@cache_page_per_item(60 * 15, key_prefix="prefix")
|
||||
def my_view(request, pk):
|
||||
...
|
||||
|
||||
This will cache the view for 15 minutes with a key prefix of 'prefix_<pk>'
|
||||
allowing cache keys to be unique per item and identifiable.
|
||||
If key_prefix is not provided it will default to 'my_view_<pk>' where
|
||||
'my_view' is the name of the view function.
|
||||
"""
|
||||
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
item_id = kwargs.get("pk") or kwargs.get("id")
|
||||
if key_prefix is None:
|
||||
rm = getattr(request, "resolver_match", None)
|
||||
if rm and rm.view_name:
|
||||
base = rm.view_name.replace(":", "_")
|
||||
else:
|
||||
base = getattr(view_func, "__name__", "view")
|
||||
else:
|
||||
base = key_prefix
|
||||
|
||||
prefix = f"{base}_{item_id}" if item_id is not None else base
|
||||
|
||||
return cache_page(timeout, key_prefix=prefix)(view_func)(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -103,6 +103,19 @@ need to distinguish caches by the ``Accept-language`` header.
|
|||
cache, this 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.
|
||||
|
||||
.. function:: invalidate_view_cache(request, key_prefix=None, cache=None):
|
||||
Delete a cached page based on either a relative URL (type: ``any``)
|
||||
or a request object (type: ``django.http.HttpRequest``).
|
||||
|
||||
The cache key is reconstructed in two steps:
|
||||
1. A headers cache key is built using the absolute URL,
|
||||
key prefix, and locale code.
|
||||
2. A response cache key is then built using the absolute URL,
|
||||
key prefix, HTTP method, and recovered headers.
|
||||
|
||||
The ``key_prefix`` must match the value used in the ``cache_page``
|
||||
decorator for the corresponding view.
|
||||
|
||||
``django.utils.dateparse``
|
||||
==========================
|
||||
|
||||
|
|
|
|||
112
tests/cache/tests.py
vendored
112
tests/cache/tests.py
vendored
|
|
@ -46,6 +46,7 @@ from django.template import engines
|
|||
from django.template.context_processors import csrf
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import (
|
||||
AsyncRequestFactory,
|
||||
RequestFactory,
|
||||
SimpleTestCase,
|
||||
TestCase,
|
||||
|
|
@ -57,11 +58,13 @@ from django.test.utils import CaptureQueriesContext
|
|||
from django.utils import timezone, translation
|
||||
from django.utils.cache import (
|
||||
get_cache_key,
|
||||
invalidate_view_cache,
|
||||
learn_cache_key,
|
||||
patch_cache_control,
|
||||
patch_vary_headers,
|
||||
)
|
||||
from django.views.decorators.cache import cache_control, cache_page
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from .models import Poll, expensive_calculation
|
||||
|
||||
|
|
@ -2534,6 +2537,7 @@ def csrf_view(request):
|
|||
)
|
||||
class CacheMiddlewareTest(SimpleTestCase):
|
||||
factory = RequestFactory()
|
||||
async_factory = AsyncRequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
self.default_cache = caches["default"]
|
||||
|
|
@ -2625,6 +2629,114 @@ class CacheMiddlewareTest(SimpleTestCase):
|
|||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result.content, b"Hello World 1")
|
||||
|
||||
def test_invalidate_view_decorator_cache_from_request(self):
|
||||
"""Invalidate cache key/value from request object"""
|
||||
view = cache_page(10)(hello_world_view)
|
||||
request = self.factory.get("/view/")
|
||||
_ = view(request, "0")
|
||||
cache_key = get_cache_key(request=request, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Verify request.content has been chached
|
||||
self.assertEqual(cached_response.content, b"Hello World 0")
|
||||
|
||||
# Delete cache key/value
|
||||
invalidate_view_cache(request, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Confirm key/value has been deleted from cache
|
||||
self.assertIsNone(cached_response)
|
||||
|
||||
def test_invalidate_view_decorator_cache_from_path(self):
|
||||
"""Invalidate cache key/value from path"""
|
||||
view = cache_page(10)(hello_world_view)
|
||||
path = "/view/"
|
||||
request = self.factory.get(path)
|
||||
_ = view(request, "0")
|
||||
cache_key = get_cache_key(request=request, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Verify request.content has been chached
|
||||
self.assertEqual(cached_response.content, b"Hello World 0")
|
||||
|
||||
# Delete cache key/value
|
||||
invalidate_view_cache(path, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Confirm key/value has been deleted from cache
|
||||
self.assertIsNone(cached_response)
|
||||
|
||||
def test_invalidate_view_decorator_cache_from_path_with_vary_headers(self):
|
||||
"""Invalidate cache key/value from path with Vary headers"""
|
||||
|
||||
# Cache view and inject Vary headers to Response object
|
||||
view = cache_page(10, key_prefix="")(
|
||||
vary_on_headers("Accept-Encoding", "Accept")(hello_world_view)
|
||||
)
|
||||
path = "/view/"
|
||||
request = self.factory.get(path)
|
||||
response = view(request, "0")
|
||||
|
||||
# Check response headers
|
||||
self.assertTrue(response.has_header("Vary"))
|
||||
|
||||
cache_key = get_cache_key(request=request, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Verify request.content has been chached
|
||||
self.assertEqual(cached_response.content, b"Hello World 0")
|
||||
|
||||
# Delete cache key/value
|
||||
invalidate_view_cache(path, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Confirm key/value has been deleted from cache
|
||||
self.assertIsNone(cached_response)
|
||||
|
||||
def test_cache_key_prefix_missmatch(self):
|
||||
# Wrap the view with the cache_page decorator (no key_prefix specified)
|
||||
view = cache_page(10)(hello_world_view)
|
||||
path = "/view/"
|
||||
request = self.factory.get(path)
|
||||
_ = view(request, "0")
|
||||
|
||||
# Attempt to retrieve the cache key without specifying key_prefix
|
||||
cache_key = get_cache_key(request=request, cache=cache)
|
||||
|
||||
# Because get_cache_key defaults to using
|
||||
# settings.CACHE_MIDDLEWARE_KEY_PREFIX when key_prefix is None,
|
||||
# this should not match the cached key
|
||||
self.assertIsNone(cache_key)
|
||||
|
||||
# Try again, explicitly passing the default cache_page key_prefix
|
||||
# (empty string)
|
||||
cache_key = get_cache_key(request=request, cache=cache, key_prefix="")
|
||||
|
||||
# The key should now be found
|
||||
self.assertIsNotNone(cache_key)
|
||||
|
||||
# Confirm that the cached response content matches the view output
|
||||
cached_response = cache.get(cache_key)
|
||||
self.assertEqual(cached_response.content, b"Hello World 0")
|
||||
|
||||
def test_invalidate_view_decorator_cache_from_async_request(self):
|
||||
"""Invalidate cache key/value from async request object"""
|
||||
view = cache_page(10)(hello_world_view)
|
||||
async_request = self.async_factory.get("/view/")
|
||||
_ = view(async_request, "0")
|
||||
cache_key = get_cache_key(request=async_request, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Verify request.content has been chached
|
||||
self.assertEqual(cached_response.content, b"Hello World 0")
|
||||
|
||||
# Delete cache key/value
|
||||
invalidate_view_cache(async_request, key_prefix="", cache=cache)
|
||||
cached_response = cache.get(cache_key)
|
||||
|
||||
# Confirm key/value has been deleted from cache
|
||||
self.assertIsNone(cached_response)
|
||||
|
||||
def test_view_decorator(self):
|
||||
# decorate the same view with different cache decorators
|
||||
default_view = cache_page(3)(hello_world_view)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue