Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace SnippetObjectType with SnippetInterface #405

Merged
merged 9 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

### Changed

- `SnippetObjectType` is replaced with `SnippetInterface` ([405](https://github.com/torchbox/wagtail-grapple/pull/405)) @mgax

### Fixed

- `value` not being queryable on `EmbedBlock` ([#399](https://github.com/torchbox/wagtail-grapple/pull/399))@JakubMastalerz
Expand Down
6 changes: 3 additions & 3 deletions docs/general-usage/decorators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -433,12 +433,12 @@ To register additional interfaces for the block, add them with your block's ``gr
from grapple.helpers import register_streamfield_block


class CustomInterface(graphene.Interface):
class MyInterface(graphene.Interface):
text = graphene.String()


@register_streamfield_block
class CustomInterfaceBlock(blocks.StructBlock):
class MyInterfaceBlock(blocks.StructBlock):
text = blocks.TextBlock()

graphql_interfaces = (CustomInterface,)
graphql_interfaces = (MyInterface,)
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
48 changes: 39 additions & 9 deletions 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,37 @@ in the interface:



``SnippetInterface``
--------------------

``SnippetInterface`` is the default interface for all Wagtail snippet models. It is accessible through the
``snippets`` field on the root query type. It exposes the following fields:

::

snippetType: String!
contentType: String!

An example of querying all snippets:

::

query {
snippets {
snippetType
contentType
...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 All @@ -118,49 +149,48 @@ Given the following example interface:
.. code-block:: python

# interfaces.py
from .interfaces import CustomInterface


class CustomInterface(graphene.Interface):
class MyInterface(graphene.Interface):
custom_field = graphene.String()

you could add it to your Page model like so:

.. code-block:: python

from wagtail.models import Page
from .interfaces import MyInterface


class MyPage(Page):
# ...

graphql_interfaces = (CustomInterface,)
graphql_interfaces = (MyInterface,)

or any Django model:

.. code-block:: python

# models.py
from django.db import models
from .interfaces import MyInterface


class MyModel(models.Model):
# ...

graphql_interfaces = (CustomInterface,)

graphql_interfaces = (MyInterface,)

or a ``StreamField`` block:

.. code-block:: python

# blocks.py
from wagtail.core import blocks
from .interfaces import MyInterface


class MyStructBlock(blocks.StructBlock):
# ...

graphql_interfaces = (CustomInterface,)
graphql_interfaces = (MyInterface,)

The provided interfaces will be added to the base interfaces for the model.
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
22 changes: 22 additions & 0 deletions grapple/types/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,25 @@ 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)
zerolab marked this conversation as resolved.
Show resolved Hide resolved


class SnippetInterface(graphene.Interface):
snippet_type = graphene.String(required=True)
content_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__
zerolab marked this conversation as resolved.
Show resolved Hide resolved

def resolve_content_type(self, info, **kwargs):
self.content_type = ContentType.objects.get_for_model(self)
return (
f"{self.content_type.app_label}.{self.content_type.model_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):
zerolab marked this conversation as resolved.
Show resolved Hide resolved
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,
}
)
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
29 changes: 28 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,30 @@ 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
contentType
}
}
"""

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")
self.assertEqual(snippets_data[0]["contentType"], "testapp.Advert")
Loading
Loading