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