From 58ad6545be730ec4ec953c0ad7bb26c683c0bd40 Mon Sep 17 00:00:00 2001 From: Edwar Baron Date: Sat, 20 May 2017 23:32:30 -0500 Subject: [PATCH] Add ability to specify min/max block counts on StreamBlock --- docs/topics/streamfield.rst | 13 +++++++-- wagtail/wagtailcore/blocks/stream_block.py | 18 +++++++++++- wagtail/wagtailcore/tests/test_blocks.py | 33 ++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/topics/streamfield.rst b/docs/topics/streamfield.rst index b24674b1d8..227ea29ae4 100644 --- a/docs/topics/streamfield.rst +++ b/docs/topics/streamfield.rst @@ -441,9 +441,18 @@ Since ``StreamField`` accepts an instance of ``StreamBlock`` as a parameter, in .. code-block:: python class HomePage(Page): - carousel = StreamField(CarouselBlock()) + carousel = StreamField(CarouselBlock(max_num=10)) -``StreamBlock`` accepts ``required`` as a keyword argument or ``Meta`` property; if true (the default), at least one sub-block must be supplied. +``StreamBlock`` accepts the following options as either keyword arguments or ``Meta`` properties: + +``required`` (default: True) + If true, at least one sub-block must be supplied. + +``min_num`` + Minimum number of sub-blocks that the stream must have. + +``max_num`` + Maximum number of sub-blocks that the stream may have. .. _streamfield_personblock_example: diff --git a/wagtail/wagtailcore/blocks/stream_block.py b/wagtail/wagtailcore/blocks/stream_block.py index b48d1fe711..6d602f764f 100644 --- a/wagtail/wagtailcore/blocks/stream_block.py +++ b/wagtail/wagtailcore/blocks/stream_block.py @@ -13,6 +13,7 @@ from django.utils import six from django.utils.encoding import python_2_unicode_compatible from django.utils.html import format_html_join from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ from wagtail.wagtailcore.utils import escape_script @@ -35,9 +36,13 @@ class StreamBlockValidationError(ValidationError): class BaseStreamBlock(Block): - def __init__(self, local_blocks=None, **kwargs): + def __init__(self, local_blocks=None, min_num=None, max_num=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 + super(BaseStreamBlock, self).__init__(**kwargs) # create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks @@ -196,6 +201,17 @@ class BaseStreamBlock(Block): if self.required and len(value) == 0: non_block_errors.append(ValidationError('This field is required', code='invalid')) + # Validate that the min_num and max_num has a value + # and if it does meet the conditions of the number of components in the block + if self.min_num and self.min_num > len(value): + non_block_errors.append(ErrorList( + [_('The minimum number of items is %s' % self.min_num)] + )) + if self.max_num and self.max_num < len(value): + non_block_errors.append(ErrorList( + [_('The maximum number of items is %s' % self.max_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 94762fea57..d76b4672d8 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -2040,6 +2040,39 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase): 3: ['Enter a valid URL.'], }) + def test_min_num_validation_errors(self): + class ValidatedBlock(blocks.StreamBlock): + char = blocks.CharBlock() + url = blocks.URLBlock() + block = ValidatedBlock(min_num=1) + + value = blocks.StreamValue(block, []) + + with self.assertRaises(ValidationError) as catcher: + block.clean(value) + self.assertEqual(catcher.exception.params, { + '__all__': [['The minimum number of items is 1']] + }) + + def test_max_num_validation_errors(self): + class ValidatedBlock(blocks.StreamBlock): + char = blocks.CharBlock() + url = blocks.URLBlock() + block = ValidatedBlock(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__': [['The maximum number of items is 1']] + }) + def test_block_level_validation_renders_errors(self): block = FooStreamBlock()