浏览代码

Pass validators to FieldBlocks (#5148)

Tom Usher 6 年之前
父节点
当前提交
0f8a55a6ce
共有 6 个文件被更改,包括 189 次插入37 次删除
  1. 1 0
      CHANGELOG.txt
  2. 1 0
      CONTRIBUTORS.rst
  3. 1 0
      docs/releases/2.5.rst
  4. 26 13
      docs/topics/streamfield.rst
  5. 56 24
      wagtail/core/blocks/field_block.py
  6. 104 0
      wagtail/core/tests/test_blocks.py

+ 1 - 0
CHANGELOG.txt

@@ -21,6 +21,7 @@ Changelog
  * Added option to specify a fallback URL on `{% pageurl %}` (Arthur Holzner)
  * Add support for more rich text formats, disabled by default: `blockquote`, `superscript`, `subscript`, `strikethrough`, `code` (Md Arifin Ibne Matin)
  * Added `max_count_per_parent` option on page models to limit the number of pages of a given type that can be created under one parent page (Wesley van Lee)
+ * `StreamField` field blocks now accept a `validators` argument (Tom Usher)
  * Fix: Set `SERVER_PORT` to 443 in `Page.dummy_request()` for HTTPS sites (Sergey Fedoseev)
  * Fix: Include port number in `Host` header of `Page.dummy_request()` (Sergey Fedoseev)
  * Fix: Validation error messages in `InlinePanel` no longer count towards `max_num` when disabling the 'add' button (Todd Dembrey, Thibaud Colas)

+ 1 - 0
CONTRIBUTORS.rst

@@ -357,6 +357,7 @@ Contributors
 * Damian Grinwis
 * Wesley van Lee
 * Md Arifin Ibne Matin
+* Tom Usher
 
 Translators
 ===========

+ 1 - 0
docs/releases/2.5.rst

@@ -31,6 +31,7 @@ Other features
  * Added option to specify a fallback URL on ``{% pageurl %}`` (Arthur Holzner)
  * Add support for more rich text formats, disabled by default: ``blockquote``, ``superscript``, ``subscript``, ``strikethrough``, ``code`` (Md Arifin Ibne Matin)
  * Added ``max_count_per_parent`` option on page models to limit the number of pages of a given type that can be created under one parent page (Wesley van Lee)
+ * ``StreamField`` field blocks now accept a ``validators`` argument (Tom Usher)
 
 
 Bug fixes

+ 26 - 13
docs/topics/streamfield.rst

@@ -87,19 +87,22 @@ A single-line text input. The following keyword arguments are accepted:
 ``help_text``
   Help text to display alongside the field.
 
+``validators``
+  A list of validation functions for the field (see `Django Validators <https://docs.djangoproject.com/en/stable/ref/validators/>`__).
+
 TextBlock
 ~~~~~~~~~
 
 ``wagtail.core.blocks.TextBlock``
 
-A multi-line text input. As with ``CharBlock``, the keyword arguments ``required`` (default: True), ``max_length``, ``min_length`` and ``help_text`` are accepted.
+A multi-line text input. As with ``CharBlock``, the keyword arguments ``required`` (default: True), ``max_length``, ``min_length``, ``help_text`` and ``validators`` are accepted.
 
 EmailBlock
 ~~~~~~~~~~
 
 ``wagtail.core.blocks.EmailBlock``
 
-A single-line email input that validates that the email is a valid Email Address. The keyword arguments ``required`` (default: True) and ``help_text`` are accepted.
+A single-line email input that validates that the email is a valid Email Address. The keyword arguments ``required`` (default: True), ``help_text`` and ``validators`` are accepted.
 
 For an example of ``EmailBlock`` in use, see :ref:`streamfield_personblock_example`
 
@@ -108,7 +111,7 @@ IntegerBlock
 
 ``wagtail.core.blocks.IntegerBlock``
 
-A single-line integer input that validates that the integer is a valid whole number. The keyword arguments ``required`` (default: True), ``max_value``, ``min_value`` and ``help_text`` are accepted.
+A single-line integer input that validates that the integer is a valid whole number. The keyword arguments ``required`` (default: True), ``max_value``, ``min_value``, ``help_text`` and ``validators`` are accepted.
 
 For an example of ``IntegerBlock`` in use, see :ref:`streamfield_personblock_example`
 
@@ -117,14 +120,14 @@ FloatBlock
 
 ``wagtail.core.blocks.FloatBlock``
 
-A single-line Float input that validates that the value is a valid floating point number. The keyword arguments ``required`` (default: True), ``max_value`` and ``min_value``  are accepted.
+A single-line Float input that validates that the value is a valid floating point number. The keyword arguments ``required`` (default: True), ``max_value``, ``min_value`` and ``validators``  are accepted.
 
 DecimalBlock
 ~~~~~~~~~~~~
 
 ``wagtail.core.blocks.DecimalBlock``
 
-A single-line decimal input that validates that the value is a valid decimal number. The keyword arguments ``required`` (default: True), ``help_text``, ``max_value``, ``min_value``, ``max_digits`` and ``decimal_places`` are accepted.
+A single-line decimal input that validates that the value is a valid decimal number. The keyword arguments ``required`` (default: True), ``help_text``, ``max_value``, ``min_value``, ``max_digits``, ``decimal_places`` and ``validators`` are accepted.
 
 For an example of ``DecimalBlock`` in use, see :ref:`streamfield_personblock_example`
 
@@ -141,14 +144,14 @@ A single-line text input that validates a string against a regex expression. The
         'invalid': "Not a valid library card number."
     })
 
