From 993cff3e22a0b308adf43a1072327699372091ce Mon Sep 17 00:00:00 2001 From: ebar0n Date: Tue, 30 May 2017 08:59:12 -0500 Subject: [PATCH] Add validations for min_max_fields on StreamBlock --- docs/topics/streamfield.rst | 5 ++- wagtail/wagtailcore/blocks/stream_block.py | 32 ++++++++++++++++++- wagtail/wagtailcore/tests/test_blocks.py | 36 ++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/docs/topics/streamfield.rst b/docs/topics/streamfield.rst index 227ea29ae..6bab7fc78 100644 --- a/docs/topics/streamfield.rst +++ b/docs/topics/streamfield.rst @@ -441,7 +441,7 @@ Since ``StreamField`` accepts an instance of ``StreamBlock`` as a parameter, in .. code-block:: python class HomePage(Page): - carousel = StreamField(CarouselBlock(max_num=10)) + carousel = StreamField(CarouselBlock(max_num=10, min_max_fields={'video': {'max_num': 2}})) ``StreamBlock`` accepts the following options as either keyword arguments or ``Meta`` properties: @@ -454,6 +454,9 @@ Since ``StreamField`` accepts an instance of ``StreamBlock`` as a parameter, in ``max_num`` Maximum number of sub-blocks that the stream may have. +``min_max_fields`` + Specifies the minimum and maximum number of each block type, as a dictionary mapping block names to dicts with (optional) ``min_num`` and ``max_num`` fields. + .. _streamfield_personblock_example: diff --git a/wagtail/wagtailcore/blocks/stream_block.py b/wagtail/wagtailcore/blocks/stream_block.py index 6d602f764..067b46f8d 100644 --- a/wagtail/wagtailcore/blocks/stream_block.py +++ b/wagtail/wagtailcore/blocks/stream_block.py @@ -36,12 +36,15 @@ class StreamBlockValidationError(ValidationError): class BaseStreamBlock(Block): - def __init__(self, local_blocks=None, min_num=None, max_num=None, **kwargs): + def __init__(self, local_blocks=None, min_num=None, max_num=None, min_max_fields=None, **kwargs): self._constructor_kwargs = kwargs # Used to validate the minimum and maximum number of elements in the block self.min_num = min_num self.max_num = max_num + if min_max_fields is None: + min_max_fields = {} + self.min_max_fields = min_max_fields super(BaseStreamBlock, self).__init__(**kwargs) @@ -212,6 +215,33 @@ class BaseStreamBlock(Block): [_('The maximum number of items is %s' % self.max_num)] )) + if self.min_max_fields: + fields = {} + for item in value: + if item.block_type not in self.min_max_fields: + continue + if item.block_type not in fields: + fields[item.block_type] = 0 + fields[item.block_type] += 1 + + for field in self.min_max_fields: + field_title = field.replace('_', ' ').title() + max_num = self.min_max_fields[field].get('max_num', None) + min_num = self.min_max_fields[field].get('min_num', None) + if field in fields: + if min_num and min_num > fields[field]: + non_block_errors.append(ErrorList( + ['{}: {}'.format(field_title, _('The minimum number of items is %s' % min_num))] + )) + if max_num and max_num < fields[field]: + non_block_errors.append(ErrorList( + ['{}: {}'.format(field_title, _('The maximum number of items is %s' % max_num))] + )) + elif min_num: + non_block_errors.append(ErrorList( + ['{}: {}'.format(field_title, _('The minimum number of items is %s' % min_num))] + )) + if errors or non_block_errors: # The message here is arbitrary - outputting error messages is delegated to the child blocks, # which only involves the 'params' list diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index d76b4672d..4cb00c862 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -2073,6 +2073,42 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase): '__all__': [['The maximum number of items is 1']] }) + def test_min_max_fields_min_validation_errors(self): + class ValidatedBlock(blocks.StreamBlock): + char = blocks.CharBlock() + url = blocks.URLBlock() + block = ValidatedBlock(min_max_fields={'char': {'min_num': 1}}) + + value = blocks.StreamValue(block, [ + ('url', 'http://example.com/'), + ('url', 'http://example.com/'), + ]) + + with self.assertRaises(ValidationError) as catcher: + block.clean(value) + self.assertEqual(catcher.exception.params, { + '__all__': [['Char: The minimum number of items is 1']] + }) + + def test_min_max_fields_max_validation_errors(self): + class ValidatedBlock(blocks.StreamBlock): + char = blocks.CharBlock() + url = blocks.URLBlock() + block = ValidatedBlock(min_max_fields={'char': {'max_num': 1}}) + + value = blocks.StreamValue(block, [ + ('char', 'foo'), + ('char', 'foo'), + ('url', 'http://example.com/'), + ('url', 'http://example.com/'), + ]) + + with self.assertRaises(ValidationError) as catcher: + block.clean(value) + self.assertEqual(catcher.exception.params, { + '__all__': [['Char: The maximum number of items is 1']] + }) + def test_block_level_validation_renders_errors(self): block = FooStreamBlock()