Prevent deconstruct_with_lookup from breaking on ListBlock subclasses with custom constructors

Fixes #12164
pull/12173/head
Matt Westcott 2024-07-24 19:17:14 +01:00 zatwierdzone przez Matt Westcott
rodzic 42c566b19b
commit 4bed63dbb7
2 zmienionych plików z 95 dodań i 6 usunięć

Wyświetl plik

@ -151,11 +151,20 @@ class ListBlock(Block):
# Default to a list consisting of one empty (i.e. default-valued) child item
self.meta.default = [self.child_block.get_default()]
# If a subclass of ListBlock overrides __init__, we cannot assume that the first argument is
# the child block, and thus we cannot rely on the conversion applied in construct_from_lookup /
# deconstruct_with_lookup to be valid. We set a flag attribute on the __init__ method so that
# we can spot this case.
__init__.has_child_block_arg = True
@classmethod
def construct_from_lookup(cls, lookup, child_block, **kwargs):
if isinstance(child_block, int):
child_block = lookup.get_block(child_block)
return cls(child_block, **kwargs)
def construct_from_lookup(cls, lookup, *args, **kwargs):
if getattr(cls.__init__, "has_child_block_arg", False) and isinstance(
args[0], int
):
child_block = lookup.get_block(args[0])
args = (child_block, *args[1:])
return cls(*args, **kwargs)
def value_from_datadict(self, data, files, prefix):
count = int(data["%s-count" % prefix])
@ -404,8 +413,11 @@ class ListBlock(Block):
def deconstruct_with_lookup(self, lookup):
path, args, kwargs = super().deconstruct_with_lookup(lookup)
if isinstance(args[0], Block):
args = (lookup.add_block(args[0]),)
if getattr(self.__init__, "has_child_block_arg", False) and isinstance(
args[0], Block
):
block_id = lookup.add_block(args[0])
args = (block_id, *args[1:])
return path, args, kwargs
class Meta:

Wyświetl plik

@ -824,6 +824,13 @@ class TestConstructStreamFieldFromLookup(TestCase):
self.assertEqual(link_text_block.name, "link_text")
# Used by TestDeconstructStreamFieldWithLookup.test_deconstruct_with_listblock_subclass -
# needs to be a module-level definition so that the path returned from deconstruct is valid
class BulletListBlock(blocks.ListBlock):
def __init__(self, **kwargs):
super().__init__(blocks.CharBlock(required=True), **kwargs)
class TestDeconstructStreamFieldWithLookup(TestCase):
def test_deconstruct(self):
class ButtonBlock(blocks.StructBlock):
@ -874,3 +881,73 @@ class TestDeconstructStreamFieldWithLookup(TestCase):
},
},
)
def test_deconstruct_with_listblock(self):
field = StreamField(
[
("heading", blocks.CharBlock(required=True)),
("bullets", blocks.ListBlock(blocks.CharBlock(required=True))),
],
blank=True,
)
field.set_attributes_from_name("body")
name, path, args, kwargs = field.deconstruct()
self.assertEqual(name, "body")
self.assertEqual(path, "wagtail.fields.StreamField")
self.assertEqual(
args,
[
[
("heading", 0),
("bullets", 1),
]
],
)
self.assertEqual(
kwargs,
{
"blank": True,
"block_lookup": {
0: ("wagtail.blocks.CharBlock", (), {"required": True}),
1: ("wagtail.blocks.ListBlock", (0,), {}),
},
},
)
def test_deconstruct_with_listblock_subclass(self):
# See https://github.com/wagtail/wagtail/issues/12164 - unlike StructBlock and StreamBlock,
# ListBlock's deconstruct method doesn't reduce subclasses to the base ListBlock class.
# Therefore, if a ListBlock subclass defines its own __init__ method with an incompatible
# signature to the base ListBlock, this custom signature will be preserved in the result of
# deconstruct(), and we cannot rely on the first argument being the child block.
field = StreamField(
[
("heading", blocks.CharBlock(required=True)),
("bullets", BulletListBlock()),
],
blank=True,
)
field.set_attributes_from_name("body")
name, path, args, kwargs = field.deconstruct()
self.assertEqual(name, "body")
self.assertEqual(path, "wagtail.fields.StreamField")
self.assertEqual(
args,
[
[
("heading", 0),
("bullets", 1),
]
],
)
self.assertEqual(
kwargs,
{
"blank": True,
"block_lookup": {
0: ("wagtail.blocks.CharBlock", (), {"required": True}),
1: ("wagtail.tests.test_streamfield.BulletListBlock", (), {}),
},
},
)