From 4a848bfb4e3ec1a84a3d36fda577c1ed784de498 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 22 Nov 2021 19:31:10 +0000 Subject: [PATCH] Implement a ListValue type for ListBlocks --- docs/releases/2.16.md | 14 +++++++++++++ wagtail/core/blocks/list_block.py | 35 +++++++++++++++++++++++++++---- wagtail/core/tests/test_blocks.py | 16 +++++++------- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/docs/releases/2.16.md b/docs/releases/2.16.md index f9d7c0e488..30a58709d6 100644 --- a/docs/releases/2.16.md +++ b/docs/releases/2.16.md @@ -32,3 +32,17 @@ ### Removed support for Python 3.6 Python 3.6 is no longer supported as of this release; please upgrade to Python 3.7 or above before upgrading Wagtail. + +### StreamField ListBlock now returns `ListValue` rather than a list instance + +The data type returned as the value of a ListBlock is now a custom class, `ListValue`, rather than a Python `list` object. This change allows it to provide a `bound_blocks` property that exposes the list items as [`BoundBlock` objects](../advanced_topics/boundblocks_and_values) rather than plain values. `ListValue` objects are mutable sequences that behave similarly to lists, and so all code that iterates over them, accesses individual elements, or manipulates them should continue to work. However, code that specifically expects a `list` object (e.g. using `isinstance` or testing for equality against a list) may need to be updated. For example, a unit test that tests the value of a `ListBlock` as follows: + +```python + self.assertEqual(page.body[0].value, ['hello', 'goodbye']) +``` + +should be rewritten as: + +```python + self.assertEqual(list(page.body[0].value), ['hello', 'goodbye']) +``` diff --git a/wagtail/core/blocks/list_block.py b/wagtail/core/blocks/list_block.py index 84636e61a6..e6141aaea5 100644 --- a/wagtail/core/blocks/list_block.py +++ b/wagtail/core/blocks/list_block.py @@ -1,4 +1,5 @@ import itertools +from collections.abc import MutableSequence from django import forms from django.core.exceptions import ValidationError @@ -48,6 +49,32 @@ class ListBlockValidationErrorAdapter(Adapter): register(ListBlockValidationErrorAdapter(), ListBlockValidationError) +class ListValue(MutableSequence): + def __init__(self, values=None): + if values is None: + self.values = [] + else: + self.values = values + + def __getitem__(self, i): + return self.values[i] + + def __setitem__(self, i, val): + self.values[i] = val + + def __delitem__(self, i): + del self.values[i] + + def __len__(self): + return len(self.values) + + def insert(self, i, item): + self.values.insert(i, item) + + def __repr__(self): + return "" % (self.values, ) + + class ListBlock(Block): def __init__(self, child_block, **kwargs): @@ -65,7 +92,7 @@ class ListBlock(Block): def get_default(self): # wrap with list() so that each invocation of get_default returns a distinct instance - return list(self.meta.default) + return ListValue(values=list(self.meta.default)) def value_from_datadict(self, data, files, prefix): count = int(data['%s-count' % prefix]) @@ -111,11 +138,11 @@ class ListBlock(Block): if any(errors) or non_block_errors: raise ListBlockValidationError(block_errors=errors, non_block_errors=non_block_errors) - return result + return ListValue(values=result) def to_python(self, value): # 'value' is a list of child block values; use bulk_to_python to convert them all in one go - return self.child_block.bulk_to_python(value) + return ListValue(values=self.child_block.bulk_to_python(value)) def bulk_to_python(self, values): # 'values' is a list of lists of child block values; concatenate them into one list so that @@ -128,7 +155,7 @@ class ListBlock(Block): result = [] offset = 0 for sublist_len in lengths: - result.append(converted_values[offset:offset + sublist_len]) + result.append(ListValue(values=converted_values[offset:offset + sublist_len])) offset += sublist_len return result diff --git a/wagtail/core/tests/test_blocks.py b/wagtail/core/tests/test_blocks.py index db9033bbfc..1d315c2e68 100644 --- a/wagtail/core/tests/test_blocks.py +++ b/wagtail/core/tests/test_blocks.py @@ -1823,9 +1823,9 @@ class TestStructBlock(SimpleTestCase): ]) shopping_lists[0]['items'].append('cake') - self.assertEqual(shopping_lists[0]['items'], ['chocolate', 'cake']) + self.assertEqual(list(shopping_lists[0]['items']), ['chocolate', 'cake']) # shopping_lists[1] should not be updated - self.assertEqual(shopping_lists[1]['items'], ['chocolate']) + self.assertEqual(list(shopping_lists[1]['items']), ['chocolate']) def test_clean(self): block = blocks.StructBlock([ @@ -2272,7 +2272,7 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase): def test_can_specify_default(self): block = blocks.ListBlock(blocks.CharBlock(), default=['peas', 'beans', 'carrots']) - self.assertEqual(block.get_default(), ['peas', 'beans', 'carrots']) + self.assertEqual(list(block.get_default()), ['peas', 'beans', 'carrots']) def test_default_default(self): """ @@ -2281,7 +2281,7 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase): """ block = blocks.ListBlock(blocks.CharBlock(default='chocolate')) - self.assertEqual(block.get_default(), ['chocolate']) + self.assertEqual(list(block.get_default()), ['chocolate']) block.set_name('test_shoppinglistblock') js_args = ListBlockAdapter().js_args(block) @@ -2303,9 +2303,9 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase): asda_shopping = block.to_python({'shop': 'Asda'}) # 'items' will default to ['chocolate'], but a distinct instance tesco_shopping['items'].append('cake') - self.assertEqual(tesco_shopping['items'], ['chocolate', 'cake']) + self.assertEqual(list(tesco_shopping['items']), ['chocolate', 'cake']) # asda_shopping should not be modified - self.assertEqual(asda_shopping['items'], ['chocolate']) + self.assertEqual(list(asda_shopping['items']), ['chocolate']) def test_adapt_with_classname_via_kwarg(self): """form_classname from kwargs to be used as an additional class when rendering list block""" @@ -2409,8 +2409,10 @@ class TestListBlockWithFixtures(TestCase): with self.assertNumQueries(1): result = block.bulk_to_python([[4, 5], [], [2]]) + # result will be a list of ListValues - convert to lists for equality check + clean_result = [list(val) for val in result] - self.assertEqual(result, [ + self.assertEqual(clean_result, [ [Page.objects.get(id=4), Page.objects.get(id=5)], [], [Page.objects.get(id=2)],