From 9358e3b611dd2c26e3f3986c9a97f8cd0c9cee12 Mon Sep 17 00:00:00 2001 From: OktayAltay Date: Thu, 23 Jun 2016 11:14:28 +0200 Subject: [PATCH] Add new FloatBlock, DecimalBlock and a RegexBlock (#2737) --- CHANGELOG.txt | 1 + docs/topics/streamfield.rst | 22 ++- wagtail/wagtailcore/blocks/field_block.py | 55 +++++- wagtail/wagtailcore/tests/test_blocks.py | 199 ++++++++++++++++------ 4 files changed, 226 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7cfcdc9e26..8fe6dba7a8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -14,6 +14,7 @@ Changelog * The wagtailimages.Filter model has been removed, and converted to a Python class instead (Gagaro) * Multiple ChooserBlocks inside a StreamField are now prefetched in bulk, for improved performance (Michael van Tellingen, Roel Bruggink, Matt Westcott) * Add new EmailBlock and IntegerBlock (Oktay Altay) + * Added a new FloatBlock, DecimalBlock and a RegexBlock (Oktay Altay, Andy Babic) * Fix: Email templates and document uploader now support custom `STATICFILES_STORAGE` (Jonny Scholes) * Fix: Removed alignment options (deprecated in HTML and not rendered by Wagtail) from `TableBlock` context menu (Moritz Pfeiffer) * Fix: Fixed incorrect CSS path on ModelAdmin's "choose a parent page" view diff --git a/docs/topics/streamfield.rst b/docs/topics/streamfield.rst index c56b9160dc..5e747dde49 100644 --- a/docs/topics/streamfield.rst +++ b/docs/topics/streamfield.rst @@ -89,7 +89,6 @@ TextBlock A multi-line text input. As with ``CharBlock``, the keyword arguments ``required``, ``max_length``, ``min_length`` and ``help_text`` are accepted. - EmailBlock ~~~~~~~~~~ @@ -104,6 +103,27 @@ IntegerBlock A single-line integer input that validates that the integer is a valid whole number. The keyword arguments ``required``, ``max_length``, ``min_length`` and ``help_text`` are accepted. +FloatBlock +~~~~~~~~~~ + +``wagtail.wagtailcore.blocks.FloatBlock`` + +A single-line Float input that validates that the value is a valid floating point number. The keyword arguments ``required``, ``max_value`` and ``min_value`` are accepted. + +DecimalBlock +~~~~~~~~~~ + +``wagtail.wagtailcore.blocks.DecimalBlock`` + +A single-line decimal input that validates that the value is a valid decimal number. The keyword arguments ``required``, ``max_value``, ``min_value``, ``max_digits`` and ``decimal_places`` are accepted. + +RegexBlock +~~~~~~~~~~ + +``wagtail.wagtailcore.blocks.RegexBlock`` + +A single-line text input that validates a string against a regex expression. The regular expression used for validation must be supplied as the first argument, or as the keyword argument ``regex``. The message text used to indicate a validation error can be customised using the ``error_message`` keyword argument to pass a custom message. The keyword arguments ``regex``, ``required``, ``max_length``, ``min_length`` and ``error_message`` are accepted. + URLBlock ~~~~~~~~ diff --git a/wagtail/wagtailcore/blocks/field_block.py b/wagtail/wagtailcore/blocks/field_block.py index 8e7d42694a..cf3d378afb 100644 --- a/wagtail/wagtailcore/blocks/field_block.py +++ b/wagtail/wagtailcore/blocks/field_block.py @@ -129,6 +129,55 @@ class TextBlock(FieldBlock): icon = "pilcrow" +class FloatBlock(FieldBlock): + + def __init__(self, required=True, max_value=None, min_value=None, *args, + **kwargs): + self.field = forms.FloatField( + required=required, + max_value=max_value, + min_value=min_value, + ) + super(FloatBlock, self).__init__(*args, **kwargs) + + class Meta: + icon = "plus-inverse" + + +class DecimalBlock(FieldBlock): + + def __init__(self, required=True, max_value=None, min_value=None, + max_digits=None, decimal_places=None, *args, **kwargs): + self.field = forms.DecimalField( + required=required, + max_value=max_value, + min_value=min_value, + max_digits=max_digits, + decimal_places=decimal_places, + ) + super(DecimalBlock, self).__init__(*args, **kwargs) + + class Meta: + icon = "plus-inverse" + + +class RegexBlock(FieldBlock): + + def __init__(self, regex, required=True, max_length=None, min_length=None, + error_message=None, *args, **kwargs): + self.field = forms.RegexField( + regex=regex, + required=required, + max_length=max_length, + min_length=min_length, + error_message=error_message, + ) + super(RegexBlock, self).__init__(*args, **kwargs) + + class Meta: + icon = "code" + + class URLBlock(FieldBlock): def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs): @@ -500,8 +549,10 @@ class PageChooserBlock(ChooserBlock): # Ensure that the blocks defined here get deconstructed as wagtailcore.blocks.FooBlock # rather than wagtailcore.blocks.field.FooBlock block_classes = [ - FieldBlock, CharBlock, URLBlock, RichTextBlock, RawHTMLBlock, ChooserBlock, PageChooserBlock, - TextBlock, BooleanBlock, DateBlock, TimeBlock, DateTimeBlock, ChoiceBlock, EmailBlock, IntegerBlock, + FieldBlock, CharBlock, URLBlock, RichTextBlock, RawHTMLBlock, ChooserBlock, + PageChooserBlock, TextBlock, BooleanBlock, DateBlock, TimeBlock, + DateTimeBlock, ChoiceBlock, EmailBlock, IntegerBlock, FloatBlock, + DecimalBlock, RegexBlock ] DECONSTRUCT_ALIASES = { cls: 'wagtail.wagtailcore.blocks.%s' % cls.__name__ diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index b46f855878..34167ad345 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals import base64 import unittest +from decimal import Decimal from django import forms from django.core.exceptions import ValidationError @@ -29,54 +30,6 @@ class FooStreamBlock(blocks.StreamBlock): class TestFieldBlock(unittest.TestCase): - def test_integerfield_type(self): - block = blocks.IntegerBlock() - digit = block.value_from_form(1234) - - self.assertEqual(type(digit), int) - - def test_integerfield_render(self): - block = blocks.IntegerBlock() - digit = block.value_from_form(1234) - - self.assertEqual(digit, 1234) - - def test_integerfield_render_required_error(self): - block = blocks.IntegerBlock() - - with self.assertRaises(ValidationError): - block.clean("") - - def test_integerfield_render_max_value_validation(self): - block = blocks.IntegerBlock(max_value=20) - - with self.assertRaises(ValidationError): - block.clean(25) - - def test_integerfield_render_min_value_validation(self): - block = blocks.IntegerBlock(min_value=20) - - with self.assertRaises(ValidationError): - block.clean(10) - - def test_emailfield_render(self): - block = blocks.EmailBlock() - email = block.render("example@email.com") - - self.assertEqual(email, "example@email.com") - - def test_emailfield_render_required_error(self): - block = blocks.EmailBlock() - - with self.assertRaises(ValidationError): - block.clean("") - - def test_emailfield_format_validation(self): - block = blocks.EmailBlock() - - with self.assertRaises(ValidationError): - block.clean("example.email.com") - def test_charfield_render(self): block = blocks.CharBlock() html = block.render("Hello world!") @@ -194,6 +147,156 @@ class TestFieldBlock(unittest.TestCase): self.assertIn('animations.js', ''.join(block.all_media().render_js())) +class TestIntegerBlock(unittest.TestCase): + def test_type(self): + block = blocks.IntegerBlock() + digit = block.value_from_form(1234) + + self.assertEqual(type(digit), int) + + def test_render(self): + block = blocks.IntegerBlock() + digit = block.value_from_form(1234) + + self.assertEqual(digit, 1234) + + def test_render_required_error(self): + block = blocks.IntegerBlock() + + with self.assertRaises(ValidationError): + block.clean("") + + def test_render_max_value_validation(self): + block = blocks.IntegerBlock(max_value=20) + + with self.assertRaises(ValidationError): + block.clean(25) + + def test_render_min_value_validation(self): + block = blocks.IntegerBlock(min_value=20) + + with self.assertRaises(ValidationError): + block.clean(10) + + +class TestEmailBlock(unittest.TestCase): + def test_render(self): + block = blocks.EmailBlock() + email = block.render("example@email.com") + + self.assertEqual(email, "example@email.com") + + def test_render_required_error(self): + block = blocks.EmailBlock() + + with self.assertRaises(ValidationError): + block.clean("") + + def test_format_validation(self): + block = blocks.EmailBlock() + + with self.assertRaises(ValidationError): + block.clean("example.email.com") + + +class TestFloatBlock(TestCase): + def test_type(self): + block = blocks.FloatBlock() + block_val = block.value_from_form(float(1.63)) + self.assertEqual(type(block_val), float) + + def test_render(self): + block = blocks.FloatBlock() + test_val = float(1.63) + block_val = block.value_from_form(test_val) + self.assertEqual(block_val, test_val) + + def test_raises_required_error(self): + block = blocks.FloatBlock() + + with self.assertRaises(ValidationError): + block.clean("") + + def test_raises_max_value_validation_error(self): + block = blocks.FloatBlock(max_value=20) + + with self.assertRaises(ValidationError): + block.clean('20.01') + + def test_raises_min_value_validation_error(self): + block = blocks.FloatBlock(min_value=20) + + with self.assertRaises(ValidationError): + block.clean('19.99') + + +class TestDecimalBlock(TestCase): + def test_type(self): + block = blocks.DecimalBlock() + block_val = block.value_from_form(Decimal('1.63')) + self.assertEqual(type(block_val), Decimal) + + def test_render(self): + block = blocks.DecimalBlock() + test_val = Decimal(1.63) + block_val = block.value_from_form(test_val) + + self.assertEqual(block_val, test_val) + + def test_raises_required_error(self): + block = blocks.DecimalBlock() + + with self.assertRaises(ValidationError): + block.clean("") + + def test_raises_max_value_validation_error(self): + block = blocks.DecimalBlock(max_value=20) + + with self.assertRaises(ValidationError): + block.clean('20.01') + + def test_raises_min_value_validation_error(self): + block = blocks.DecimalBlock(min_value=20) + + with self.assertRaises(ValidationError): + block.clean('19.99') + + +class TestRegexBlock(TestCase): + + def test_render(self): + block = blocks.RegexBlock(regex=r'^[0-9]{3}$') + test_val = '123' + block_val = block.value_from_form(test_val) + + self.assertEqual(block_val, test_val) + + def test_raises_required_error(self): + block = blocks.RegexBlock(regex=r'^[0-9]{3}$') + + with self.assertRaises(ValidationError): + block.clean("") + + def test_raises_validation_error(self): + block = blocks.RegexBlock(regex=r'^[0-9]{3}$') + + with self.assertRaises(ValidationError): + block.clean("[/]") + + def test_raises_custom_error_message(self): + test_message = 'Not a valid library card number.' + block = blocks.RegexBlock(regex=r'^[0-9]{3}$', error_message=test_message) + + with self.assertRaises(ValidationError): + block.clean("[/]") + + html = block.render_form( + "[/]", + errors=ErrorList([ValidationError(test_message)])) + + self.assertIn(test_message, html) + + class TestRichTextBlock(TestCase): fixtures = ['test.json']