diff --git a/docs/general-usage/graphql-types.rst b/docs/general-usage/graphql-types.rst index ff441b6e..156397b0 100644 --- a/docs/general-usage/graphql-types.rst +++ b/docs/general-usage/graphql-types.rst @@ -102,33 +102,6 @@ The following fields are returned: fileHash: String - -SnippetObjectType -^^^^^^^^^^^^^^^^^ - -You won't see much of ``SnippetObjectType`` as it's only a Union type that -groups all your Snippet models together. You can query all the available snippets -under the ``snippets`` field under the root Query, The query is similar to -an interface but ``SnippetObjectType`` doesn't provide any fields itself. - -When snippets are attached to Pages you interact with your generated type itself -as opposed to an interface or base type. - -An example of querying all snippets: - -:: - - query { - snippets { - ...on Advert { - id - url - text - } - } - } - - SettingObjectType ^^^^^^^^^^^^^^^^^ diff --git a/docs/general-usage/interfaces.rst b/docs/general-usage/interfaces.rst index ea3d9d2d..55f61469 100644 --- a/docs/general-usage/interfaces.rst +++ b/docs/general-usage/interfaces.rst @@ -50,7 +50,7 @@ the name of the model: } You can change the default ``PageInterface`` to your own interface by changing the -:ref:`PAGE_INTERFACE` setting. +:ref:`PAGE_INTERFACE` setting. As mentioned above there is both a plural ``pages`` and singular ``page`` field on the root Query type that returns a ``PageInterface``. @@ -107,6 +107,35 @@ in the interface: +``SnippetInterface`` +-------------------- + +``SnippetInterface`` is the default interface for all Wagtail snippet models. It is accessible throught the +``snippets`` field on the root query type. It exposes the following fields: + +:: + + snipeptType: String! + +An example of querying all snippets: + +:: + + query { + snippets { + snippetType + ...on Advert { + id + url + text + } + } + } + +You can change the default ``SnippetInterface`` to your own interface by changing the +:ref:`SNIPPET_INTERFACE` setting. + + Adding your own interfaces -------------------------- diff --git a/docs/getting-started/settings.rst b/docs/getting-started/settings.rst index 23f37179..ac245a5a 100644 --- a/docs/getting-started/settings.rst +++ b/docs/getting-started/settings.rst @@ -141,10 +141,10 @@ Limit the maximum number of items that ``QuerySetList`` and ``PaginatedQuerySet` Default: ``100`` -.. _page interface settings: +Wagtail model interfaces +^^^^^^^^^^^^^^^^^^^^^^^^ -Wagtail Page interface -^^^^^^^^^^^^^^^^^^^^^^ +.. _page interface setting: ``PAGE_INTERFACE`` ****************** @@ -153,3 +153,14 @@ Used to construct the schema for Wagtail Page-derived models. It can be overridd page models. Default: ``grapple.types.interfaces.PageInterface`` + + +.. _snippet interface setting: + +``SNIPPET_INTERFACE`` +********************* + +Used to construct the schema for Wagtail snippet models. It can be overridden to provide a custom interface for all +snippet models. + +Default: ``grapple.types.interfaces.SnippetInterface`` diff --git a/grapple/actions.py b/grapple/actions.py index b3601b2a..3c8332c2 100644 --- a/grapple/actions.py +++ b/grapple/actions.py @@ -27,6 +27,7 @@ from .types.images import ImageObjectType, ImageRenditionObjectType from .types.pages import Page, get_page_interface from .types.rich_text import RichText as RichTextType +from .types.snippets import get_snippet_interface from .types.streamfield import generate_streamfield_union @@ -612,7 +613,7 @@ def register_snippet_model(cls: Type[models.Model], type_prefix: str): return # Create a GQL type that implements Snippet Interface - snippet_node_type = build_node_type(cls, type_prefix, None) + snippet_node_type = build_node_type(cls, type_prefix, get_snippet_interface()) if snippet_node_type: registry.snippets[cls] = snippet_node_type diff --git a/grapple/settings.py b/grapple/settings.py index 3c5d10ae..3a2154f6 100644 --- a/grapple/settings.py +++ b/grapple/settings.py @@ -29,6 +29,7 @@ "MAX_PAGE_SIZE": 100, "RICHTEXT_FORMAT": "html", "PAGE_INTERFACE": "grapple.types.interfaces.PageInterface", + "SNIPPET_INTERFACE": "grapple.types.interfaces.SnippetInterface", } # List of settings that have been deprecated diff --git a/grapple/types/interfaces.py b/grapple/types/interfaces.py index 84c41c14..eaf85daa 100644 --- a/grapple/types/interfaces.py +++ b/grapple/types/interfaces.py @@ -209,3 +209,18 @@ def resolve_raw_value(self, info, **kwargs): return self.value.source return self.value + + +def get_snippet_interface(): + return import_string(grapple_settings.SNIPPET_INTERFACE) + + +class SnippetInterface(graphene.Interface): + snippet_type = graphene.String(required=True) + + @classmethod + def resolve_type(cls, instance, info, **kwargs): + return registry.snippets[type(instance)] + + def resolve_snippet_type(self, info, **kwargs): + return self.__class__.__name__ diff --git a/grapple/types/snippets.py b/grapple/types/snippets.py index 8e496943..b948fb78 100644 --- a/grapple/types/snippets.py +++ b/grapple/types/snippets.py @@ -1,49 +1,19 @@ import graphene from ..registry import registry - - -class SnippetTypes: - # SnippetObjectType class can only be created if - # registry.snippets.types is non-empty, and should only be created - # once (graphene complains if we register multiple type classes - # with identical names) - _SnippetObjectType = None - - @classmethod - def get_object_type(cls): - if cls._SnippetObjectType is None and registry.snippets: - - class SnippetObjectType(graphene.Union): - class Meta: - types = registry.snippets.types - - cls._SnippetObjectType = SnippetObjectType - return cls._SnippetObjectType +from .interfaces import get_snippet_interface def SnippetsQuery(): - SnippetObjectType = SnippetTypes.get_object_type() - - if SnippetObjectType is not None: - - class Mixin: - snippets = graphene.List(graphene.NonNull(SnippetObjectType), required=True) - # Return all snippets. - - def resolve_snippets(self, info, **kwargs): - snippet_objects = [] - for snippet in registry.snippets: - for object in snippet._meta.model.objects.all(): - snippet_objects.append(object) - - return snippet_objects - - return Mixin + class Mixin: + snippets = graphene.List(graphene.NonNull(get_snippet_interface), required=True) - else: + def resolve_snippets(self, info, **kwargs): + snippet_objects = [] + for snippet in registry.snippets: + for object in snippet._meta.model.objects.all(): + snippet_objects.append(object) - class Mixin: - pass + return snippet_objects - return Mixin + return Mixin diff --git a/grapple/types/streamfield.py b/grapple/types/streamfield.py index 7c2aa844..4e98fa3f 100644 --- a/grapple/types/streamfield.py +++ b/grapple/types/streamfield.py @@ -353,8 +353,7 @@ def resolve_items(self, info, **kwargs): def register_streamfield_blocks(): from .documents import get_document_type from .images import get_image_type - from .interfaces import get_page_interface - from .snippets import SnippetTypes + from .interfaces import get_page_interface, get_snippet_interface class PageChooserBlock(graphene.ObjectType): page = graphene.Field(get_page_interface(), required=False) @@ -391,20 +390,17 @@ def resolve_image(self, info, **kwargs): } ) - SnippetObjectType = SnippetTypes.get_object_type() - if SnippetObjectType is not None: + class SnippetChooserBlock(graphene.ObjectType): + snippet = graphene.Field(get_snippet_interface(), required=False) - class SnippetChooserBlock(graphene.ObjectType): - snippet = graphene.Field(SnippetObjectType, required=False) - - class Meta: - interfaces = (StreamFieldInterface,) + class Meta: + interfaces = (StreamFieldInterface,) - def resolve_snippet(self, info, **kwargs): - return self.value + def resolve_snippet(self, info, **kwargs): + return self.value - registry.streamfield_blocks.update( - { - wagtail.snippets.blocks.SnippetChooserBlock: SnippetChooserBlock, - } - ) + registry.streamfield_blocks.update( + { + wagtail.snippets.blocks.SnippetChooserBlock: SnippetChooserBlock, + } + ) diff --git a/tests/settings_custom_interfaces.py b/tests/settings_custom_interfaces.py index 319403fb..31628ae1 100644 --- a/tests/settings_custom_interfaces.py +++ b/tests/settings_custom_interfaces.py @@ -2,3 +2,4 @@ GRAPPLE["PAGE_INTERFACE"] = "testapp.interfaces.CustomPageInterface" # noqa: F405 +GRAPPLE["SNIPPET_INTERFACE"] = "testapp.interfaces.CustomSnippetInterface" # noqa: F405 diff --git a/tests/test_grapple.py b/tests/test_grapple.py index 1c2445f9..20df8bc3 100644 --- a/tests/test_grapple.py +++ b/tests/test_grapple.py @@ -10,7 +10,7 @@ from django.db import connection from django.test import RequestFactory, TestCase, override_settings from graphene.test import Client -from testapp.factories import BlogPageFactory +from testapp.factories import AdvertFactory, BlogPageFactory from testapp.models import GlobalSocialMediaSettings, HomePage, SocialMediaSettings from wagtail.documents import get_document_model from wagtail.models import Page, Site @@ -1579,3 +1579,28 @@ def test_query_single_setting_without_site_filter_and_multiple_sites(self): "data": {"setting": None}, }, ) + + +class SnippetsTest(BaseGrappleTest): + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.advert = AdvertFactory() + + def test_snippets(self): + query = """ + { + snippets { + snippetType + } + } + """ + + executed = self.client.execute(query) + + self.assertEqual(type(executed["data"]), dict) + self.assertEqual(type(executed["data"]["snippets"]), list) + self.assertEqual(type(executed["data"]["snippets"][0]), dict) + + snippets_data = executed["data"]["snippets"] + self.assertEqual(snippets_data[0]["snippetType"], "Advert") diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py index 4e3dd23b..a1276010 100644 --- a/tests/test_interfaces.py +++ b/tests/test_interfaces.py @@ -5,9 +5,13 @@ from django.test import override_settings, tag from test_grapple import BaseGrappleTestWithIntrospection from testapp.factories import AdditionalInterfaceBlockFactory, BlogPageFactory -from testapp.interfaces import CustomPageInterface +from testapp.interfaces import CustomPageInterface, CustomSnippetInterface -from grapple.types.interfaces import PageInterface, get_page_interface +from grapple.types.interfaces import ( + PageInterface, + get_page_interface, + get_snippet_interface, +) @skipIf( @@ -41,6 +45,12 @@ def test_schema_with_default_page_interface(self): def test_get_page_interface_with_custom_page_interface(self): self.assertIs(get_page_interface(), CustomPageInterface) + @override_settings( + GRAPPLE={"SNIPPET_INTERFACE": "testapp.interfaces.CustomSnippetInterface"} + ) + def test_get_snippet_interface_with_custom_page_interface(self): + self.assertIs(get_snippet_interface(), CustomSnippetInterface) + def test_streamfield_block_with_additional_interface(self): query = """ query($id: ID) { @@ -86,7 +96,7 @@ def test_schema_for_snippet_with_graphql_interface(self): results = self.introspect_schema_by_type("Advert") self.assertListEqual( sorted(results["data"]["__type"]["interfaces"], key=lambda x: x["name"]), - [{"name": "AdditionalInterface"}], + [{"name": "AdditionalInterface"}, {"name": "SnippetInterface"}], ) def test_schema_for_django_model_with_graphql_interfaces(self): @@ -108,3 +118,10 @@ def test_schema_with_custom_page_interface(self): self.assertListEqual( results["data"]["__type"]["interfaces"], [{"name": "CustomPageInterface"}] ) + + def test_schema_with_custom_snippet_interface(self): + results = self.introspect_schema_by_type("Person") + self.assertListEqual( + results["data"]["__type"]["interfaces"], + [{"name": "CustomSnippetInterface"}], + ) diff --git a/tests/testapp/interfaces.py b/tests/testapp/interfaces.py index 08d45d0d..41baec05 100644 --- a/tests/testapp/interfaces.py +++ b/tests/testapp/interfaces.py @@ -1,6 +1,6 @@ import graphene -from grapple.types.interfaces import PageInterface +from grapple.types.interfaces import PageInterface, SnippetInterface class AdditionalInterface(graphene.Interface): @@ -9,3 +9,10 @@ class AdditionalInterface(graphene.Interface): class CustomPageInterface(PageInterface): custom_text = graphene.String() + + +class CustomSnippetInterface(SnippetInterface): + custom_text = graphene.String() + + def resolve_custom_text(self, info, **kwargs): + return str(self)