-The keyword arguments ``regex``, ``help_text``, ``required`` (default: True), ``max_length``, ``min_length`` and ``error_messages`` are accepted.
+The keyword arguments ``regex``, ``help_text``, ``required`` (default: True), ``max_length``, ``min_length``, ``error_messages`` and ``validators`` are accepted.
 
 URLBlock
 ~~~~~~~~
 
 ``wagtail.core.blocks.URLBlock``
 
-A single-line text input that validates that the string is a valid URL. The keyword arguments ``required`` (default: True), ``max_length``, ``min_length`` and ``help_text`` are accepted.
+A single-line text input that validates that the string is a valid URL. The keyword arguments ``required`` (default: True), ``max_length``, ``min_length``, ``help_text`` and ``validators`` are accepted.
 
 BooleanBlock
 ~~~~~~~~~~~~
@@ -162,7 +165,7 @@ DateBlock
 
 ``wagtail.core.blocks.DateBlock``
 
-A date picker. The keyword arguments ``required`` (default: True), ``help_text`` and ``format`` are accepted.
+A date picker. The keyword arguments ``required`` (default: True), ``help_text``, ``format`` and ``validators`` are accepted.
 
 ``format`` (default: None)
   Date format. This must be one of the recognised formats listed in the `DATE_INPUT_FORMATS <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DATE_INPUT_FORMATS>`_ setting. If not specified Wagtail will use ``WAGTAIL_DATE_FORMAT`` setting with fallback to '%Y-%m-%d'.
@@ -172,14 +175,14 @@ TimeBlock
 
 ``wagtail.core.blocks.TimeBlock``
 
-A time picker. The keyword arguments ``required`` (default: True) and ``help_text`` are accepted.
+A time picker. The keyword arguments ``required`` (default: True), ``help_text`` and ``validators`` are accepted.
 
 DateTimeBlock
 ~~~~~~~~~~~~~
 
 ``wagtail.core.blocks.DateTimeBlock``
 
-A combined date / time picker. The keyword arguments ``required`` (default: True), ``help_text`` and ``format`` are accepted.
+A combined date / time picker. The keyword arguments ``required`` (default: True), ``help_text``, ``format`` and ``validators`` are accepted.
 
 ``format`` (default: None)
   Date format. This must be one of the recognised formats listed in the `DATETIME_INPUT_FORMATS <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DATETIME_INPUT_FORMATS>`_ setting. If not specified Wagtail will use ``WAGTAIL_DATETIME_FORMAT`` setting with fallback to '%Y-%m-%d %H:%M'.
@@ -189,14 +192,21 @@ RichTextBlock
 
 ``wagtail.core.blocks.RichTextBlock``
 
