From 60263dfdad3f0c8b383b396e1f9271770a3e3771 Mon Sep 17 00:00:00 2001 From: Haydn Greatnews Date: Thu, 28 Mar 2019 15:32:03 +1300 Subject: [PATCH] Add testing for streamfield count restrictions passed to StreamField --- wagtail/core/tests/test_streamfield.py | 146 +++++++++++++++++- ...ountsstreammodel_minmaxcountstreammodel.py | 30 ++++ wagtail/tests/testapp/models.py | 26 ++++ 3 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 wagtail/tests/testapp/migrations/0058_blockcountsstreammodel_minmaxcountstreammodel.py diff --git a/wagtail/core/tests/test_streamfield.py b/wagtail/core/tests/test_streamfield.py index 819cc5e32e..74401338cd 100644 --- a/wagtail/core/tests/test_streamfield.py +++ b/wagtail/core/tests/test_streamfield.py @@ -8,12 +8,12 @@ from django.test import TestCase from django.utils.safestring import SafeString from wagtail.core import blocks -from wagtail.core.blocks import StreamValue +from wagtail.core.blocks import StreamBlockValidationError, StreamValue from wagtail.core.fields import StreamField from wagtail.core.rich_text import RichText from wagtail.images.models import Image from wagtail.images.tests.utils import get_test_image_file -from wagtail.tests.testapp.models import StreamModel +from wagtail.tests.testapp.models import BlockCountsStreamModel, MinMaxCountStreamModel, StreamModel class TestLazyStreamField(TestCase): @@ -255,3 +255,145 @@ class TestRequiredStreamField(TestCase): def test_blank_field_is_not_required(self): field = StreamField([('paragraph', blocks.CharBlock())], blank=True) self.assertFalse(field.stream_block.required) + + +class TestStreamFieldCountValidation(TestCase): + def setUp(self): + self.image = Image.objects.create( + title='Test image', + file=get_test_image_file()) + + self.rich_text_body = {'type': 'rich_text', 'value': '

Rich text

'} + self.image_body = {'type': 'image', 'value': self.image.pk} + self.text_body = {'type': 'text', 'value': 'Hello, World!'} + + def test_minmax_pass_to_block(self): + instance = MinMaxCountStreamModel.objects.create(body=json.dumps([])) + internal_block = instance.body.stream_block + + self.assertEqual(internal_block.meta.min_num, 2) + self.assertEqual(internal_block.meta.max_num, 5) + + def test_counts_pass_to_block(self): + instance = BlockCountsStreamModel.objects.create(body=json.dumps([])) + block_counts = instance.body.stream_block.meta.block_counts + + self.assertEqual(block_counts.get('text'), {'min_num': 1}) + self.assertEqual(block_counts.get('rich_text'), {'max_num': 1}) + self.assertEqual(block_counts.get('image'), {'min_num': 1, 'max_num': 1}) + + def test_minimum_count(self): + # Single block should fail validation + body = [self.rich_text_body] + instance = MinMaxCountStreamModel.objects.create(body=json.dumps(body)) + with self.assertRaises(StreamBlockValidationError) as catcher: + instance.body.stream_block.clean(instance.body) + self.assertEqual(catcher.exception.params, { + '__all__': ['The minimum number of items is 2'] + }) + + # 2 blocks okay + body = [self.rich_text_body, self.text_body] + instance = MinMaxCountStreamModel.objects.create(body=json.dumps(body)) + self.assertTrue(instance.body.stream_block.clean(instance.body)) + + def test_maximum_count(self): + # 5 blocks okay + body = [self.rich_text_body] * 5 + instance = MinMaxCountStreamModel.objects.create(body=json.dumps(body)) + self.assertTrue(instance.body.stream_block.clean(instance.body)) + + # 6 blocks should fail validation + body = [self.rich_text_body, self.text_body] * 3 + instance = MinMaxCountStreamModel.objects.create(body=json.dumps(body)) + with self.assertRaises(StreamBlockValidationError) as catcher: + instance.body.stream_block.clean(instance.body) + self.assertEqual(catcher.exception.params, { + '__all__': ['The maximum number of items is 5'] + }) + + def test_block_counts_minimums(self): + instance = BlockCountsStreamModel.objects.create(body=json.dumps([])) + + # Zero blocks should fail validation (requires one text, one image) + instance = BlockCountsStreamModel.objects.create(body=json.dumps([])) + with self.assertRaises(StreamBlockValidationError) as catcher: + instance.body.stream_block.clean(instance.body) + errors = list(catcher.exception.params['__all__']) + self.assertIn('This field is required.', errors) + self.assertIn('Text: The minimum number of items is 1', errors) + self.assertIn('Image: The minimum number of items is 1', errors) + self.assertEqual(len(errors), 3) + + # One plain text should fail validation + body = [self.text_body] + instance = BlockCountsStreamModel.objects.create(body=json.dumps(body)) + with self.assertRaises(StreamBlockValidationError) as catcher: + instance.body.stream_block.clean(instance.body) + self.assertEqual(catcher.exception.params, { + '__all__': ['Image: The minimum number of items is 1'] + }) + + # One text, one image should be okay + body = [self.text_body, self.image_body] + instance = BlockCountsStreamModel.objects.create(body=json.dumps(body)) + self.assertTrue(instance.body.stream_block.clean(instance.body)) + + def test_block_counts_maximums(self): + instance = BlockCountsStreamModel.objects.create(body=json.dumps([])) + + # Base is one text, one image + body = [self.text_body, self.image_body] + instance = BlockCountsStreamModel.objects.create(body=json.dumps(body)) + self.assertTrue(instance.body.stream_block.clean(instance.body)) + + # Two rich text should error + body = [self.text_body, self.image_body, self.rich_text_body, self.rich_text_body] + instance = BlockCountsStreamModel.objects.create(body=json.dumps(body)) + + with self.assertRaises(StreamBlockValidationError): + instance.body.stream_block.clean(instance.body) + + # Two images should error + body = [self.text_body, self.image_body, self.image_body] + instance = BlockCountsStreamModel.objects.create(body=json.dumps(body)) + + with self.assertRaises(StreamBlockValidationError) as catcher: + instance.body.stream_block.clean(instance.body) + self.assertEqual(catcher.exception.params, { + '__all__': ['Image: The maximum number of items is 1'] + }) + + # One text, one rich, one image should be okay + body = [self.text_body, self.image_body, self.rich_text_body] + instance = BlockCountsStreamModel.objects.create(body=json.dumps(body)) + self.assertTrue(instance.body.stream_block.clean(instance.body)) + + def test_streamfield_count_argument_precedence(self): + + class TestStreamBlock(blocks.StreamBlock): + heading = blocks.CharBlock() + paragraph = blocks.RichTextBlock() + + class Meta: + min_num = 2 + max_num = 5 + block_counts = {'heading': {'max_num': 1}} + + # args being picked up from the class definition + field = StreamField(TestStreamBlock) + self.assertEqual(field.stream_block.meta.min_num, 2) + self.assertEqual(field.stream_block.meta.max_num, 5) + self.assertEqual(field.stream_block.meta.block_counts['heading']['max_num'], 1) + + # args being overridden by StreamField + field = StreamField(TestStreamBlock, min_num=3, max_num=6, block_counts={'heading': {'max_num': 2}}) + self.assertEqual(field.stream_block.meta.min_num, 3) + self.assertEqual(field.stream_block.meta.max_num, 6) + self.assertEqual(field.stream_block.meta.block_counts['heading']['max_num'], 2) + + # passing None from StreamField should cancel limits set at the block level + field = StreamField(TestStreamBlock, min_num=None, max_num=None, block_counts=None) + self.assertEqual(field.stream_block.meta.min_num, None) + self.assertEqual(field.stream_block.meta.max_num, None) + self.assertEqual(field.stream_block.meta.block_counts, None) diff --git a/wagtail/tests/testapp/migrations/0058_blockcountsstreammodel_minmaxcountstreammodel.py b/wagtail/tests/testapp/migrations/0058_blockcountsstreammodel_minmaxcountstreammodel.py new file mode 100644 index 0000000000..7b9e935899 --- /dev/null +++ b/wagtail/tests/testapp/migrations/0058_blockcountsstreammodel_minmaxcountstreammodel.py @@ -0,0 +1,30 @@ +# Generated by Django 2.1.7 on 2019-03-28 02:30 + +from django.db import migrations, models +import wagtail.core.blocks +import wagtail.core.fields +import wagtail.images.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0057_customdocumentwithauthor'), + ] + + operations = [ + migrations.CreateModel( + name='BlockCountsStreamModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', wagtail.core.fields.StreamField([('text', wagtail.core.blocks.CharBlock()), ('rich_text', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())])), + ], + ), + migrations.CreateModel( + name='MinMaxCountStreamModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', wagtail.core.fields.StreamField([('text', wagtail.core.blocks.CharBlock()), ('rich_text', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())])), + ], + ), + ] diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py index 73c07725d6..0e64bdc6e9 100644 --- a/wagtail/tests/testapp/models.py +++ b/wagtail/tests/testapp/models.py @@ -1007,12 +1007,38 @@ class StreamModel(models.Model): ]) +class MinMaxCountStreamModel(models.Model): + body = StreamField([ + ('text', CharBlock()), + ('rich_text', RichTextBlock()), + ('image', ImageChooserBlock()), + ], + min_num=2, + max_num=5, + ) + + +class BlockCountsStreamModel(models.Model): + body = StreamField([ + ('text', CharBlock()), + ('rich_text', RichTextBlock()), + ('image', ImageChooserBlock()), + ], + block_counts={ + "text": {"min_num": 1}, + "rich_text": {"max_num": 1}, + "image": {"min_num": 1, "max_num": 1}, + } + ) + + class ExtendedImageChooserBlock(ImageChooserBlock): """ Example of Block with custom get_api_representation method. If the request has an 'extended' query param, it returns a dict of id and title, otherwise, it returns the default value. """ + def get_api_representation(self, value, context=None): image_id = super().get_api_representation(value, context=context) if 'request' in context and context['request'].query_params.get('extended', False):