"""Purge the frontend cache when content changes. The frontend caches all GraphQL responses under a single "cms" cache tag; any content change POSTs to its /api/revalidate endpoint for a global purge. """ import logging import threading import urllib.request from django.conf import settings from django.db import transaction from django.db.models.signals import post_delete, post_save logger = logging.getLogger(__name__) WEBHOOK_TIMEOUT_SECONDS = 5 # Non-page models exposed over GraphQL; pages are covered by wagtail signals REVALIDATING_MODELS = [ "contacts.ContactEntity", "events.EventCategory", "events.EventOrganizer", "images.CustomImage", "openinghours.OpeningHoursSet", ] _warned_unconfigured = False def _send_webhook(url, secret, reason): request = urllib.request.Request( url, data=b"", method="POST", headers={"X-Revalidate-Secret": secret} ) try: with urllib.request.urlopen(request, timeout=WEBHOOK_TIMEOUT_SECONDS) as response: logger.info("frontend revalidation ok (%s): HTTP %s", reason, response.status) except Exception: logger.exception("frontend revalidation failed (%s)", reason) def trigger_frontend_revalidation(reason): global _warned_unconfigured secret = settings.REVALIDATE_WEBHOOK_SECRET if not secret: if not _warned_unconfigured: logger.warning("REVALIDATE_WEBHOOK_SECRET is not set, skipping frontend revalidation") _warned_unconfigured = True return url = f"{settings.FRONTEND_BASE_URL}/api/revalidate" # After commit so the frontend can't re-render from pre-commit data, # in a thread so publishing never blocks on the frontend transaction.on_commit( lambda: threading.Thread( target=_send_webhook, args=(url, secret, reason), daemon=True ).start() ) def _on_page_published(sender, instance, **kwargs): trigger_frontend_revalidation(f"page_published: {instance.slug}") def _on_page_unpublished(sender, instance, **kwargs): trigger_frontend_revalidation(f"page_unpublished: {instance.slug}") def _on_page_moved(sender, instance, **kwargs): trigger_frontend_revalidation(f"post_page_move: {instance.slug}") def _on_page_deleted(sender, instance, **kwargs): trigger_frontend_revalidation(f"page_deleted: {instance.slug}") def _on_model_changed(sender, instance, **kwargs): trigger_frontend_revalidation(f"{sender._meta.label}: {instance}") def register_signal_handlers(): from wagtail.models import Page from wagtail.signals import page_published, page_unpublished, post_page_move page_published.connect(_on_page_published, dispatch_uid="revalidation.page_published") page_unpublished.connect(_on_page_unpublished, dispatch_uid="revalidation.page_unpublished") post_page_move.connect(_on_page_moved, dispatch_uid="revalidation.post_page_move") # Wagtail sends no signal on page deletion; MTI deletes of any page type # also emit post_delete for the base Page row post_delete.connect(_on_page_deleted, sender=Page, dispatch_uid="revalidation.page_deleted") for label in REVALIDATING_MODELS: post_save.connect( _on_model_changed, sender=label, dispatch_uid=f"revalidation.save.{label}" ) post_delete.connect( _on_model_changed, sender=label, dispatch_uid=f"revalidation.delete.{label}" )