-A WYSIWYG editor for creating formatted text including links, bold / italics etc. The keyword argument ``features`` is accepted, to specify the set of features allowed (see :ref:`rich_text_features`).
+A WYSIWYG editor for creating formatted text including links, bold / italics etc. The keyword arguments ``required`` (default: True), ``help_text``, ``validators``, ``editor`` and ``features`` are accepted.
+
+``editor`` (default: ``default``)
+  The rich text editor to be used (see :ref:`WAGTAILADMIN_RICH_TEXT_EDITORS`).
+
+``features`` (default: None)
+  Specify the set of features allowed (see :ref:`rich_text_features`).
+
 
 RawHTMLBlock
 ~~~~~~~~~~~~
 
 ``wagtail.core.blocks.RawHTMLBlock``
 
-A text area for entering raw HTML which will be rendered unescaped in the page output. The keyword arguments ``required`` (default: True), ``max_length``, ``min_length`` and ``help_text`` are accepted.
+A text area for entering raw HTML which will be rendered unescaped in the page output. The keyword arguments ``required`` (default: True), ``max_length``, ``min_length``, ``help_text`` and ``validators`` are accepted.
 
 .. WARNING::
    When this block is in use, there is nothing to prevent editors from inserting malicious scripts into the page, including scripts that would allow the editor to acquire administrator privileges when another administrator views the page. Do not use this block unless your editors are fully trusted.
@@ -206,7 +216,7 @@ BlockQuoteBlock
 
 ``wagtail.core.blocks.BlockQuoteBlock``
 
-A text field, the contents of which will be wrapped in an HTML `<blockquote>` tag pair. The keyword arguments ``required`` (default: True), ``max_length``, ``min_length`` and ``help_text`` are accepted.
+A text field, the contents of which will be wrapped in an HTML `<blockquote>` tag pair. The keyword arguments ``required`` (default: True), ``max_length``, ``min_length``, ``help_text`` and ``validators`` are accepted.
 
 
 ChoiceBlock
@@ -225,6 +235,9 @@ A dropdown select box for choosing from a list of choices. The following keyword
 ``help_text``
   Help text to display alongside the field.
 
+``validators``
+  A list of validation functions for the field (see `Django Validators <https://docs.djangoproject.com/en/stable/ref/validators/>`__).
+
 ``ChoiceBlock`` can also be subclassed to produce a reusable block with the same list of choices everywhere it is used. For example, a block definition such as:
 
 .. code-block:: python

+ 56 - 24
wagtail/core/blocks/field_block.py

@@ -96,14 +96,15 @@ class FieldBlock(Block):
 
 class CharBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs):
+    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, validators=(), **kwargs):
         # CharField's 'label' and 'initial' parameters are not exposed, as Block handles that functionality natively
         # (via 'label' and 'default')
         self.field = forms.CharField(
             required=required,
             help_text=help_text,
             max_length=max_length,
-            min_length=min_length
+            min_length=min_length,
+            validators=validators,
         )
         super().__init__(**kwargs)
 
@@ -113,12 +114,13 @@ class CharBlock(FieldBlock):
 
 class TextBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, rows=1, max_length=None, min_length=None, **kwargs):
+    def __init__(self, required=True, help_text=None, rows=1, max_length=None, min_length=None, validators=(), **kwargs):
         self.field_options = {
             'required': required,
             'help_text': help_text,
             'max_length': max_length,
-            'min_length': min_length
+            'min_length': min_length,
+            'validators': validators,
         }
         self.rows = rows
         super().__init__(**kwargs)
@@ -151,12 +153,13 @@ class BlockQuoteBlock(TextBlock):
 
 class FloatBlock(FieldBlock):
 
-    def __init__(self, required=True, max_value=None, min_value=None, *args,
+    def __init__(self, required=True, max_value=None, min_value=None, validators=(), *args,
                  **kwargs):
         self.field = forms.FloatField(
             required=required,
             max_value=max_value,
             min_value=min_value,
+            validators=validators,
         )
         super().__init__(*args, **kwargs)
 
@@ -167,7 +170,7 @@ class FloatBlock(FieldBlock):
 class DecimalBlock(FieldBlock):
 
     def __init__(self, required=True, help_text=None, max_value=None, min_value=None,
-                 max_digits=None, decimal_places=None, *args, **kwargs):
+                 max_digits=None, decimal_places=None, validators=(), *args, **kwargs):
         self.field = forms.DecimalField(
             required=required,
             help_text=help_text,
@@ -175,6 +178,7 @@ class DecimalBlock(FieldBlock):
             min_value=min_value,
             max_digits=max_digits,
             decimal_places=decimal_places,
+            validators=validators,
         )
         super().__init__(*args, **kwargs)
 
