Source code for Happyface4.middleware
"""
Custom middleware for HappyFace4.
ConditionalCacheMiddleware: Prevents cache updates during getData execution
to avoid caching incomplete data when database updates are in progress.
TimezoneMiddleware: Activates the browser's timezone per request (HFC #14)
and keys the page cache by it.
"""
from django.core.cache import cache
from django.utils import timezone
from django.utils.deprecation import MiddlewareMixin
import logging
import zoneinfo
logger = logging.getLogger("Happyface4")
TIMEZONE_COOKIE_NAME = "hf_timezone"
[docs]
class ConditionalCacheMiddleware(MiddlewareMixin):
"""
Prevents cache updates during getData execution.
When getData is running, database updates may be in progress (deleting old
records and inserting new ones). To prevent users from seeing and caching
incomplete data, this middleware:
1. Checks for 'getData_in_progress' flag in Redis before each request
2. If flag is set, disables cache updates for that request
3. User still gets served from existing cache (stale but complete data)
4. After getData completes, old cache is invalidated and fresh data cached
This ensures users always see complete datasets, never partial data during
database updates.
"""
[docs]
def process_request(self, request):
"""
Check if getData is currently running.
If so, mark this request to skip cache updates.
"""
try:
if cache.get("getData_in_progress"):
# Disable cache update for this request
request._cache_update_disabled = True
logger.debug(
f"getData in progress - cache updates disabled for {request.path}"
)
except Exception as e:
# Don't break requests if cache check fails
logger.warning(f"Failed to check getData_in_progress flag: {e}")
return None
[docs]
def process_response(self, request, response):
"""
If getData is running, add headers to prevent caching this response.
This works with UpdateCacheMiddleware to skip cache storage.
"""
if getattr(request, "_cache_update_disabled", False):
# Tell Django's UpdateCacheMiddleware not to cache this response
# Must include 'private' as that's what Django's cache middleware checks
response["Cache-Control"] = "private, no-cache, no-store, must-revalidate"
response["Pragma"] = "no-cache"
response["Expires"] = "0"
logger.debug(f"Prevented caching of {request.path} during getData")
return response
[docs]
class TimezoneMiddleware(MiddlewareMixin):
"""Activate the browser's timezone for the current request (HFC #14).
The timezone travels in the ``hf_timezone`` cookie, which a small script
in ``index.html`` sets from ``Intl.DateTimeFormat().resolvedOptions().timeZone``.
With ``USE_TZ = True`` template rendering of aware datetimes then happens
in the browser's timezone; the fallback (no/unknown cookie) is the server
default ``settings.TIME_ZONE``.
The page cache is keyed per timezone: the request is tagged with a
synthetic ``X-HF-Timezone`` header and the response carries
``Vary: X-HF-Timezone``, which the cache middlewares include in the cache
key. Requests without the cookie (e.g. the getData cache warmup) share
the default entry, so the warmup does not multiply.
"""
[docs]
def process_request(self, request):
tz_name = request.COOKIES.get(TIMEZONE_COOKIE_NAME, "")
request.META["HTTP_X_HF_TIMEZONE"] = tz_name
if tz_name:
try:
timezone.activate(zoneinfo.ZoneInfo(tz_name))
except Exception:
logger.warning(
f"Unknown timezone '{tz_name}' requested; falling back to the server default."
)
timezone.deactivate()
else:
timezone.deactivate()
return None
[docs]
def process_response(self, request, response):
vary = response.headers.get("Vary", "")
if "x-hf-timezone" not in vary.lower():
response.headers["Vary"] = (
f"{vary}, X-HF-Timezone" if vary else "X-HF-Timezone"
)
return response