torchbox / wagtail-grapple

A Wagtail app that makes building GraphQL endpoints a breeze!
https://wagtail-grapple.readthedocs.io/en/latest/
Other
152 stars 57 forks source link

register_streamfield_block not documented? #150

Open lanshark opened 3 years ago

lanshark commented 3 years ago

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?

zerolab commented 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 ;)

lanshark commented 3 years ago

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)

dopry commented 2 years ago

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?

dopry commented 2 years ago

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.

dopry commented 2 years ago

Actually, it seems like blocks is coming from the struct block base class, grapple.types.streamfield.StructBlock

lucasgueiros commented 1 year ago

Please, include this on docs. I took several hours to find it out.