From 2c434c350a933a5513ed4e499ce26c472de15961 Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 10:34:48 +0200
Subject: [PATCH 1/9] send cached response through UpdateCacheMiddleware to
apply the wagtail cache header when using cache_page()
---
wagtailcache/cache.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index 0d92c75..03cbe84 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -412,10 +412,9 @@ def _wrapped_view_func(
) -> HttpResponse:
# Try to fetch an already cached page from wagtail-cache.
response = FetchFromCacheMiddleware().process_request(request)
- if response:
- return response
- # Since we don't have a response at this point, process the request.
- response = view_func(request, *args, **kwargs)
+ if response is None:
+ # Since we don't have a response at this point, process the request.
+ response = view_func(request, *args, **kwargs)
# Cache the response.
response = UpdateCacheMiddleware().process_response(request, response)
return response
From be1e0dfd6717823cb8265b64bfe5df891c84e069 Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 10:54:12 +0200
Subject: [PATCH 2/9] separate max age setting
---
wagtailcache/cache.py | 8 +++++---
wagtailcache/settings.py | 1 +
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index 03cbe84..c238cf7 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -164,7 +164,7 @@ def _get_cache_key(r: WSGIRequest, c: BaseCache) -> str:
def _learn_cache_key(
- r: WSGIRequest, s: HttpResponse, t: int, c: BaseCache
+ r: WSGIRequest, s: HttpResponse, t: Optional[int], c: BaseCache
) -> str:
"""
Wrapper for Django's learn_cache_key which first strips specific
@@ -327,10 +327,12 @@ def process_response(
# ``Cache-Control`` header before reverting to using the cache's
# default.
timeout = get_max_age(response)
+ max_age = timeout
if timeout is None:
timeout = self._wagcache.default_timeout
- patch_response_headers(response, timeout)
- if timeout:
+ max_age = wagtailcache_settings.WAGTAIL_CACHE_MAX_AGE
+ patch_response_headers(response, max_age)
+ if timeout != 0:
try:
cache_key = _learn_cache_key(
request, response, timeout, self._wagcache
diff --git a/wagtailcache/settings.py b/wagtailcache/settings.py
index d56c9a6..380ddba 100644
--- a/wagtailcache/settings.py
+++ b/wagtailcache/settings.py
@@ -12,6 +12,7 @@ class _DefaultSettings:
WAGTAIL_CACHE_BACKEND = "default"
WAGTAIL_CACHE_HEADER = "X-Wagtail-Cache"
WAGTAIL_CACHE_IGNORE_COOKIES = True
+ WAGTAIL_CACHE_MAX_AGE = 5 * 60
WAGTAIL_CACHE_IGNORE_QS = [
r"^_bta_.*$", # Bronto
r"^_ga$", # Google Analytics
From e8bc29bdfbb349d0a455a5572212aa1e1983c3a7 Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 12:30:15 +0200
Subject: [PATCH 3/9] set Cache-Control to no-cache rather than
max-age= when WAGTAIL_CACHE_MAX_AGE is set to None
---
wagtailcache/cache.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index c238cf7..6bed573 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -17,7 +17,7 @@
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse
from django.template.response import SimpleTemplateResponse
-from django.utils.cache import cc_delim_re
+from django.utils.cache import cc_delim_re, patch_cache_control
from django.utils.cache import get_cache_key
from django.utils.cache import get_max_age
from django.utils.cache import has_vary_header
@@ -331,7 +331,11 @@ def process_response(
if timeout is None:
timeout = self._wagcache.default_timeout
max_age = wagtailcache_settings.WAGTAIL_CACHE_MAX_AGE
- patch_response_headers(response, max_age)
+ if max_age is None:
+ patch_cache_control(response, no_cache=True)
+ else:
+ patch_response_headers(response, max_age)
+
if timeout != 0:
try:
cache_key = _learn_cache_key(
From 5ef4a664b97df93b88d27ecbb71c374d62f301e7 Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 12:49:08 +0200
Subject: [PATCH 4/9] fix exception on cache settings page if the cache timeout
is set to None
---
wagtailcache/templates/wagtailcache/index.html | 6 +++++-
wagtailcache/templatetags/wagtailcache_tags.py | 13 +------------
wagtailcache/views.py | 3 +++
3 files changed, 9 insertions(+), 13 deletions(-)
diff --git a/wagtailcache/templates/wagtailcache/index.html b/wagtailcache/templates/wagtailcache/index.html
index 58aaca0..63f9d80 100644
--- a/wagtailcache/templates/wagtailcache/index.html
+++ b/wagtailcache/templates/wagtailcache/index.html
@@ -25,7 +25,11 @@ {% trans "Status" %}
{% trans "ENABLED" %}
- {% trans "Cached pages are automatically refreshed every" %} {% cache_timeout %}.
+ {% if cache_timeout == None %}
+ {% trans "Cached pages do not expire." %}
+ {% else %}
+ {% trans "Cached pages are automatically refreshed every" %} {{ cache_timeout|seconds_to_readable }}.
+ {% endif %}
{% trans "To modify this, change the TIMEOUT
value of the cache backend in the project settings." %}
diff --git a/wagtailcache/templatetags/wagtailcache_tags.py b/wagtailcache/templatetags/wagtailcache_tags.py
index c1e75b9..63f6674 100644
--- a/wagtailcache/templatetags/wagtailcache_tags.py
+++ b/wagtailcache/templatetags/wagtailcache_tags.py
@@ -6,10 +6,10 @@
from wagtailcache.settings import wagtailcache_settings
-
register = template.Library()
+@register.filter
def seconds_to_readable(seconds: int) -> str:
"""
Converts int seconds to a human readable string.
@@ -43,14 +43,3 @@ def get_wagtailcache_setting(value: str) -> Optional[object]:
Returns a wagtailcache Django setting, or default.
"""
return getattr(wagtailcache_settings, value, None)
-
-
-@register.simple_tag
-def cache_timeout() -> str:
- """
- Returns the wagtailcache timeout in human readable format.
- """
- timeout = caches[
- wagtailcache_settings.WAGTAIL_CACHE_BACKEND
- ].default_timeout
- return seconds_to_readable(timeout)
diff --git a/wagtailcache/views.py b/wagtailcache/views.py
index 30b5a70..d0e1fd9 100644
--- a/wagtailcache/views.py
+++ b/wagtailcache/views.py
@@ -26,6 +26,9 @@ def index(request):
"wagtailcache/index.html",
{
"keyring": keyring,
+ "cache_timeout": caches[
+ wagtailcache_settings.WAGTAIL_CACHE_BACKEND
+ ].default_timeout
},
)
From a083f71a91decfb5f2b73f1c6074052f71f74661 Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 12:52:19 +0200
Subject: [PATCH 5/9] fix formatting etc
---
wagtailcache/cache.py | 14 ++------------
wagtailcache/templatetags/wagtailcache_tags.py | 2 +-
wagtailcache/views.py | 2 +-
3 files changed, 4 insertions(+), 14 deletions(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index 6bed573..8e9fe8c 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -17,11 +17,12 @@
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse
from django.template.response import SimpleTemplateResponse
-from django.utils.cache import cc_delim_re, patch_cache_control
+from django.utils.cache import cc_delim_re
from django.utils.cache import get_cache_key
from django.utils.cache import get_max_age
from django.utils.cache import has_vary_header
from django.utils.cache import learn_cache_key
+from django.utils.cache import patch_cache_control
from django.utils.cache import patch_response_headers
from django.utils.deprecation import MiddlewareMixin
from wagtail import hooks
@@ -191,7 +192,6 @@ def __init__(self, get_response=None):
def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
if not wagtailcache_settings.WAGTAIL_CACHE:
return None
-
# Check if request is cacheable
# Only cache GET and HEAD requests.
# Don't cache requests that are previews.
@@ -203,7 +203,6 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
and not getattr(request, "is_preview", False)
and not (hasattr(request, "user") and request.user.is_authenticated)
)
-
# Allow the user to override our caching decision.
for fn in hooks.get_hooks("is_request_cacheable"):
result = fn(request, is_cacheable)
@@ -214,16 +213,13 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
setattr(request, "_wagtailcache_update", False)
setattr(request, "_wagtailcache_skip", True)
return None # Don't bother checking the cache.
-
# Try and get the cached response.
try:
cache_key = _get_cache_key(request, self._wagcache)
-
# No cache information available, need to rebuild.
if cache_key is None:
setattr(request, "_wagtailcache_update", True)
return None
-
# We have a key, get the cached response.
response = self._wagcache.get(cache_key)
@@ -233,12 +229,10 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
setattr(request, "_wagtailcache_error", True)
logger.exception("Could not fetch page from cache backend.")
return None
-
# No cache information available, need to rebuild.
if response is None:
setattr(request, "_wagtailcache_update", True)
return None
-
# Hit. Return cached response.
setattr(request, "_wagtailcache_update", False)
return response
@@ -288,7 +282,6 @@ def process_response(
_chop_response_vary(request, response)
# We don't need to update the cache, just return.
return response
-
# Check if the response is cacheable
# Don't cache private or no-cache responses.
# Do cache 200, 301, 302, 304, and 404 codes so that wagtail doesn't
@@ -308,19 +301,16 @@ def process_response(
and has_vary_header(response, "Cookie")
)
)
-
# Allow the user to override our caching decision.
for fn in hooks.get_hooks("is_response_cacheable"):
result = fn(response, is_cacheable)
if isinstance(result, bool):
is_cacheable = result
-
# If we are not allowed to cache the response, just return.
if not is_cacheable:
# Add response header to indicate this was intentionally not cached.
_patch_header(response, Status.SKIP)
return response
-
# Potentially remove the ``Vary: Cookie`` header.
_chop_response_vary(request, response)
# Try to get the timeout from the ``max-age`` section of the
diff --git a/wagtailcache/templatetags/wagtailcache_tags.py b/wagtailcache/templatetags/wagtailcache_tags.py
index 63f6674..48404d1 100644
--- a/wagtailcache/templatetags/wagtailcache_tags.py
+++ b/wagtailcache/templatetags/wagtailcache_tags.py
@@ -1,11 +1,11 @@
from typing import Optional
from django import template
-from django.core.cache import caches
from django.utils.translation import gettext_lazy as _
from wagtailcache.settings import wagtailcache_settings
+
register = template.Library()
diff --git a/wagtailcache/views.py b/wagtailcache/views.py
index d0e1fd9..f1b2af6 100644
--- a/wagtailcache/views.py
+++ b/wagtailcache/views.py
@@ -28,7 +28,7 @@ def index(request):
"keyring": keyring,
"cache_timeout": caches[
wagtailcache_settings.WAGTAIL_CACHE_BACKEND
- ].default_timeout
+ ].default_timeout,
},
)
From 31148f6101ab2d5a9d357fceed50dc8865c0690a Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 12:14:17 +0200
Subject: [PATCH 6/9] set etag on responses and return Not Modified when the
If-None-Match header matches the etag of the response in the cache
---
wagtailcache/cache.py | 23 +++++++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index 8e9fe8c..41014c4 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -17,7 +17,8 @@
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse
from django.template.response import SimpleTemplateResponse
-from django.utils.cache import cc_delim_re
+from django.utils.cache import cc_delim_re, set_response_etag, \
+ patch_cache_control
from django.utils.cache import get_cache_key
from django.utils.cache import get_max_age
from django.utils.cache import has_vary_header
@@ -25,6 +26,7 @@
from django.utils.cache import patch_cache_control
from django.utils.cache import patch_response_headers
from django.utils.deprecation import MiddlewareMixin
+from django.utils.http import parse_etags
from wagtail import hooks
from wagtailcache.settings import wagtailcache_settings
@@ -233,8 +235,17 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
if response is None:
setattr(request, "_wagtailcache_update", True)
return None
- # Hit. Return cached response.
+
+ # Hit. Return cached response or Not Modified.
setattr(request, "_wagtailcache_update", False)
+ response: HttpResponse
+ if "If-None-Match" in request.headers and "Etag" in response.headers and \
+ response.headers["Etag"] in parse_etags(
+ request.headers["If-None-Match"]):
+ not_modified = HttpResponse(status=304)
+ not_modified.headers["Etag"] = response.headers["Etag"]
+ # TODO: Cache-Control and Expires?
+ return not_modified
return response
@@ -325,6 +336,7 @@ def process_response(
patch_cache_control(response, no_cache=True)
else:
patch_response_headers(response, max_age)
+ patch_cache_control(response, must_revalidate=True)
if timeout != 0:
try:
@@ -344,13 +356,20 @@ def process_response(
uri_keys.append(cache_key)
keyring[uri] = uri_keys
self._wagcache.set("keyring", keyring)
+
+ def set_etag(r):
+ if "Etag" not in r.headers:
+ set_response_etag(r)
+
if isinstance(response, SimpleTemplateResponse):
def callback(r):
+ set_etag(response)
self._wagcache.set(cache_key, r, timeout)
response.add_post_render_callback(callback)
else:
+ set_etag(response)
self._wagcache.set(cache_key, response, timeout)
# Add a response header to indicate this was a cache miss.
_patch_header(response, Status.MISS)
From db965133722d3355e7eead2375b8a3ece7053cef Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 12:32:24 +0200
Subject: [PATCH 7/9] fix formatting
---
wagtailcache/cache.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index 41014c4..cf98799 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -241,7 +241,7 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
response: HttpResponse
if "If-None-Match" in request.headers and "Etag" in response.headers and \
response.headers["Etag"] in parse_etags(
- request.headers["If-None-Match"]):
+ request.headers["If-None-Match"]):
not_modified = HttpResponse(status=304)
not_modified.headers["Etag"] = response.headers["Etag"]
# TODO: Cache-Control and Expires?
From 63d1c0b369bba423ca27a9ce4b41b38f07dbe342 Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Tue, 16 Jul 2024 12:56:19 +0200
Subject: [PATCH 8/9] fix formatting
---
wagtailcache/cache.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index cf98799..44664e3 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -17,14 +17,14 @@
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse
from django.template.response import SimpleTemplateResponse
-from django.utils.cache import cc_delim_re, set_response_etag, \
- patch_cache_control
+from django.utils.cache import cc_delim_re
from django.utils.cache import get_cache_key
from django.utils.cache import get_max_age
from django.utils.cache import has_vary_header
from django.utils.cache import learn_cache_key
from django.utils.cache import patch_cache_control
from django.utils.cache import patch_response_headers
+from django.utils.cache import set_response_etag
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import parse_etags
from wagtail import hooks
@@ -223,7 +223,7 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
setattr(request, "_wagtailcache_update", True)
return None
# We have a key, get the cached response.
- response = self._wagcache.get(cache_key)
+ response: HttpResponse = self._wagcache.get(cache_key)
except Exception:
# If the cache backend is currently unresponsive or errors out,
@@ -235,13 +235,14 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
if response is None:
setattr(request, "_wagtailcache_update", True)
return None
-
# Hit. Return cached response or Not Modified.
setattr(request, "_wagtailcache_update", False)
- response: HttpResponse
- if "If-None-Match" in request.headers and "Etag" in response.headers and \
- response.headers["Etag"] in parse_etags(
- request.headers["If-None-Match"]):
+ if (
+ "If-None-Match" in request.headers
+ and "Etag" in response.headers
+ and response.headers["Etag"]
+ in parse_etags(request.headers["If-None-Match"])
+ ):
not_modified = HttpResponse(status=304)
not_modified.headers["Etag"] = response.headers["Etag"]
# TODO: Cache-Control and Expires?
From 5b781824a610e5e2d4c01907e8c3b0b9d48087c7 Mon Sep 17 00:00:00 2001
From: Gwendolyn
Date: Fri, 19 Jul 2024 11:39:00 +0200
Subject: [PATCH 9/9] set cache-control and expires header on 304 response
---
wagtailcache/cache.py | 30 ++++++++++++++++--------------
wagtailcache/settings.py | 1 +
2 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/wagtailcache/cache.py b/wagtailcache/cache.py
index 44664e3..0cb5f3c 100644
--- a/wagtailcache/cache.py
+++ b/wagtailcache/cache.py
@@ -31,7 +31,6 @@
from wagtailcache.settings import wagtailcache_settings
-
logger = logging.getLogger("wagtail-cache")
@@ -144,11 +143,11 @@ def _chop_response_vary(r: WSGIRequest, s: HttpResponse) -> HttpResponse:
and s.has_header("Vary")
and has_vary_header(s, "Cookie")
and not (
- settings.CSRF_COOKIE_NAME in s.cookies
- or settings.CSRF_COOKIE_NAME in r.COOKIES
- or settings.SESSION_COOKIE_NAME in s.cookies
- or settings.SESSION_COOKIE_NAME in r.COOKIES
- )
+ settings.CSRF_COOKIE_NAME in s.cookies
+ or settings.CSRF_COOKIE_NAME in r.COOKIES
+ or settings.SESSION_COOKIE_NAME in s.cookies
+ or settings.SESSION_COOKIE_NAME in r.COOKIES
+ )
):
_delete_vary_cookie(s)
return s
@@ -238,14 +237,16 @@ def process_request(self, request: WSGIRequest) -> Optional[HttpResponse]:
# Hit. Return cached response or Not Modified.
setattr(request, "_wagtailcache_update", False)
if (
- "If-None-Match" in request.headers
+ wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS
+ and "If-None-Match" in request.headers
and "Etag" in response.headers
and response.headers["Etag"]
in parse_etags(request.headers["If-None-Match"])
):
not_modified = HttpResponse(status=304)
not_modified.headers["Etag"] = response.headers["Etag"]
- # TODO: Cache-Control and Expires?
+ not_modified.headers["Cache-Control"] = response.headers["Cache-Control"]
+ not_modified.headers["Expires"] = response.headers["Expires"]
return not_modified
return response
@@ -308,10 +309,10 @@ def process_response(
and response.status_code in (200, 301, 302, 304, 404)
and not response.streaming
and not (
- not request.COOKIES
- and response.cookies
- and has_vary_header(response, "Cookie")
- )
+ not request.COOKIES
+ and response.cookies
+ and has_vary_header(response, "Cookie")
+ )
)
# Allow the user to override our caching decision.
for fn in hooks.get_hooks("is_response_cacheable"):
@@ -337,7 +338,8 @@ def process_response(
patch_cache_control(response, no_cache=True)
else:
patch_response_headers(response, max_age)
- patch_cache_control(response, must_revalidate=True)
+ if wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS:
+ patch_cache_control(response, must_revalidate=True)
if timeout != 0:
try:
@@ -359,7 +361,7 @@ def process_response(
self._wagcache.set("keyring", keyring)
def set_etag(r):
- if "Etag" not in r.headers:
+ if wagtailcache_settings.WAGTAIL_CACHE_USE_ETAGS and "Etag" not in r.headers:
set_response_etag(r)
if isinstance(response, SimpleTemplateResponse):
diff --git a/wagtailcache/settings.py b/wagtailcache/settings.py
index 380ddba..fda096c 100644
--- a/wagtailcache/settings.py
+++ b/wagtailcache/settings.py
@@ -13,6 +13,7 @@ class _DefaultSettings:
WAGTAIL_CACHE_HEADER = "X-Wagtail-Cache"
WAGTAIL_CACHE_IGNORE_COOKIES = True
WAGTAIL_CACHE_MAX_AGE = 5 * 60
+ WAGTAIL_CACHE_USE_ETAGS = False
WAGTAIL_CACHE_IGNORE_QS = [
r"^_bta_.*$", # Bronto
r"^_ga$", # Google Analytics