98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
"""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}"
|
|
)
|