@@ -185,7 +189,7 @@ class DecimalBlock(FieldBlock):
 class RegexBlock(FieldBlock):
 
     def __init__(self, regex, required=True, help_text=None, max_length=None, min_length=None,
-                 error_messages=None, *args, **kwargs):
+                 error_messages=None, validators=(), *args, **kwargs):
         self.field = forms.RegexField(
             regex=regex,
             required=required,
@@ -193,6 +197,7 @@ class RegexBlock(FieldBlock):
             max_length=max_length,
             min_length=min_length,
             error_messages=error_messages,
+            validators=validators,
         )
         super().__init__(*args, **kwargs)
 
@@ -202,12 +207,13 @@ class RegexBlock(FieldBlock):
 
 class URLBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs):
+    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, validators=(), **kwargs):
         self.field = forms.URLField(
             required=required,
             help_text=help_text,
             max_length=max_length,
-            min_length=min_length
+            min_length=min_length,
+            validators=validators,
         )
         super().__init__(**kwargs)
 
@@ -231,8 +237,12 @@ class BooleanBlock(FieldBlock):
 
 class DateBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, format=None, **kwargs):
-        self.field_options = {'required': required, 'help_text': help_text}
+    def __init__(self, required=True, help_text=None, format=None, validators=(), **kwargs):
+        self.field_options = {
+            'required': required,
+            'help_text': help_text,
+            'validators': validators,
+        }
         try:
             self.field_options['input_formats'] = kwargs.pop('input_formats')
         except KeyError:
@@ -264,8 +274,12 @@ class DateBlock(FieldBlock):
 
 class TimeBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, **kwargs):
-        self.field_options = {'required': required, 'help_text': help_text}
+    def __init__(self, required=True, help_text=None, validators=(), **kwargs):
+        self.field_options = {
+            'required': required,
+            'help_text': help_text,
+            'validators': validators
+        }
         super().__init__(**kwargs)
 
     @cached_property
@@ -287,8 +301,12 @@ class TimeBlock(FieldBlock):
 
 class DateTimeBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, format=None, **kwargs):
-        self.field_options = {'required': required, 'help_text': help_text}
+    def __init__(self, required=True, help_text=None, format=None, validators=(), **kwargs):
+        self.field_options = {
+            'required': required,
+            'help_text': help_text,
+            'validators': validators,
+        }
         self.format = format
         super().__init__(**kwargs)
 
@@ -312,10 +330,11 @@ class DateTimeBlock(FieldBlock):
 
 
 class EmailBlock(FieldBlock):
-    def __init__(self, required=True, help_text=None, **kwargs):
+    def __init__(self, required=True, help_text=None, validators=(), **kwargs):
         self.field = forms.EmailField(
             required=required,
             help_text=help_text,
+            validators=validators,
         )
         super().__init__(**kwargs)
 
@@ -326,12 +345,13 @@ class EmailBlock(FieldBlock):
 class IntegerBlock(FieldBlock):
 
     def __init__(self, required=True, help_text=None, min_value=None,
-                 max_value=None, **kwargs):
+                 max_value=None, validators=(), **kwargs):
         self.field = forms.IntegerField(
             required=required,
             help_text=help_text,
             min_value=min_value,
-            max_value=max_value
+            max_value=max_value,
+            validators=validators,
         )
         super().__init__(**kwargs)
 
@@ -343,7 +363,7 @@ class ChoiceBlock(FieldBlock):
 
     choices = ()
 
-    def __init__(self, choices=None, default=None, required=True, help_text=None, **kwargs):
+    def __init__(self, choices=None, default=None, required=True, help_text=None, validators=(), **kwargs):
         if choices is None:
             # no choices specified, so pick up the choice defined at the class level
             choices = self.choices
