Skip to content

Instantly share code, notes, and snippets.

@thclark
Created May 23, 2019 19:18
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thclark/100d6aa6d0995984589b983f896002d4 to your computer and use it in GitHub Desktop.
Save thclark/100d6aa6d0995984589b983f896002d4 to your computer and use it in GitHub Desktop.
Wagtail serializers for streamfield - example of customizing per-block
class HeroBlock(StructBlock):
content = StreamBlock([
('button', StructBlock([
('text', CharBlock(required=False, max_length=80, label='Label')),
('url', URLBlock(required=False, label='URL')),
], label='Call to action', help_text='A "call-to-action" button, like "Sign Up Now!"')),
('video', EmbedBlock(label='Video')),
('quote', StructBlock([
('text', TextBlock()),
('author', CharBlock(required=False)),
], required=False, label='Quote', help_text='An inspiring quotation, optionally attributed to someone'))
], required=False)
class Meta:
icon = 'placeholder'
label = 'Hero Section
from wagtail.core.blocks import StreamBlock, ListBlock, StructBlock
from rest_framework.fields import Field
class StreamField(Field):
"""
Serializes StreamField values.
Stream fields are stored in JSON format in the database. We reuse that in
the API.
Example:
"body": [
{
"type": "heading",
"value": {
"text": "Hello world!",
"size": "h1"
}
},
{
"type": "paragraph",
"value": "Some content"
}
{
"type": "image",
"value": 1
}
]
Where "heading" is a struct block containing "text" and "size" fields, and "paragraph" is a simple text block.
NOTE: foreign keys are represented slightly differently in stream fields compared to other parts of the wagtail API.
In stream fields, a foreign key is represented by an integer (the ID of the related object) but elsewhere in the API,
foreign objects are nested objects with id and meta as attributes.
This creates problems, in particular, for serialising images - since for each image, you need a second call to the
API to find its URL then create the page. Yet another example of Wagtail's weird customisations being very
unhelpful.
You can override the behaviour using get_api_representation **at the model or snippet level**
So, I drilled into the StreamField code, and pulled out where the actual serialization happens. Here, we treat the
api representation built into the block as default; but can override it for specific block names.
"""
def __init__(self, *args, **kwargs):
self.serializers = kwargs.pop('serializers', object())
self.recurse = False # kwargs.pop('recurse', True) --- Recursion not working yet; see below
super(StreamField, self).__init__(*args, **kwargs)
def to_representation(self, value, ctr=0):
# print('AT RECURSION LEVEL:', ctr)
representation = []
if value is None:
return representation
for stream_item in value:
# print('Stream item type:', type(stream_item))
# If there's a serializer for the child block type in the mapping, switch to it
if stream_item.block.name in self.serializers.keys():
item_serializer = self.serializers[stream_item.block.name]
if item_serializer is not None:
# print('Switching to specified serializer {} for stream_item {}'.format(
# self.serializers[stream_item.block.name].__class__.__name__,
# stream_item.block.name
# ))
item_representation = item_serializer(context=self.context).to_representation(stream_item.value)
else:
print('Specified serializer for child {} is explicitly set to None - using default API representation for block:', stream_item.block.name)
item_representation = stream_item.block.get_api_representation(stream_item.value, context=self.context)
# If recursion is turned on, recurse through structural blocks using the present serializer mapping
# TODO the blocks data structure is borked. Nodes and leaves are flattened together so I can't recurse down the tree straightforwardly.
# I have to apply the default representation at this level since it works on the whole block, then extract child items that are either custom-mapped or
# structural and recurse down only those, zipping their results back together. Spent several hours, leaving it until someone has a very strong use case.
elif self.recurse and isinstance(stream_item.block, (StructBlock, StreamBlock, ListBlock, )):
# print('Recursing serialiser for structural block::', stream_item.block.name)
item_representation = self.to_representation(stream_item.value)
else:
# print('No recursion, or leaf node reached with no specified serializer mapping for block:', stream_item.block.name)
item_representation = stream_item.block.get_api_representation(stream_item.value, context=self.context)
representation.append({
'type': stream_item.block.name,
'value': item_representation,
'id': stream_item.id,
})
return representation
from wagtail.admin.edit_handlers import StreamFieldPanel
from wagtail.core.blocks import CharBlock, TextBlock, BlockQuoteBlock, PageChooserBlock, URLBlock, StructBlock
from wagtail.core.fields import StreamField
from wagtail.core.models import Page
from .blocks import HeroSectionBlock
class HomePage(Page):
""" HomePage is used to manage SEO data and content including the hero banner
"""
body = StreamField([
('heading', CharBlock(required=False, label='Heading', max_length=120, help_text='', icon='arrow-right')),
('quote', BlockQuoteBlock(required=False, label='Quote', help_text='')),
('hero_section', HeroSectionBlock()),
], blank=True, help_text='Assorted example content in a Streamfield, including the HeaderSectionBlock from above')
content_panels = Page.content_panels + [
StreamFieldPanel('body', heading='Page contents'),
]
import logging
from rest_framework import serializers
from wagtail.images.api.v2.serializers import ImageDownloadUrlField
from wagtail.documents.api.v2.serializers import DocumentDownloadUrlField
from wagtail_events.models import EventIndex, Event
from wagtail_references.serializers import ReferenceSerializer
from .fields import StreamField
from .models import HomePage
class FirstQuoteSerializer(serializers.Serializer):
text = serializers.SerializerMethodField()
author = serializers.SerializerMethodField()
class Meta:
fields = ('text', 'author')
def get_text(self, obj):
return 'output using first quote serializer'
def get_author(self, obj):
return 'output using first quote serializer'
class SecondQuoteSerializer(serializers.Serializer):
text = serializers.SerializerMethodField()
author = serializers.SerializerMethodField()
class Meta:
fields = ('text', 'author')
def get_text(self, obj):
return 'output using second quote serializer'
def get_author(self, obj):
return 'output using second quote serializer'
class HeroSectionSerializer(serializers.Serializer):
""" See how the StreamField is used to nest serializers to the next level down?
"""
content = StreamField(serializers={
'quote': SecondQuoteSerializer
})
class Meta:
fields = (
'content'
)
class HomePageSerializer(serializers.ModelSerializer):
""" You can use whatever page serializer you want - this cut down one here for completeness of the example.
"""
body = StreamField(serializers={
'quote': FirstQuoteSerializer,
'image': ImageSerializer,
'hero_section': HeroSectionSerializer
})
class Meta:
model = HomePage
fields = (
'slug',
'body'
'title',
)
@allcaps
Copy link

allcaps commented Mar 22, 2023

Thank you for this snippet. It has proven very useful in some of our headless Wagtail projects!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment