Wagtail and GraphQL

I’ve been looking into whether using Wagtail with GraphQL as a headless CMS would be viable for a future project. To start with I used Brent Clark’s excellent and easy to follow tutorial which got me up and running.

Using Graphene i quickly found it was fine for normal Django fields but when it came to things like Wagtail’s tags or Streamfield or image fields it didn’t know how to handle them. Sometimes it returned just an image ID for example but no url that a front end could use.

Searching the web I couldn’t find much specifically on Wagtail and GraphQL and not too much on Django/GraphQL, I did find Patrick Arminio’s example code which was very helpful with dealing with StreamField and how to setup a converter app. I’ve based how I’ve handled things a lot on that. Some of the examples I found in comments or snippets and have been reused here though I can’t remember where I found the tags example. I’m hoping posting it all here will help others and save time.

So anyway here’s what I did for each field type I needed, let me know if you have a better method, I’m sure this isn’t at all the best but it does work.

Tag fields

Firstly in my converter.py I import a load of things, get_image_filename is a helper that allows me to pass a Wagtail image ID and a filter then get a url for a specific rendition.

from wagtail.wagtailcore.fields import StreamField
import graphene
from graphene.types import Scalar
from modelcluster.tags import ClusterTaggableManager
from taggit.models import Tag, TaggedItemBase
from graphene_django.converter import convert_django_field
from graphene import String
from myapp.utils import get_image_filename
from bs4 import BeautifulSoup

Then how to handle a tag field based on ClusterTaggableManager

 

class FlatTags(graphene.String):

    @classmethod
    def serialize(cls, value):
        tagsList = []
        for tag in value.all():
            tagsList.append(tag.name)
        return tagsList

@convert_django_field.register(ClusterTaggableManager)
def convert_tag_field_to_string(field, registry=None):
    return graphene.Field(FlatTags,
        description=field.help_text,
        required=not field.null)

StreamField types

The way we have our Streamfield setup is we included column blocks these can then contain more blocks, so when it finds a column block it will go through that and call itself again. We don’t allow column blocks within themselves so it only ever goes one level down.

In the standard outputted paragraph block, images will be embed tags with an ID, and type etc, this code uses BeautifulSoup to swap them for image tags with a rendition URL, some extra code to change the filter used based on the image editors class would be good but this example is just a simple rough guide.

We have an image_link block type that by default just returns a Wagtail Image ID, we replace this with the rendition url, title, and the ID again.

 

def convert_image_embeds(html):
    soup = BeautifulSoup(html, 'html.parser')
    embeds = soup.find_all('embed', embedtype='image')
    if len(embeds) > 0:
        for each in embeds:
            image_id = each.attrs['id']
            image_alt = each.attrs['alt']
            image_url = get_image_filename(image_id, 'width-300')

            new_image_tag = "<img alt="&quot; + image_alt + &quot;" />"
            new_image_tag = BeautifulSoup(new_image_tag, 'html.parser')
            each.replaceWith(new_image_tag.img)

        new_html = str(soup.encode('ascii', 'ignore').decode('ascii'))
        return new_html
    else:
        return str(soup.encode('ascii', 'ignore').decode('ascii'))

def iterate_sf(blocks):
    for index, item in enumerate(blocks):
            if item['type'] == "image_link":
                # get more useful image info and replace ID field
                image_url = get_image_filename(item['value']['image'], 'width-300')
                item['value'].update({'image': image_url })

            if item['type'] == "paragraph":
                item['value'] = convert_image_embeds(item['value'])

            if item['type'] == "two_columns":
                iterate_sf(item['value']['left_column'])
                iterate_sf(item['value']['right_column'])

class StreamFieldType(Scalar):
    @staticmethod
    def serialize(dt):
        iterate_sf(dt.stream_data)
        return dt.stream_data

Here is the code for get_image_filename which lives in our utils.py in our main app

 

from django.shortcuts import get_object_or_404
from wagtail.wagtailimages.models import Image
from wagtail.wagtailimages.exceptions import InvalidFilterSpecError
from wagtail.wagtailimages.models import SourceImageIOError

def get_image_filename(img_id,filter):
    the_image = get_object_or_404(Image, pk=img_id)
    try:
        rendition = the_image.get_rendition(filter + "|format-jpeg|jpegquality-80")
    except SourceImageIOError:
        return {'filename':'error:SourceImageIOError','id':0,'title':''}
    except InvalidFilterSpecError:
        return {'filename':'error:InvalidFilterSpecError','id':0,'title':''}

    image_info = {'filename':str(rendition.file),'id':str(rendition.id),'title':str(the_image.title)}
    return image_info

Image Fields in your model

So in our example model we have an image field called item_image, in our schema.py’s MarketItemNode we add a new field which will contain our more useful image data and call it image_data, then a resolver will use get_image_filename function to return what we need if an image is present.

 

class MarketItemNode(DjangoObjectType):
    image_data = GenericScalar(required=True)

    class Meta:
        model = MarketItem
        only_fields = ['id', 'title', 'description', 'image_data', 'type_of_notice', 'location']

    def resolve_image_data(self, info):
        image_child = self.item_image

        if image_child:
            return get_image_filename(image_child.id,'width-200')
        return {}

Let me know if you know a better way to achieve these things perhaps using a Union type, then a type for each block which has been mentioned and sounds much better. Any feedback welcome. Thanks

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.