@@ -375,7 +395,12 @@ class ChoiceBlock(FieldBlock):
         # If we have a default choice and the field is required, we don't need to add a blank option.
         callable_choices = self.get_callable_choices(choices, blank_choice=not(default and required))
 
-        self.field = forms.ChoiceField(choices=callable_choices, required=required, help_text=help_text)
+        self.field = forms.ChoiceField(
+            choices=callable_choices,
+            required=required,
+            help_text=help_text,
+            validators=validators,
+        )
         super().__init__(default=default, **kwargs)
 
     def get_callable_choices(self, choices, blank_choice=True):
@@ -449,8 +474,12 @@ class ChoiceBlock(FieldBlock):
 
 class RichTextBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, editor='default', features=None, **kwargs):
-        self.field_options = {'required': required, 'help_text': help_text}
+    def __init__(self, required=True, help_text=None, editor='default', features=None, validators=(), **kwargs):
+        self.field_options = {
+            'required': required,
+            'help_text': help_text,
+            'validators': validators,
+        }
         self.editor = editor
         self.features = features
         super().__init__(**kwargs)
@@ -497,9 +526,10 @@ class RichTextBlock(FieldBlock):
 
 class RawHTMLBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs):
+    def __init__(self, required=True, help_text=None, max_length=None, min_length=None, validators=(), **kwargs):
         self.field = forms.CharField(
             required=required, help_text=help_text, max_length=max_length, min_length=min_length,
+            validators=validators,
             widget=forms.Textarea)
         super().__init__(**kwargs)
 
@@ -527,9 +557,10 @@ class RawHTMLBlock(FieldBlock):
 
 class ChooserBlock(FieldBlock):
 
-    def __init__(self, required=True, help_text=None, **kwargs):
+    def __init__(self, required=True, help_text=None, validators=(), **kwargs):
         self._required = required
         self._help_text = help_text
+        self._validators = validators
         super().__init__(**kwargs)
 
     """Abstract superclass for fields that implement a chooser interface (page, image, snippet etc)"""
@@ -537,6 +568,7 @@ class ChooserBlock(FieldBlock):
     def field(self):
         return forms.ModelChoiceField(
             queryset=self.target_model.objects.all(), widget=self.widget, required=self._required,
+            validators=self._validators,
             help_text=self._help_text)
 
     def to_python(self, value):

+ 104 - 0
wagtail/core/tests/test_blocks.py

@@ -90,6 +90,16 @@ class TestFieldBlock(WagtailTestUtils, SimpleTestCase):
 
         self.assertEqual(content, ["Hello world!"])
 
