Add server-side min_num and max_num validation for ListBlock

Fixes #5300
pull/7345/head
Matt Westcott 2021-07-09 18:18:57 +01:00
rodzic bdfa3811d1
commit 8c97c254a7
3 zmienionych plików z 77 dodań i 1 usunięć

Wyświetl plik

@ -440,9 +440,11 @@ Structural block types
]))),
])
The following additional option is available as either a keyword argument or a Meta class attribute:
The following additional options are available as either keyword arguments or Meta class attributes:
:param form_classname: An HTML ``class`` attribute to set on the root element of this block as displayed in the editing interface.
:param min_num: Minimum number of sub-blocks that the list must have.
:param max_num: Maximum number of sub-blocks that the list may have.
.. class:: wagtail.core.blocks.StreamBlock

Wyświetl plik

@ -98,6 +98,16 @@ class ListBlock(Block):
else:
errors.append(None)
if self.meta.min_num is not None and self.meta.min_num > len(value):
non_block_errors.append(ValidationError(
_('The minimum number of items is %d') % self.meta.min_num
))
if self.meta.max_num is not None and self.meta.max_num < len(value):
non_block_errors.append(ValidationError(
_('The maximum number of items is %d') % self.meta.max_num
))
if any(errors) or non_block_errors:
raise ListBlockValidationError(block_errors=errors, non_block_errors=non_block_errors)
@ -172,6 +182,10 @@ class ListBlock(Block):
# descendant block type
icon = "placeholder"
form_classname = None
min_num = None
max_num = None
MUTABLE_META_ATTRIBUTES = ['min_num', 'max_num']
class ListBlockAdapter(Adapter):
@ -193,6 +207,12 @@ class ListBlockAdapter(Adapter):
meta['helpText'] = help_text
meta['helpIcon'] = get_help_icon()
if block.meta.min_num is not None:
meta['minNum'] = block.meta.min_num
if block.meta.max_num is not None:
meta['maxNum'] = block.meta.max_num
return [
block.name,
block.child_block,

Wyświetl plik

@ -2179,6 +2179,34 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
},
})
def test_adapt_with_min_num_max_num(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = blocks.ListBlock(LinkBlock, min_num=2, max_num=5)
block.set_name('test_listblock')
js_args = ListBlockAdapter().js_args(block)
self.assertEqual(js_args[0], 'test_listblock')
self.assertIsInstance(js_args[1], LinkBlock)
self.assertEqual(js_args[2], {'title': None, 'link': None})
self.assertEqual(js_args[3], {
'label': 'Test listblock',
'icon': 'placeholder',
'classname': None,
'minNum': 2,
'maxNum': 5,
'strings': {
'DELETE': 'Delete',
'DUPLICATE': 'Duplicate',
'MOVE_DOWN': 'Move down',
'MOVE_UP': 'Move up',
'ADD': 'Add',
},
})
def test_searchable_content(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
@ -2332,6 +2360,32 @@ class TestListBlock(WagtailTestUtils, SimpleTestCase):
},
})
def test_min_num_validation_errors(self):
block = blocks.ListBlock(blocks.CharBlock(), min_num=2)
with self.assertRaises(ValidationError) as catcher:
block.clean(['foo'])
self.assertEqual(catcher.exception.params, {
'block_errors': [None],
'non_block_errors': ['The minimum number of items is 2']
})
# a value with >= 2 blocks should pass validation
self.assertTrue(block.clean(['foo', 'bar']))
def test_max_num_validation_errors(self):
block = blocks.ListBlock(blocks.CharBlock(), max_num=2)
with self.assertRaises(ValidationError) as catcher:
block.clean(['foo', 'bar', 'baz'])
self.assertEqual(catcher.exception.params, {
'block_errors': [None, None, None],
'non_block_errors': ['The maximum number of items is 2']
})
# a value with <= 2 blocks should pass validation
self.assertTrue(block.clean(['foo', 'bar']))
class TestListBlockWithFixtures(TestCase):
fixtures = ['test.json']