Open lanshark opened 3 years ago
Hey @lanshark, if they are custom StructBlocks then it is a good idea to register them, otherwise you have to go down a nested rabbit hole of blog type fragments. See example/home/blocks.py
We know documentation could use improvement -- any suggestions on how we can make it better are welcome. PRs even more so ;)
So, essentially, if it's gonna end up as a StreamField 'higher up', it should be decorated as such, and if it has component parts, those should be listed in graphql_fields for that component.
As for the PR, I may take a shot at it. I'm also looking into porting the channels implementation to a Django-3 version (and possibly also one to support Django-2)
I'm quite confused by @register_streamfield_block. In my project, I have a bunch of custom block types. For each block, I've implemented the block and a resolver and then registered them in my app... It's been a lot of boiler plate to write. Here is a simple and complex example from my current project...
blocks.py
class HeadingBlock(StructBlock):
size = ChoiceBlock(choices=HeadingSizes.choices, required=True)
text = CharBlock()
class DynamicPageListBlock(StructBlock):
"""
Custom `StructBlock` that allows the user to display a list of pages that are dynamically selected
from the site.
"""
parent_page = PageChooserBlock(required=True, page_type="MyApp.Page")
page_types = MultipleChoiceBlock(choices=PageType.choices, required=True)
sort_field = ChoiceBlock(choices=PageSortField.choices, required=True)
sort_direction = ChoiceBlock(choices=PageSortDirection.choices, required=True)
per_page = IntegerBlock(min_value=1, max_value=50, required=True, default=10)
pagination_enabled = BooleanBlock(required=False)
class Meta:
icon = "list-ul"
grapple.py
class HeadingBlock(graphene.ObjectType):
class Meta:
interfaces = (StreamFieldInterface,)
text = graphene.String(required=False)
size = graphene.String(required=False)
def resolve_text(self, info, **kwargs):
return self.value["text"]
def resolve_size(self, info, **kwargs):
return self.value["size"]
class DynamicPageListBlock(graphene.ObjectType):
from grapple.types.pages import PageInterface
pageCount = graphene.Int()
# first page of pages for SEO we render these with SSG, but additional paginated results
# will be pulled client side.
pages = graphene.List(PageInterface)
pageTypes = graphene.List(graphene.String)
paginationEnabled = graphene.Boolean()
# root page to use for selection, only children will be rendered.
parentPage = graphene.Field(PageInterface)
perPage = graphene.Int()
sortField = graphene.String()
sortDirection = graphene.String()
class Meta:
interfaces = (StreamFieldInterface,)
def resolve_children_only(self, info, **kwargs):
return self.value["children_only"]
def resolve_pageTypes(self, info, **kwargs):
return self.value["page_types"]
def resolve_paginationEnabled(self, info, **kwargs):
return self.value["pagination_enabled"]
def resolve_parentPage(self, info, **kwargs):
return self.value["parent_page"]
def resolve_perPage(self, info, **kwargs):
return self.value["per_page"]
def resolve_sortField(self, info, **kwargs):
return self.value["sort_field"]
def resolve_sortDirection(self, info, **kwargs):
return self.value["sort_direction"]
def resolve_pageCount(self, info, **kwargs):
query = _pagesQuery(self, info, **kwargs)
return query.count() / self.value["per_page"]
def resolve_pages(self, info, **kwargs):
query = _pagesQuery(self, info, **kwargs)
return query[: self.value["per_page"]]
app.py
class MyApp(AppConfig):
name = "MyApp"
def ready(self):
"""
Register GraphQL Resolvers for our custom Wagtail Blocks. `
"""
from . import blocks
from . import grapple
registry.streamfield_blocks[blocks.DynamicPageListBlock] = grapple.DynamicPageListBlock
registry.streamfield_blocks[blocks.HeadingBlock] = grapple.HeadingBlock
It seems like with @register_streamfield_block I could reduce this to blocks.py
@register_streamfield_block
class HeadingBlock(StructBlock):
size = ChoiceBlock(choices=HeadingSizes.choices, required=True)
text = CharBlock()
graphql_fields = [
GraphQLString("size"),
GraphQLString("text"),
]
@register_streamfield_block
class DynamicPageListBlock(StructBlock):
"""
Custom `StructBlock` that allows the user to display a list of pages that are dynamically selected
from the site.
"""
parent_page = PageChooserBlock(required=True, page_type="MyApp.Page")
page_types = MultipleChoiceBlock(choices=PageType.choices, required=True)
sort_field = ChoiceBlock(choices=PageSortField.choices, required=True)
sort_direction = ChoiceBlock(choices=PageSortDirection.choices, required=True)
per_page = IntegerBlock(min_value=1, max_value=50, required=True, default=10)
pagination_enabled = BooleanBlock(required=False)
class Meta:
icon = "list-ul"
graphql_fields = [
GraphQLInt("parent_page"),
GraphQLString("page_types"),
GraphQLString("sort_field"),
GraphQLString("sort_direction"),
GraphQLInt("per_page"),
GraphQLBoolean("pagination_enabled"),
# shouldn't DynamicPageListBlock.get_pages be inferred as the resolver method?
GraphQLField("pageCount", graphene.Int, source="get_pageCount")
GraphQLField("pages", graphene.List(PageInterface), source="get_pages")
]
get_pageCount(self):
query = _pagesQuery(self, info, **kwargs)
return query.count() / self.value["per_page"]
get_pages(self):
query = _pagesQuery(self, info, **kwargs)
return query[: self.value["per_page"]]
Is that the correct understanding of the decorator?
I dug into this some more... using @register_streamfield_block seems to add a blocks
property as well that will include all your graphql fields... It's serviceable as I can still access the individual fields directly...
I don't really understand the use case for the register method to add a 'blocks' graphql field. Maybe there is an implicit assumption here that blocks are declared in the order they should appear in the stream? It would be nice if whoever wrote this left a few inline comments about what is going on and why...
The real work of register_streamfield_block
is to add a class to the streamfield_types
array, which is then picked up in grapple.actions.import_apps where grapple.types.streamfield.generate_streamfield_union
is called first and finally grapple.actions.build_streamfield_type
is called to generate the resolver. I didn't really find where the blocks field is being added.
Actually, it seems like blocks
is coming from the struct block base class, grapple.types.streamfield.StructBlock
Please, include this on docs. I took several hours to find it out.
Hi guys - new to wagtail-grapple (great product, btw).
I am seeing references to register_streamfield_block in several places in the example, but no definition of exactly what it is, or when it should be used. Seems like it should be documented on the Decorators page.
IF I have nested StructBlocks inside StreamFields, and the top-level is just a StreamField which contains those blocks, do I need to use the decorator on the nested blocks?