Skip to content

Commit

Permalink
Replace SnippetObjectType with SnippetInterface
Browse files Browse the repository at this point in the history
Fixes #386

API clients could use a field on snippet objects to determine the type
of snippet they are looking at. Therefore, we change the snippet type to
an interface, similar to the page interface, so it can expose a new
field called `snippetType`.
  • Loading branch information
mgax committed Sep 20, 2024
1 parent 3205be2 commit 29daccb
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 93 deletions.
27 changes: 0 additions & 27 deletions docs/general-usage/graphql-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^^

Expand Down
31 changes: 30 additions & 1 deletion docs/general-usage/interfaces.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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<page interface settings>` setting.
:ref:`PAGE_INTERFACE<page interface setting>` setting.

As mentioned above there is both a plural ``pages`` and singular ``page``
field on the root Query type that returns a ``PageInterface``.
Expand Down Expand Up @@ -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<snippet interface setting>` setting.


Adding your own interfaces
--------------------------

Expand Down
17 changes: 14 additions & 3 deletions docs/getting-started/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
******************
Expand All @@ -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``
3 changes: 2 additions & 1 deletion grapple/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions grapple/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions grapple/types/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
50 changes: 10 additions & 40 deletions grapple/types/snippets.py
Original file line number Diff line number Diff line change
@@ -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
28 changes: 12 additions & 16 deletions grapple/types/streamfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
}
)
1 change: 1 addition & 0 deletions tests/settings_custom_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@


GRAPPLE["PAGE_INTERFACE"] = "testapp.interfaces.CustomPageInterface" # noqa: F405
GRAPPLE["SNIPPET_INTERFACE"] = "testapp.interfaces.CustomSnippetInterface" # noqa: F405
27 changes: 26 additions & 1 deletion tests/test_grapple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
23 changes: 20 additions & 3 deletions tests/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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):
Expand All @@ -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"}],
)
9 changes: 8 additions & 1 deletion tests/testapp/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import graphene

from grapple.types.interfaces import PageInterface
from grapple.types.interfaces import PageInterface, SnippetInterface


class AdditionalInterface(graphene.Interface):
Expand All @@ -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)

0 comments on commit 29daccb

Please sign in to comment.