dnscms: hide drafts from search results

This commit is contained in:
2026-05-26 00:01:37 +02:00
parent 1b5483602f
commit 09d1078dce
2 changed files with 124 additions and 0 deletions
+36
View File
@@ -1,6 +1,12 @@
from django.apps import apps as django_apps
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.html import format_html from django.utils.html import format_html
from grapple.registry import registry as grapple_registry
from wagtail import hooks from wagtail import hooks
from wagtail.documents import get_document_model
from wagtail.images import get_image_model
from wagtail.models import Page
from wagtail.search.backends import get_search_backend
@hooks.register("register_rich_text_features") @hooks.register("register_rich_text_features")
@@ -8,6 +14,36 @@ def enable_additional_rich_text_features(features):
features.default_features.extend(["h5", "h6", "blockquote"]) features.default_features.extend(["h5", "h6", "blockquote"])
@hooks.register("register_schema_query")
def filter_search_to_live_pages(query_mixins):
"""
Grapple's default `search` resolver hits every page regardless of publish
state, exposing drafts on the public API. Prepend a mixin so MRO picks our
`resolve_search`, which restricts Page subclasses to live + public.
"""
if not grapple_registry.class_models:
return
class SearchLivePublicMixin:
def resolve_search(self, info, **kwargs):
query = kwargs.get("query")
if not query:
return None
s = get_search_backend()
results = []
models = [get_document_model(), get_image_model()]
for app in grapple_registry.apps:
models += django_apps.all_models[app].values()
for model in models:
if issubclass(model, Page):
results += s.search(query, model.objects.live().public())
else:
results += s.search(query, model)
return results
query_mixins.insert(0, SearchLivePublicMixin)
@hooks.register("construct_page_action_menu") @hooks.register("construct_page_action_menu")
def make_publish_default_action(menu_items, request, context): def make_publish_default_action(menu_items, request, context):
for index, item in enumerate(menu_items): for index, item in enumerate(menu_items):
+88
View File
@@ -0,0 +1,88 @@
from wagtail.search.backends import get_search_backend
from tests.conftest import EventPageFactory, GenericPageFactory
SEARCH_QUERY = """
query Search($query: String) {
results: search(query: $query) {
__typename
... on PageInterface {
title
}
}
}
"""
def _index(page):
# Wagtail's post_save signal enqueues indexing via django-tasks, which isn't
# drained synchronously in tests. Call the backend directly so the page is
# findable through the live search code path.
get_search_backend().add(page)
def _titles_for(body, typename):
return [r["title"] for r in body["data"]["results"] if r["__typename"] == typename]
def test_search_returns_live_generic_page(home_page, graphql_post):
page = GenericPageFactory(
parent=home_page,
title="PublishedGenericSearchToken",
slug="published-generic-search",
)
_index(page)
response, body = graphql_post(SEARCH_QUERY, {"query": "PublishedGenericSearchToken"})
assert response.status_code == 200
assert "errors" not in body, body
assert "PublishedGenericSearchToken" in _titles_for(body, "GenericPage")
def test_search_excludes_draft_generic_page(home_page, graphql_post):
page = GenericPageFactory(
parent=home_page,
title="DraftGenericSearchToken",
slug="draft-generic-search",
live=False,
)
_index(page)
response, body = graphql_post(SEARCH_QUERY, {"query": "DraftGenericSearchToken"})
assert response.status_code == 200
assert "errors" not in body, body
assert "DraftGenericSearchToken" not in _titles_for(body, "GenericPage")
def test_search_returns_live_event_page(home_page, event_index, graphql_post):
page = EventPageFactory(
parent=event_index,
title="PublishedEventSearchToken",
slug="published-event-search",
)
_index(page)
response, body = graphql_post(SEARCH_QUERY, {"query": "PublishedEventSearchToken"})
assert response.status_code == 200
assert "errors" not in body, body
assert "PublishedEventSearchToken" in _titles_for(body, "EventPage")
def test_search_excludes_draft_event_page(home_page, event_index, graphql_post):
page = EventPageFactory(
parent=event_index,
title="DraftEventSearchToken",
slug="draft-event-search",
live=False,
)
_index(page)
response, body = graphql_post(SEARCH_QUERY, {"query": "DraftEventSearchToken"})
assert response.status_code == 200
assert "errors" not in body, body
assert "DraftEventSearchToken" not in _titles_for(body, "EventPage")