diff --git a/wagtail/api/v2/serializers.py b/wagtail/api/v2/serializers.py index 896a3ee812..e44f56957e 100644 --- a/wagtail/api/v2/serializers.py +++ b/wagtail/api/v2/serializers.py @@ -225,7 +225,7 @@ class StreamField(Field): foreign objects are nested objects with id and meta as attributes. """ def to_representation(self, value): - return value.stream_block.get_prep_value(value) + return value.stream_block.get_api_representation(value, self.context) class TagsField(Field): diff --git a/wagtail/api/v2/tests/test_pages.py b/wagtail/api/v2/tests/test_pages.py index d8b70107e8..ceb8c24c0c 100644 --- a/wagtail/api/v2/tests/test_pages.py +++ b/wagtail/api/v2/tests/test_pages.py @@ -1025,6 +1025,18 @@ class TestPageDetailWithStreamField(TestCase): # ForeignKeys in a StreamField shouldn't be translated into dictionary representation self.assertEqual(content['body'], [{'type': 'image', 'value': 1}]) + def test_image_block_with_custom_get_api_representation(self): + stream_page = self.make_stream_page('[{"type": "image", "value": 1}]') + + response_url = '{}?extended=1'.format( + reverse('wagtailapi_v2:pages:detail', args=(stream_page.id, )) + ) + response = self.client.get(response_url) + content = json.loads(response.content.decode('utf-8')) + + # the custom get_api_representation returns a dict of id and title for the image + self.assertEqual(content['body'], [{'type': 'image', 'value': {'id': 1, 'title': 'A missing image'}}]) + @override_settings( WAGTAILFRONTENDCACHE={ diff --git a/wagtail/tests/testapp/migrations/0013_auto_20161220_1957.py b/wagtail/tests/testapp/migrations/0013_auto_20161220_1957.py new file mode 100644 index 0000000000..e3f6e443df --- /dev/null +++ b/wagtail/tests/testapp/migrations/0013_auto_20161220_1957.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-20 10:57 +from __future__ import unicode_literals + +from django.db import migrations +import wagtail.tests.testapp.models +import wagtail.wagtailcore.blocks +import wagtail.wagtailcore.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0012_panelsettings_tabbedsettings'), + ] + + operations = [ + migrations.AlterField( + model_name='streampage', + name='body', + field=wagtail.wagtailcore.fields.StreamField([('text', wagtail.wagtailcore.blocks.CharBlock()), ('rich_text', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.tests.testapp.models.ExtendedImageChooserBlock())]), + ), + ] diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py index 641d98df24..f984200e54 100644 --- a/wagtail/tests/testapp/models.py +++ b/wagtail/tests/testapp/models.py @@ -638,11 +638,27 @@ class StreamModel(models.Model): ]) +class ExtendedImageChooserBlock(ImageChooserBlock): + """ + Example of Block with custom get_api_representation method. + If the request has an 'extended' query param, it returns a dict of id and title, + otherwise, it returns the default value. + """ + def get_api_representation(self, value, context=None): + image_id = super(ExtendedImageChooserBlock, self).get_api_representation(value, context=context) + if 'request' in context and context['request'].query_params.get('extended', False): + return { + 'id': image_id, + 'title': value.title + } + return image_id + + class StreamPage(Page): body = StreamField([ ('text', CharBlock()), ('rich_text', RichTextBlock()), - ('image', ImageChooserBlock()), + ('image', ExtendedImageChooserBlock()), ]) api_fields = ('body',) diff --git a/wagtail/wagtailcore/blocks/base.py b/wagtail/wagtailcore/blocks/base.py index 17e8f8d25d..1531762983 100644 --- a/wagtail/wagtailcore/blocks/base.py +++ b/wagtail/wagtailcore/blocks/base.py @@ -257,6 +257,12 @@ class Block(six.with_metaclass(BaseBlock, object)): return mark_safe(render_to_string(template, new_context)) + def get_api_representation(self, value, context=None): + """ + Can be used to customise the API response and defaults to the value returned by get_prep_value. + """ + return self.get_prep_value(value) + def render_basic(self, value, context=None): """ Return a text rendering of 'value', suitable for display on templates. render() will fall back on diff --git a/wagtail/wagtailcore/blocks/list_block.py b/wagtail/wagtailcore/blocks/list_block.py index 7e940100b9..f12e4d0b84 100644 --- a/wagtail/wagtailcore/blocks/list_block.py +++ b/wagtail/wagtailcore/blocks/list_block.py @@ -143,6 +143,13 @@ class ListBlock(Block): for item in value ] + def get_api_representation(self, value, context=None): + # recursively call get_api_representation on children and return as a list + return [ + self.child_block.get_api_representation(item, context=context) + for item in value + ] + def render_basic(self, value, context=None): children = format_html_join( '\n', '
  • {0}
  • ', diff --git a/wagtail/wagtailcore/blocks/stream_block.py b/wagtail/wagtailcore/blocks/stream_block.py index b515c40a94..9e3502e9a2 100644 --- a/wagtail/wagtailcore/blocks/stream_block.py +++ b/wagtail/wagtailcore/blocks/stream_block.py @@ -211,6 +211,16 @@ class BaseStreamBlock(Block): for child in value # child is a BoundBlock instance ] + def get_api_representation(self, value, context=None): + if value is None: + # treat None as identical to an empty stream + return [] + + return [ + {'type': child.block.name, 'value': child.block.get_api_representation(child.value, context=context)} + for child in value # child is a BoundBlock instance + ] + def render_basic(self, value, context=None): return format_html_join( '\n', '
    {0}
    ', diff --git a/wagtail/wagtailcore/blocks/struct_block.py b/wagtail/wagtailcore/blocks/struct_block.py index dc60e1b161..f8eb75509e 100644 --- a/wagtail/wagtailcore/blocks/struct_block.py +++ b/wagtail/wagtailcore/blocks/struct_block.py @@ -138,6 +138,13 @@ class BaseStructBlock(Block): for name, val in value.items() ]) + def get_api_representation(self, value, context=None): + # recursively call get_api_representation on children and return as a plain dict + return dict([ + (name, self.child_blocks[name].get_api_representation(val, context=context)) + for name, val in value.items() + ]) + def get_searchable_content(self, value): content = [] diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index 14002ea205..517d08fc42 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -961,6 +961,39 @@ class TestStructBlock(SimpleTestCase): self.assertHTMLEqual(html, expected_html) + def test_get_api_representation_calls_same_method_on_fields_with_context(self): + """ + The get_api_representation method of a StructBlock should invoke + the block's get_api_representation method on each field and the + context should be passed on. + """ + class ContextBlock(blocks.CharBlock): + def get_api_representation(self, value, context=None): + return context[value] + + class AuthorBlock(blocks.StructBlock): + language = ContextBlock() + author = ContextBlock() + + block = AuthorBlock() + api_representation = block.get_api_representation( + { + 'language': 'en', + 'author': 'wagtail', + }, + context={ + 'en': 'English', + 'wagtail': 'Wagtail!' + } + ) + + self.assertDictEqual( + api_representation, { + 'language': 'English', + 'author': 'Wagtail!' + } + ) + def test_render_unknown_field(self): class LinkBlock(blocks.StructBlock): title = blocks.CharBlock() @@ -1334,6 +1367,28 @@ class TestListBlock(unittest.TestCase): self.assertIn('

    Bonjour le monde!

    ', html) self.assertIn('

    Au revoir le monde!

    ', html) + def test_get_api_representation_calls_same_method_on_children_with_context(self): + """ + The get_api_representation method of a ListBlock should invoke + the block's get_api_representation method on each child and + the context should be passed on. + """ + class ContextBlock(blocks.CharBlock): + def get_api_representation(self, value, context=None): + return context[value] + + block = blocks.ListBlock( + ContextBlock() + ) + api_representation = block.get_api_representation(["en", "fr"], context={ + 'en': 'Hello world!', + 'fr': 'Bonjour le monde!' + }) + + self.assertEqual( + api_representation, ['Hello world!', 'Bonjour le monde!'] + ) + def render_form(self): class LinkBlock(blocks.StructBlock): title = blocks.CharBlock() @@ -1652,6 +1707,38 @@ class TestStreamBlock(SimpleTestCase): return block.render(value) + def test_get_api_representation_calls_same_method_on_children_with_context(self): + """ + The get_api_representation method of a StreamBlock should invoke + the block's get_api_representation method on each child and + the context should be passed on. + """ + class ContextBlock(blocks.CharBlock): + def get_api_representation(self, value, context=None): + return context[value] + + block = blocks.StreamBlock([ + ('language', ContextBlock()), + ('author', ContextBlock()), + ]) + api_representation = block.get_api_representation( + block.to_python([ + {'type': 'language', 'value': 'en'}, + {'type': 'author', 'value': 'wagtail'}, + ]), + context={ + 'en': 'English', + 'wagtail': 'Wagtail!' + } + ) + + self.assertListEqual( + api_representation, [ + {'type': 'language', 'value': 'English'}, + {'type': 'author', 'value': 'Wagtail!'}, + ] + ) + def test_render(self): html = self.render_article([ {