question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How to let GraphQL read Streamfield

See original GitHub issue

Any ideas how can I solve this problem to let StreamField read on GraphQL?

Below are the code and error show. after i run localhost:8080/graphql

image

schema.py

import graphene
from graphene_django import DjangoObjectType
from pages.models import Page

class PageType(DjangoObjectType):
    class Meta:
        model = Page

# Query
class Query(graphene.ObjectType):
    pages = graphene.List(PageType)

    def resolve_pages(self, info, **kwargs):
        return Page.objects.all()

models.py

from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList, StreamFieldPanel
from wagtail.wagtailcore.models import Page as WagtailPage, Orderable
from django.http import JsonResponse
from wagtail.wagtailcore.fields import StreamField
from wagtail.wagtailcore import blocks
from wagtail.wagtailimages.blocks import ImageChooserBlock
from wagtail.wagtaildocs.blocks import DocumentChooserBlock
from wagtail.wagtailsnippets.blocks import SnippetChooserBlock
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from wagtail.wagtailembeds.blocks import EmbedBlock
from modelcluster.fields import ParentalKey

from django.db import models


class PageIndex:
    # Parent page / subpage type rules
    parent_page_types = []
    subpage_types = []

    def serve(self, request):
        return JsonResponse({
            'title': self.title,
            'body': self.body,
        })


class CarouselBlock(blocks.StreamBlock):
    image = ImageChooserBlock()
    caption = blocks.TextBlock(blank=True)

    class Meta:
        icon = 'image'


class Page(WagtailPage):
    body = StreamField([
        ('title', blocks.TextBlock(icon="title")),
        ('paragraph', blocks.RichTextBlock(editor='tinymce')),
        ('url', blocks.URLBlock()),
        ('blockquote', blocks.BlockQuoteBlock()),
        ('document', DocumentChooserBlock()),
        ('image', ImageChooserBlock()),
        ('media', EmbedBlock()),
        ('snippet', SnippetChooserBlock(target_model='contents.Content')),
    ], blank=True, null=True)

    content_panels = WagtailPage.content_panels + [
        StreamFieldPanel('body'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Contents'),
        ObjectList(WagtailPage.promote_panels, heading='Promote'),
        ObjectList(WagtailPage.settings_panels, heading='Settings', classname="settings"),
    ])

    def get_url_parts(self, *args, **kwargs):
        super(Page, self).get_url_parts(*args, **kwargs)


# Snippet Model
class PageContentPlacement(Orderable, models.Model):
    page = ParentalKey('pages.Page', related_name='content_placements')
    content = models.ForeignKey('contents.Content', related_name='+')

    class Meta:
        verbose_name = "content placement"
        verbose_name_plural = "content placements"

    panels = [
        SnippetChooserPanel('content'),
    ]

    def __str__(self):
        return self.page.title + " -> " + self.content.description

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:2
  • Comments:12 (2 by maintainers)

github_iconTop GitHub Comments

5reactions
osartuncommented, Apr 30, 2018

I created an implementation that works! And I’m posting it here, so that nobody has to struggle with it the way I did.

I’m not sure how Pythonic this solution is though.

First: I wanted to have a Union type for the StreamField so that I can use Inline Fragments in the GraphQL query for each block. As there are potentially many kinds of StreamFields in a wagtail app with different kinds of blocks I wanted to have a function that generates the Union type the way I want to have it. So, I created create_stream_field_type it returns a tuple with the graphene schema type and a resolver function.

# utils.py
import graphene

# …

def create_stream_field_type(field_name, **kwargs):
    block_type_handlers = kwargs.copy()

    class StreamFieldType(graphene.Union):
        class Meta:
            types = (GenericStreamBlock, ) + tuple(
                block_type_handlers.values())

    def convert_block(block):
        block_type = block.get('type')
        value = block.get('value')
        if block_type in block_type_handlers:
            handler = block_type_handlers.get(block_type)
            if isinstance(value, dict):
                return handler(value=value, block_type=block_type, **value)
            else:
                return handler(value=value, block_type=block_type)
        else:
            return GenericStreamBlock(value=value, block_type=block_type)

    def resolve_field(self, info):
        field = getattr(self, field_name)
        if not isinstance(field, StreamValue):
            raise Exception(
                f"Field '{field_name}' of {type(self)} not a StreamField")
        return [convert_block(block) for block in field.stream_data]

    return (graphene.List(StreamFieldType), resolve_field)