+    def test_charfield_with_validator(self):
+        def validate_is_foo(value):
+            if value != 'foo':
+                raise ValidationError("Value must be 'foo'")
+
+        block = blocks.CharBlock(validators=[validate_is_foo])
+
+        with self.assertRaises(ValidationError):
+            block.clean("bar")
+
     def test_choicefield_render(self):
         class ChoiceBlock(blocks.FieldBlock):
             field = forms.ChoiceField(choices=(
@@ -259,6 +269,16 @@ class TestIntegerBlock(unittest.TestCase):
         with self.assertRaises(ValidationError):
             block.clean(10)
 
+    def test_render_with_validator(self):
+        def validate_is_even(value):
+            if value % 2 > 0:
+                raise ValidationError("Value must be even")
+
+        block = blocks.IntegerBlock(validators=[validate_is_even])
+
+        with self.assertRaises(ValidationError):
+            block.clean(3)
+
 
 class TestEmailBlock(unittest.TestCase):
     def test_render(self):
@@ -279,6 +299,16 @@ class TestEmailBlock(unittest.TestCase):
         with self.assertRaises(ValidationError):
             block.clean("example.email.com")
 
+    def test_render_with_validator(self):
+        def validate_is_example_domain(value):
+            if not value.endswith('@example.com'):
+                raise ValidationError("E-mail address must end in @example.com")
+
+        block = blocks.EmailBlock(validators=[validate_is_example_domain])
+
+        with self.assertRaises(ValidationError):
+            block.clean("foo@example.net")
+
 
 class TestBlockQuoteBlock(unittest.TestCase):
     def test_render(self):
@@ -287,6 +317,16 @@ class TestBlockQuoteBlock(unittest.TestCase):
 
         self.assertEqual(quote, "<blockquote>Now is the time...</blockquote>")
 
+    def test_render_with_validator(self):
+        def validate_is_proper_story(value):
+            if not value.startswith('Once upon a time'):
+                raise ValidationError("Value must be a proper story")
+
+        block = blocks.BlockQuoteBlock(validators=[validate_is_proper_story])
+
+        with self.assertRaises(ValidationError):
+            block.clean("A long, long time ago")
+
 
 class TestFloatBlock(TestCase):
     def test_type(self):
@@ -318,6 +358,16 @@ class TestFloatBlock(TestCase):
         with self.assertRaises(ValidationError):
             block.clean('19.99')
 
+    def test_render_with_validator(self):
+        def validate_is_even(value):
+            if value % 2 > 0:
+                raise ValidationError("Value must be even")
+
+        block = blocks.FloatBlock(validators=[validate_is_even])
+
+        with self.assertRaises(ValidationError):
+            block.clean('3.0')
+
 
 class TestDecimalBlock(TestCase):
     def test_type(self):
@@ -350,6 +400,16 @@ class TestDecimalBlock(TestCase):
         with self.assertRaises(ValidationError):
             block.clean('19.99')
 
+    def test_render_with_validator(self):
+        def validate_is_even(value):
+            if value % 2 > 0:
+                raise ValidationError("Value must be even")
+
+        block = blocks.DecimalBlock(validators=[validate_is_even])
+
+        with self.assertRaises(ValidationError):
+            block.clean('3.0')
+
 
 class TestRegexBlock(TestCase):
 
@@ -404,6 +464,16 @@ class TestRegexBlock(TestCase):
 
         self.assertIn(test_message, html)
 
+    def test_render_with_validator(self):
+        def validate_is_foo(value):
+            if value != 'foo':
+                raise ValidationError("Value must be 'foo'")
+
+        block = blocks.RegexBlock(regex=r'^.*$', validators=[validate_is_foo])
+
+        with self.assertRaises(ValidationError):
+            block.clean('bar')
+
 
 class TestRichTextBlock(TestCase):
     fixtures = ['test.json']
@@ -469,6 +539,16 @@ class TestRichTextBlock(TestCase):
         self.assertIsInstance(result, RichText)
         self.assertEqual(result.source, '')
 
+    def test_render_with_validator(self):
+        def validate_contains_foo(value):
+            if 'foo' not in value:
+                raise ValidationError("Value must contain 'foo'")
+
+        block = blocks.RichTextBlock(validators=[validate_contains_foo])
+
+        with self.assertRaises(ValidationError):
+            block.clean(RichText('<p>bar</p>'))
+
 
 class TestChoiceBlock(WagtailTestUtils, SimpleTestCase):
     def setUp(self):
@@ -753,6 +833,20 @@ class TestChoiceBlock(WagtailTestUtils, SimpleTestCase):
             )
         )
 
+    def test_render_with_validator(self):
+        choices = [
+            ('tea', 'Tea'),
+            ('coffee', 'Coffee'),
+        ]
+
+        def validate_tea_is_selected(value):
+            raise ValidationError("You must select 'tea'")
+
+        block = blocks.ChoiceBlock(choices=choices, validators=[validate_tea_is_selected])
+
+        with self.assertRaises(ValidationError):
+            block.clean('coffee')
+
 
 class TestRawHTMLBlock(unittest.TestCase):
     def test_get_default_with_fallback_value(self):
@@ -831,6 +925,16 @@ class TestRawHTMLBlock(unittest.TestCase):
         self.assertEqual(result, '')
         self.assertIsInstance(result, SafeData)
 
+    def test_render_with_validator(self):
+        def validate_contains_foo(value):
+            if 'foo' not in value:
+                raise ValidationError("Value must contain 'foo'")
+
+        block = blocks.RawHTMLBlock(validators=[validate_contains_foo])
+
+        with self.assertRaises(ValidationError):
+            block.clean(mark_safe('<p>bar</p>'))
+
 
 class TestMeta(unittest.TestCase):
     def test_set_template_with_meta(self):