Created
May 23, 2019 19:18
-
-
Save thclark/100d6aa6d0995984589b983f896002d4 to your computer and use it in GitHub Desktop.
Wagtail serializers for streamfield - example of customizing per-block
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for this snippet. It has proven very useful in some of our headless Wagtail projects!