Skip to content

Commit

Permalink
SteamField block extra interfaces (part 1) (#365)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgax authored Sep 8, 2023
1 parent 683418a commit a56315c
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 21 deletions.
15 changes: 15 additions & 0 deletions docs/general-usage/decorators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,18 @@ To extend the schema with custom StreamField block types, the ``register_streamf
graphql_description = "This is a streamblock with a textblock child"
If a block's ``Meta`` class has a ``graphql_description`` attribute, this value will be exposed as the ``description`` in introspection queries.

To register additional interfaces for the block, add them with the ``interfaces`` argument:

.. code-block:: python
import graphene
class CustomInterface(graphene.Interface):
text = graphene.String()
@register_streamfield_block(interfaces=(CustomInterface,))
class CustomInterfaceBlock(blocks.StructBlock):
text = blocks.TextBlock()
11 changes: 7 additions & 4 deletions grapple/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from collections.abc import Iterable
from types import MethodType
from typing import Any, Dict, Type, Union
from typing import Any, Dict, Tuple, Type, Union

import graphene

Expand Down Expand Up @@ -80,7 +80,7 @@ def import_apps():
node_type = build_streamfield_type(
cls,
streamfield_type["type_prefix"],
streamfield_type["interface"],
streamfield_type["interfaces"],
base_type,
)

Expand Down Expand Up @@ -448,22 +448,25 @@ def custom_cls_resolver(*, cls, graphql_field):
def build_streamfield_type(
cls: type,
type_prefix: str,
interface: graphene.Interface,
interfaces: Tuple[graphene.Interface],
base_type=graphene.ObjectType,
):
"""
Build a graphql type for a StreamBlock or StructBlock class
If it has custom fields then implement them.
"""

# Alias the argument name so we can use it in the class block
interfaces_ = interfaces

# Create a new blank node type
class Meta:
if hasattr(cls, "graphql_types"):
types = [
registry.streamfield_blocks.get(block) for block in cls.graphql_types
]
else:
interfaces = (interface,) if interface is not None else ()
interfaces = interfaces_ if interfaces_ is not None else ()
# Add description to type if the Meta class declares it
description = getattr(cls._meta_class, "graphql_description", None)

Expand Down
38 changes: 22 additions & 16 deletions grapple/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,28 @@
field_middlewares = {}


def register_streamfield_block(cls):
base_block = None
for block_class in inspect.getmro(cls):
if block_class in registry.streamfield_blocks:
base_block = registry.streamfield_blocks[block_class]

streamfield_types.append(
{
"cls": cls,
"type_prefix": "",
"interface": StreamFieldInterface,
"base_type": base_block,
}
)

return cls
def register_streamfield_block(klass=None, interfaces=()):
def decorator(cls):
base_block = None
for block_class in inspect.getmro(cls):
if block_class in registry.streamfield_blocks:
base_block = registry.streamfield_blocks[block_class]

streamfield_types.append(
{
"cls": cls,
"type_prefix": "",
"interfaces": tuple({StreamFieldInterface, *interfaces}),
"base_type": base_block,
}
)

return cls

if isinstance(klass, type) and not interfaces:
return decorator(klass)

return decorator


def register_graphql_schema(schema_cls):
Expand Down
2 changes: 1 addition & 1 deletion tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Test requirements.
black>=23.7.0
ruff=0.0.285
ruff==0.0.285

# Runtime requirements
Django>=3.2,<5.0
Expand Down
29 changes: 29 additions & 0 deletions tests/test_blog.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from testapp.factories import (
AdvertFactory,
BlogPageFactory,
CustomInterfaceBlockFactory,
PersonFactory,
TextWithCallableBlockFactory,
)
Expand Down Expand Up @@ -128,6 +129,7 @@ def setUp(self):
},
),
("text_with_callable", TextWithCallableBlockFactory()),
("custom_interface_block", CustomInterfaceBlockFactory()),
],
parent=self.home,
summary=self.richtext_sample,
Expand Down Expand Up @@ -1202,3 +1204,30 @@ def test_struct_block_no_description(self):
"""
response = self.client.execute(query)
self.assertIsNone(response["data"]["__type"]["description"])

def test_custom_interface_block(self):
query = """
query($id: ID) {
page(id: $id) {
... on BlogPage {
body {
blockType
...on CustomInterface {
customText
}
}
}
}
}
"""
executed = self.client.execute(query, variables={"id": self.blog_page.id})
body = executed["data"]["page"]["body"]

for block in body:
if block["blockType"] == "CustomInterfaceBlock":
self.assertRegex(
block["customText"], r"^Block with custom interface \d+$"
)
return

self.fail("Query by interface didn't match anything")
18 changes: 18 additions & 0 deletions tests/testapp/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,23 @@ def get_link_url(
return ""


class CustomInterface(graphene.Interface):
custom_text = graphene.String()


@register_streamfield_block(interfaces=(CustomInterface,))
class CustomInterfaceBlock(blocks.StructBlock):
"""
Specify a custom GraphQL interface for our block.
"""

custom_text = blocks.TextBlock()

graphql_fields = [
GraphQLString("custom_text"),
]


class StreamFieldBlock(blocks.StreamBlock):
heading = blocks.CharBlock(classname="full title")
paragraph = blocks.RichTextBlock()
Expand All @@ -309,3 +326,4 @@ class StreamFieldBlock(blocks.StreamBlock):
block_with_name = BlockWithName()
advert = SnippetChooserBlock("testapp.Advert")
person = SnippetChooserBlock("testapp.Person")
custom_interface_block = CustomInterfaceBlock()
8 changes: 8 additions & 0 deletions tests/testapp/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from wagtail import blocks

from testapp.blocks import (
CustomInterfaceBlock,
ImageGalleryBlock,
ImageGalleryImage,
ImageGalleryImages,
Expand Down Expand Up @@ -85,6 +86,13 @@ class Meta:
model = TextWithCallableBlock


class CustomInterfaceBlockFactory(wagtail_factories.StructBlockFactory):
custom_text = factory.Sequence(lambda n: f"Block with custom interface {n}")

class Meta:
model = CustomInterfaceBlock


class BlogPageRelatedLinkFactory(factory.django.DjangoModelFactory):
class Meta:
model = BlogPageRelatedLink
Expand Down

0 comments on commit a56315c

Please sign in to comment.