By default it resolves each block as a GenericStreamBlock. This is its implementation:

# utils.py
import graphene
from graphene.types.generic import GenericScalar

# …

class GenericStreamBlock(graphene.ObjectType):
    block_type = graphene.String()
    value = GenericScalar()

This is how you use it:

# schema.py

class PromoPageNode(DjangoObjectType):
    content, resolve_content = create_stream_field_type('content')
    …

And this is how a query would look like:

{
  allPromoPages(language: "en") {
    content {
      ... on GenericStreamBlock {
        blockType
        value
      }
    }
  }
}

Now, this is cool and all, but it actually doesn’t help in any way.

To use the power of inline fragments you need to define a custom type for each block. Let’s start with a ParagraphBlock.

# schema.py
from .utils import GenericStreamBlock, create_stream_field_type

# …

class ParagraphBlock(GenericStreamBlock):
    pass

# …

class PromoPageNode(DjangoObjectType):
    content, resolve_content = create_stream_field_type('content', paragraph=ParagraphBlock)
    # …

All paragraph blocks are now resolved as a ParagraphBlock.

The query could look like this now:

{
  allPromoPages(language: "en") {
    content {	
      ... on ParagraphBlock {
        value
      }
      ... on GenericStreamBlock {
        blockType
        value
      }
    }
  }
}

My paragraph blocks are pretty simple. I have some more complex StructBlocks though and this is where this implementation comes in really handy.

# schema.py
from promo.snippets import Testimonial
from .utils import GenericStreamBlock, create_stream_field_type

# …

class TestimonialNode(DjangoObjectType):
    class Meta:
        model = Testimonial


class FeatureBlock(GenericStreamBlock):
    headline = graphene.String()
    color = graphene.String()
    description = graphene.String()
    screenshot = graphene.ID()


class TestimonialBlock(GenericStreamBlock):
    headline = graphene.String()
    text = graphene.String()
    testimonials = graphene.List(TestimonialNode)

    def resolve_testimonials(self, info):
        testimonial_ids = self.value.get('testimonials')
        return Testimonial.objects.filter(id__in=testimonial_ids)


class PromoPageNode(DjangoObjectType):
    content, resolve_content = create_stream_field_type(
        'content', paragraph=ParagraphBlock, feature=FeatureBlock, testimonials=TestimonialBlock)

Finally this gives me all the power to create a query like this:

{
  allPromoPages(language: "en") {
    content {	
      ... on ParagraphBlock {
        value
      }
      ... on GenericStreamBlock {
        blockType
        value
      }
      ... on FeatureBlock {
        headline
        color
        description
      }
      ... on TestimonialBlock {
        headline
        text
        testimonials {
          id
          name
          gender
          age
        }
      }
    }
  }
}

I hope this helps people who also want to implement a powerful GraphQL solution for StreamFields with Graphene.

0reactions
stale[bot]commented, Jul 7, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Supporting StreamFields, Snippets and Images in a Wagtail ...
This tutorial shows you how you can create a Wagtail GraphQL API that supports StreamFields, Snippets, and Images in Wagtail.
Read more >
Developers - How to let GraphQL read Streamfield - - Bountysource
Any ideas how can I solve this problem to let StreamField read on GraphQL? Below are the code and error show. after i...
Read more >
Streamfields — wagtail-graphql-api documentation
Each Wagtail project will have its own definition of stream field blocks. wagtail-graphql-api does a job of serialising them. However each of the...
Read more >
Customizing the behavior of cached fields - Apollo GraphQL
A read function that specifies what happens when the field's cached value is read ... Let's say our graph's schema includes the following...
Read more >
How Hasura GraphQL Engine works
The Hasura GraphQL Engine automatically generates a unified GraphQL schema from your ... A subscription stream field with where , and cursor arguments....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found