Implement a ListValue type for ListBlocks

pull/7831/head
Matt Westcott 2021-11-22 19:31:10 +00:00 zatwierdzone przez Matt Westcott
rodzic 93229cfc14
commit 4a848bfb4e
3 zmienionych plików z 54 dodań i 11 usunięć

Wyświetl plik

@ -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'])
```

Wyświetl plik

@ -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 "<ListValue: %r>" % (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

Wyświetl plik

@ -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)],