import base64
import collections
import copy
import json
import unittest
from decimal import Decimal
# non-standard import name for gettext_lazy, to prevent strings from being picked up for translation
from django import forms
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.utils import ErrorList
from django.template.loader import render_to_string
from django.test import SimpleTestCase, TestCase
from django.utils.safestring import SafeData, mark_safe
from django.utils.translation import gettext_lazy as _
from wagtail import blocks
from wagtail.blocks.base import get_error_json_data
from wagtail.blocks.definition_lookup import BlockDefinitionLookup
from wagtail.blocks.field_block import FieldBlockAdapter
from wagtail.blocks.list_block import ListBlockAdapter, ListBlockValidationError
from wagtail.blocks.static_block import StaticBlockAdapter
from wagtail.blocks.stream_block import StreamBlockAdapter, StreamBlockValidationError
from wagtail.blocks.struct_block import StructBlockAdapter, StructBlockValidationError
from wagtail.models import Page
from wagtail.rich_text import RichText
from wagtail.test.testapp.blocks import LinkBlock as CustomLinkBlock
from wagtail.test.testapp.blocks import SectionBlock
from wagtail.test.testapp.models import EventPage, SimplePage
from wagtail.test.utils import WagtailTestUtils
from wagtail.utils.deprecation import RemovedInWagtail70Warning
class FooStreamBlock(blocks.StreamBlock):
text = blocks.CharBlock()
error = 'At least one block must say "foo"'
def clean(self, value):
value = super().clean(value)
if not any(block.value == "foo" for block in value):
raise blocks.StreamBlockValidationError(
non_block_errors=ErrorList([self.error])
)
return value
class ContextCharBlock(blocks.CharBlock):
def get_context(self, value, parent_context=None):
value = str(value).upper()
return super(blocks.CharBlock, self).get_context(value, parent_context)
class TestBlock(SimpleTestCase):
def test_normalize(self):
"""The base normalize implementation should return its argument unchanged"""
obj = object()
self.assertIs(blocks.Block().normalize(obj), obj)
class TestFieldBlock(WagtailTestUtils, SimpleTestCase):
def test_charfield_render(self):
block = blocks.CharBlock()
html = block.render("Hello world!")
self.assertEqual(html, "Hello world!")
def test_charfield_render_with_template(self):
block = blocks.CharBlock(template="tests/blocks/heading_block.html")
html = block.render("Hello world!")
self.assertEqual(html, "
Hello world!
")
def test_charblock_adapter(self):
block = blocks.CharBlock(help_text="Some helpful text")
block.set_name("test_block")
js_args = FieldBlockAdapter().js_args(block)
self.assertEqual(js_args[0], "test_block")
self.assertIsInstance(js_args[1], forms.TextInput)
self.assertEqual(
js_args[2],
{
"label": "Test block",
"helpText": "Some helpful text",
"required": True,
"icon": "placeholder",
"classname": "w-field w-field--char_field w-field--text_input",
"showAddCommentButton": True,
"strings": {"ADD_COMMENT": "Add Comment"},
},
)
def test_charblock_adapter_form_classname(self):
"""
Meta data test for FormField; this checks if both the meta values
form_classname and classname are accepted and are rendered
in the form
"""
block = blocks.CharBlock(form_classname="special-char-formclassname")
block.set_name("test_block")
js_args = FieldBlockAdapter().js_args(block)
self.assertIn(" special-char-formclassname", js_args[2]["classname"])
# Checks if it is backward compatible with classname
block_with_classname = blocks.CharBlock(classname="special-char-classname")
block_with_classname.set_name("test_block")
js_args = FieldBlockAdapter().js_args(block_with_classname)
self.assertIn(" special-char-classname", js_args[2]["classname"])
def test_charfield_render_with_template_with_extra_context(self):
block = ContextCharBlock(template="tests/blocks/heading_block.html")
html = block.render(
"Bonjour le monde!",
context={
"language": "fr",
},
)
self.assertEqual(html, '
BONJOUR LE MONDE!
')
def test_charfield_get_form_state(self):
block = blocks.CharBlock()
form_state = block.get_form_state("Hello world!")
self.assertEqual(form_state, "Hello world!")
def test_charfield_searchable_content(self):
block = blocks.CharBlock()
content = block.get_searchable_content("Hello world!")
self.assertEqual(content, ["Hello world!"])
def test_search_index_searchable_content(self):
block = blocks.CharBlock(search_index=False)
content = block.get_searchable_content("Hello world!")
self.assertEqual(content, [])
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=(
("choice-1", "Choice 1"),
("choice-2", "Choice 2"),
)
)
block = ChoiceBlock()
html = block.render("choice-2")
self.assertEqual(html, "choice-2")
def test_adapt_custom_choicefield(self):
class ChoiceBlock(blocks.FieldBlock):
field = forms.ChoiceField(
choices=(
("choice-1", "Choice 1"),
("choice-2", "Choice 2"),
)
)
block = ChoiceBlock()
block.set_name("test_choiceblock")
js_args = FieldBlockAdapter().js_args(block)
self.assertEqual(js_args[0], "test_choiceblock")
self.assertIsInstance(js_args[1], forms.Select)
self.assertEqual(
js_args[1].choices,
[
("choice-1", "Choice 1"),
("choice-2", "Choice 2"),
],
)
self.assertEqual(
js_args[2],
{
"label": "Test choiceblock",
"required": True,
"icon": "placeholder",
"classname": "w-field w-field--choice_field w-field--select",
"showAddCommentButton": True,
"strings": {"ADD_COMMENT": "Add Comment"},
},
)
def test_searchable_content(self):
"""
FieldBlock should not return anything for `get_searchable_content` by
default. Subclasses are free to override it and provide relevant
content.
"""
class CustomBlock(blocks.FieldBlock):
field = forms.CharField(required=True)
block = CustomBlock()
self.assertEqual(block.get_searchable_content("foo bar"), [])
def test_form_handling_is_independent_of_serialisation(self):
class Base64EncodingCharBlock(blocks.CharBlock):
"""A CharBlock with a deliberately perverse JSON (de)serialisation format
so that it visibly blows up if we call to_python / get_prep_value where we shouldn't
"""
def to_python(self, jsonish_value):
# decode as base64 on the way out of the JSON serialisation
return base64.b64decode(jsonish_value)
def get_prep_value(self, native_value):
# encode as base64 on the way into the JSON serialisation
return base64.b64encode(native_value)
block = Base64EncodingCharBlock()
form_state = block.get_form_state("hello world")
self.assertEqual(form_state, "hello world")
def test_prepare_value_called(self):
"""
Check that Field.prepare_value is called before sending the value to
the widget for rendering.
Actual real-world use case: A Youtube field that produces YoutubeVideo
instances from IDs, but videos are entered using their full URLs.
"""
class PrefixWrapper:
prefix = "http://example.com/"
def __init__(self, value):
self.value = value
def with_prefix(self):
return self.prefix + self.value
@classmethod
def from_prefixed(cls, value):
if not value.startswith(cls.prefix):
raise ValueError
return cls(value[len(cls.prefix) :])
def __eq__(self, other):
return self.value == other.value
class PrefixField(forms.Field):
def clean(self, value):
value = super().clean(value)
return PrefixWrapper.from_prefixed(value)
def prepare_value(self, value):
return value.with_prefix()
class PrefixedBlock(blocks.FieldBlock):
def __init__(self, required=True, help_text="", **kwargs):
super().__init__(**kwargs)
self.field = PrefixField(required=required, help_text=help_text)
block = PrefixedBlock()
# Check that the form value is serialized with a prefix correctly
value = PrefixWrapper("foo")
form_state = block.get_form_state(value)
self.assertEqual(form_state, "http://example.com/foo")
# Check that the value was coerced back to a PrefixValue
data = {"url": "http://example.com/bar"}
new_value = block.clean(block.value_from_datadict(data, {}, "url"))
self.assertEqual(new_value, PrefixWrapper("bar"))
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)
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):
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")
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 TestBooleanBlock(unittest.TestCase):
def test_get_form_state(self):
block = blocks.BooleanBlock(required=False)
form_state = block.get_form_state(True)
self.assertIs(form_state, True)
form_state = block.get_form_state(False)
self.assertIs(form_state, False)
class TestBlockQuoteBlock(unittest.TestCase):
def test_render(self):
block = blocks.BlockQuoteBlock()
quote = block.render("Now is the time...")
self.assertEqual(quote, "
Now is the time...
")
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):
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")
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):
block = blocks.DecimalBlock()
block_val = block.value_from_form(Decimal("1.63"))
self.assertEqual(type(block_val), Decimal)
def test_type_to_python(self):
block = blocks.DecimalBlock()
block_val = block.to_python(
"1.63"
) # decimals get saved as string in JSON field
self.assertEqual(type(block_val), Decimal)
def test_type_to_python_decimal_none_value(self):
block = blocks.DecimalBlock()
block_val = block.to_python(None)
self.assertIsNone(block_val)
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")
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")
def test_round_trip_to_db_preserves_type(self):
block = blocks.DecimalBlock()
original_value = Decimal(1.63)
db_value = json.dumps(
block.get_prep_value(original_value), cls=DjangoJSONEncoder
)
restored_value = block.to_python(json.loads(db_value))
self.assertEqual(type(restored_value), Decimal)
self.assertEqual(original_value, restored_value)
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) as context:
block.clean("")
self.assertIn("This field is required.", context.exception.messages)
def test_raises_custom_required_error(self):
test_message = "Oops, you missed a bit."
block = blocks.RegexBlock(
regex=r"^[0-9]{3}$",
error_messages={
"required": test_message,
},
)
with self.assertRaises(ValidationError) as context:
block.clean("")
self.assertIn(test_message, context.exception.messages)
def test_raises_validation_error(self):
block = blocks.RegexBlock(regex=r"^[0-9]{3}$")
with self.assertRaises(ValidationError) as context:
block.clean("[/]")
self.assertIn("Enter a valid value.", context.exception.messages)
def test_raises_custom_error_message(self):
test_message = "Not a valid library card number."
block = blocks.RegexBlock(
regex=r"^[0-9]{3}$", error_messages={"invalid": test_message}
)
with self.assertRaises(ValidationError) as context:
block.clean("[/]")
self.assertIn(test_message, context.exception.messages)
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"]
def test_get_default_with_fallback_value(self):
default_value = blocks.RichTextBlock().get_default()
self.assertIsInstance(default_value, RichText)
self.assertEqual(default_value.source, "")
def test_get_default_with_default_none(self):
default_value = blocks.RichTextBlock(default=None).get_default()
self.assertIsInstance(default_value, RichText)
self.assertEqual(default_value.source, "")
def test_get_default_with_empty_string(self):
default_value = blocks.RichTextBlock(default="").get_default()
self.assertIsInstance(default_value, RichText)
self.assertEqual(default_value.source, "")
def test_get_default_with_nonempty_string(self):
default_value = blocks.RichTextBlock(default="
Our Santa pet Wagtail has some cool stuff in store for you all!
"
)
result = block.get_searchable_content(value)
self.assertEqual(
result,
[
"Merry Christmas! & a happy new year \n"
"Our Santa pet Wagtail has some cool stuff in store for you all!"
],
)
def test_search_index_get_searchable_content(self):
block = blocks.RichTextBlock(search_index=False)
value = RichText(
'
"))
class TestMeta(unittest.TestCase):
def test_set_template_with_meta(self):
class HeadingBlock(blocks.CharBlock):
class Meta:
template = "heading.html"
block = HeadingBlock()
self.assertEqual(block.meta.template, "heading.html")
def test_set_template_with_constructor(self):
block = blocks.CharBlock(template="heading.html")
self.assertEqual(block.meta.template, "heading.html")
def test_set_template_with_constructor_overrides_meta(self):
class HeadingBlock(blocks.CharBlock):
class Meta:
template = "heading.html"
block = HeadingBlock(template="subheading.html")
self.assertEqual(block.meta.template, "subheading.html")
def test_meta_nested_inheritance(self):
"""
Check that having a multi-level inheritance chain works
"""
class HeadingBlock(blocks.CharBlock):
class Meta:
template = "heading.html"
test = "Foo"
class SubHeadingBlock(HeadingBlock):
class Meta:
template = "subheading.html"
block = SubHeadingBlock()
self.assertEqual(block.meta.template, "subheading.html")
self.assertEqual(block.meta.test, "Foo")
def test_meta_multi_inheritance(self):
"""
Check that multi-inheritance and Meta classes work together
"""
class LeftBlock(blocks.CharBlock):
class Meta:
template = "template.html"
clash = "the band"
label = "Left block"
class RightBlock(blocks.CharBlock):
class Meta:
default = "hello"
clash = "the album"
label = "Right block"
class ChildBlock(LeftBlock, RightBlock):
class Meta:
label = "Child block"
block = ChildBlock()
# These should be directly inherited from the LeftBlock/RightBlock
self.assertEqual(block.meta.template, "template.html")
self.assertEqual(block.meta.default, "hello")
# This should be inherited from the LeftBlock, solving the collision,
# as LeftBlock comes first
self.assertEqual(block.meta.clash, "the band")
# This should come from ChildBlock itself, ignoring the label on
# LeftBlock/RightBlock
self.assertEqual(block.meta.label, "Child block")
class TestStructBlock(SimpleTestCase):
def test_initialisation(self):
block = blocks.StructBlock(
[
("title", blocks.CharBlock()),
("link", blocks.URLBlock()),
]
)
self.assertEqual(list(block.child_blocks.keys()), ["title", "link"])
def test_initialisation_from_subclass(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = LinkBlock()
self.assertEqual(list(block.child_blocks.keys()), ["title", "link"])
def test_initialisation_from_subclass_with_extra(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = LinkBlock([("classname", blocks.CharBlock())])
self.assertEqual(
list(block.child_blocks.keys()), ["title", "link", "classname"]
)
def test_initialisation_with_multiple_subclassses(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
class StyledLinkBlock(LinkBlock):
classname = blocks.CharBlock()
block = StyledLinkBlock()
self.assertEqual(
list(block.child_blocks.keys()), ["title", "link", "classname"]
)
def test_initialisation_with_mixins(self):
"""
The order of fields of classes with multiple parent classes is slightly
surprising at first. Child fields are inherited in a bottom-up order,
by traversing the MRO in reverse. In the example below,
``StyledLinkBlock`` will have an MRO of::
[StyledLinkBlock, StylingMixin, LinkBlock, StructBlock, ...]
This will result in ``classname`` appearing *after* ``title`` and
``link`` in ``StyleLinkBlock`.child_blocks`, even though
``StylingMixin`` appeared before ``LinkBlock``.
"""
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
class StylingMixin(blocks.StructBlock):
classname = blocks.CharBlock()
class StyledLinkBlock(StylingMixin, LinkBlock):
source = blocks.CharBlock()
block = StyledLinkBlock()
self.assertEqual(
list(block.child_blocks.keys()), ["title", "link", "classname", "source"]
)
def test_render(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = LinkBlock()
html = block.render(
block.to_python(
{
"title": "Wagtail site",
"link": "http://www.wagtail.org",
}
)
)
expected_html = "\n".join(
[
"
",
"
title
",
"
Wagtail site
",
"
link
",
"
http://www.wagtail.org
",
"
",
]
)
self.assertHTMLEqual(html, expected_html)
def test_get_api_representation_calls_same_method_on_fields_with_context(self):
"""
The get_api_representation method of a StructBlock should invoke
the block's get_api_representation method on each field and the
context should be passed on.
"""
class ContextBlock(blocks.CharBlock):
def get_api_representation(self, value, context=None):
return context[value]
class AuthorBlock(blocks.StructBlock):
language = ContextBlock()
author = ContextBlock()
block = AuthorBlock()
api_representation = block.get_api_representation(
{
"language": "en",
"author": "wagtail",
},
context={"en": "English", "wagtail": "Wagtail!"},
)
self.assertDictEqual(
api_representation, {"language": "English", "author": "Wagtail!"}
)
def test_render_unknown_field(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = LinkBlock()
html = block.render(
block.to_python(
{
"title": "Wagtail site",
"link": "http://www.wagtail.org",
"image": 10,
}
)
)
self.assertIn("
title
", html)
self.assertIn("
Wagtail site
", html)
self.assertIn("
link
", html)
self.assertIn("
http://www.wagtail.org
", html)
# Don't render the extra item
self.assertNotIn("
image
", html)
def test_render_bound_block(self):
# the string representation of a bound block should be the value as rendered by
# the associated block
class SectionBlock(blocks.StructBlock):
title = blocks.CharBlock()
body = blocks.RichTextBlock()
block = SectionBlock()
struct_value = block.to_python(
{
"title": "hello",
"body": "world",
}
)
body_bound_block = struct_value.bound_blocks["body"]
expected = "world"
self.assertEqual(str(body_bound_block), expected)
def test_get_form_context(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = LinkBlock()
context = block.get_form_context(
block.to_python(
{
"title": "Wagtail site",
"link": "http://www.wagtail.org",
}
),
prefix="mylink",
)
self.assertIsInstance(context["children"], collections.OrderedDict)
self.assertEqual(len(context["children"]), 2)
self.assertIsInstance(context["children"]["title"], blocks.BoundBlock)
self.assertEqual(context["children"]["title"].value, "Wagtail site")
self.assertIsInstance(context["children"]["link"], blocks.BoundBlock)
self.assertEqual(context["children"]["link"].value, "http://www.wagtail.org")
self.assertEqual(context["block_definition"], block)
self.assertEqual(context["prefix"], "mylink")
def test_adapt(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock(required=False)
link = blocks.URLBlock(required=False)
block = LinkBlock()
block.set_name("test_structblock")
js_args = StructBlockAdapter().js_args(block)
self.assertEqual(js_args[0], "test_structblock")
self.assertEqual(
js_args[2],
{
"label": "Test structblock",
"required": False,
"icon": "placeholder",
"classname": "struct-block",
},
)
self.assertEqual(len(js_args[1]), 2)
title_field, link_field = js_args[1]
self.assertEqual(title_field, block.child_blocks["title"])
self.assertEqual(link_field, block.child_blocks["link"])
def test_adapt_with_form_template(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock(required=False)
link = blocks.URLBlock(required=False)
class Meta:
form_template = "tests/block_forms/struct_block_form_template.html"
block = LinkBlock()
block.set_name("test_structblock")
js_args = StructBlockAdapter().js_args(block)
self.assertEqual(
js_args[2],
{
"label": "Test structblock",
"required": False,
"icon": "placeholder",
"classname": "struct-block",
"formTemplate": "
Hello
",
},
)
def test_adapt_with_form_template_jinja(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock(required=False)
link = blocks.URLBlock(required=False)
class Meta:
form_template = "tests/jinja2/struct_block_form_template.html"
block = LinkBlock()
block.set_name("test_structblock")
js_args = StructBlockAdapter().js_args(block)
self.assertEqual(
js_args[2],
{
"label": "Test structblock",
"required": False,
"icon": "placeholder",
"classname": "struct-block",
"formTemplate": "
Hello
",
},
)
def test_get_default(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock(default="Torchbox")
link = blocks.URLBlock(default="http://www.torchbox.com")
block = LinkBlock()
default_val = block.get_default()
self.assertEqual(default_val.get("title"), "Torchbox")
def test_adapt_with_help_text_on_meta(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
class Meta:
help_text = "Self-promotion is encouraged"
block = LinkBlock()
block.set_name("test_structblock")
js_args = StructBlockAdapter().js_args(block)
self.assertEqual(
js_args[2],
{
"label": "Test structblock",
"required": False,
"icon": "placeholder",
"classname": "struct-block",
"helpIcon": (
''
),
"helpText": "Self-promotion is encouraged",
},
)
def test_adapt_with_help_text_as_argument(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = LinkBlock(help_text="Self-promotion is encouraged")
block.set_name("test_structblock")
js_args = StructBlockAdapter().js_args(block)
self.assertEqual(
js_args[2],
{
"label": "Test structblock",
"required": False,
"icon": "placeholder",
"classname": "struct-block",
"helpIcon": (
''
),
"helpText": "Self-promotion is encouraged",
},
)
def test_searchable_content(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = LinkBlock()
content = block.get_searchable_content(
block.to_python(
{
"title": "Wagtail site",
"link": "http://www.wagtail.org",
}
)
)
self.assertEqual(content, ["Wagtail site"])
def test_value_from_datadict(self):
block = blocks.StructBlock(
[
("title", blocks.CharBlock()),
("link", blocks.URLBlock()),
]
)
struct_val = block.value_from_datadict(
{"mylink-title": "Torchbox", "mylink-link": "http://www.torchbox.com"},
{},
"mylink",
)
self.assertEqual(struct_val["title"], "Torchbox")
self.assertEqual(struct_val["link"], "http://www.torchbox.com")
self.assertIsInstance(struct_val, blocks.StructValue)
self.assertIsInstance(struct_val.bound_blocks["link"].block, blocks.URLBlock)
def test_value_omitted_from_data(self):
block = blocks.StructBlock(
[
("title", blocks.CharBlock()),
("link", blocks.URLBlock()),
]
)
# overall value is considered present in the form if any sub-field is present
self.assertFalse(
block.value_omitted_from_data({"mylink-title": "Torchbox"}, {}, "mylink")
)
self.assertTrue(
block.value_omitted_from_data({"nothing-here": "nope"}, {}, "mylink")
)
def test_default_is_returned_as_structvalue(self):
"""When returning the default value of a StructBlock (e.g. because it's
a child of another StructBlock, and the outer value is missing that key)
we should receive it as a StructValue, not just a plain dict"""
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
class EventBlock(blocks.StructBlock):
title = blocks.CharBlock()
guest_speaker = PersonBlock(
default={"first_name": "Ed", "surname": "Balls"}
)
event_block = EventBlock()
event = event_block.to_python({"title": "Birthday party"})
self.assertEqual(event["guest_speaker"]["first_name"], "Ed")
self.assertIsInstance(event["guest_speaker"], blocks.StructValue)
def test_default_value_is_distinct_instance(self):
"""
Whenever the default value of a StructBlock is invoked, it should be a distinct
instance of the dict so that modifying it doesn't modify other places where the
default value appears.
"""
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
class EventBlock(blocks.StructBlock):
title = blocks.CharBlock()
guest_speaker = PersonBlock(
default={"first_name": "Ed", "surname": "Balls"}
)
event_block = EventBlock()
event1 = event_block.to_python(
{"title": "Birthday party"}
) # guest_speaker will default to Ed Balls
event2 = event_block.to_python(
{"title": "Christmas party"}
) # guest_speaker will default to Ed Balls, but a distinct instance
event1["guest_speaker"]["surname"] = "Miliband"
self.assertEqual(event1["guest_speaker"]["surname"], "Miliband")
# event2 should not be modified
self.assertEqual(event2["guest_speaker"]["surname"], "Balls")
def test_bulk_to_python_returns_distinct_default_instances(self):
"""
Whenever StructBlock.bulk_to_python invokes a child block's get_default method to
fill in missing fields, it should use a separate invocation for each record so that
we don't end up with the same instance of a mutable value on multiple records
"""
class ShoppingListBlock(blocks.StructBlock):
shop = blocks.CharBlock()
items = blocks.ListBlock(blocks.CharBlock(default="chocolate"))
block = ShoppingListBlock()
shopping_lists = block.bulk_to_python(
[
{"shop": "Tesco"}, # 'items' defaults to ['chocolate']
{
"shop": "Asda"
}, # 'items' defaults to ['chocolate'], but a distinct instance
]
)
shopping_lists[0]["items"].append("cake")
self.assertEqual(list(shopping_lists[0]["items"]), ["chocolate", "cake"])
# shopping_lists[1] should not be updated
self.assertEqual(list(shopping_lists[1]["items"]), ["chocolate"])
def test_clean(self):
block = blocks.StructBlock(
[
("title", blocks.CharBlock()),
("link", blocks.URLBlock()),
]
)
value = block.to_python(
{"title": "Torchbox", "link": "http://www.torchbox.com/"}
)
clean_value = block.clean(value)
self.assertIsInstance(clean_value, blocks.StructValue)
self.assertEqual(clean_value["title"], "Torchbox")
value = block.to_python({"title": "Torchbox", "link": "not a url"})
with self.assertRaises(ValidationError):
block.clean(value)
def test_non_block_validation_error(self):
class LinkBlock(blocks.StructBlock):
page = blocks.PageChooserBlock(required=False)
url = blocks.URLBlock(required=False)
def clean(self, value):
result = super().clean(value)
if not (result["page"] or result["url"]):
raise StructBlockValidationError(
non_block_errors=ErrorList(
["Either page or URL must be specified"]
)
)
return result
block = LinkBlock()
bad_data = {"page": None, "url": ""}
with self.assertRaises(ValidationError):
block.clean(bad_data)
good_data = {"page": None, "url": "https://wagtail.org/"}
self.assertEqual(block.clean(good_data), good_data)
def test_bound_blocks_are_available_on_template(self):
"""
Test that we are able to use value.bound_blocks within templates
to access a child block's own HTML rendering
"""
block = SectionBlock()
value = block.to_python({"title": "Hello", "body": "italic world"})
result = block.render(value)
self.assertEqual(result, """
monde italique""")
def test_render_structvalue(self):
"""
The HTML representation of a StructValue should use the block's template
"""
block = SectionBlock()
value = block.to_python({"title": "Hello", "body": "italic world"})
result = value.__html__()
self.assertEqual(result, """
Hello
italic world""")
# value.render_as_block() should be equivalent to value.__html__()
result = value.render_as_block()
self.assertEqual(result, """
Hello
italic world""")
def test_str_structvalue(self):
"""
The str() representation of a StructValue should NOT render the template, as that's liable
to cause an infinite loop if any debugging / logging code attempts to log the fact that
it rendered a template with this object in the context:
https://github.com/wagtail/wagtail/issues/2874
https://github.com/jazzband/django-debug-toolbar/issues/950
"""
block = SectionBlock()
value = block.to_python({"title": "Hello", "body": "italic world"})
result = str(value)
self.assertNotIn("
", result)
# The expected rendering should correspond to the native representation of an OrderedDict:
# "StructValue([('title', u'Hello'), ('body', )])"
# - give or take some quoting differences between Python versions
self.assertIn("StructValue", result)
self.assertIn("title", result)
self.assertIn("Hello", result)
def test_render_structvalue_with_extra_context(self):
block = SectionBlock()
value = block.to_python({"title": "Bonjour", "body": "monde italique"})
result = value.render_as_block(context={"language": "fr"})
self.assertEqual(result, """
Bonjour
monde italique""")
def test_copy_structvalue(self):
block = SectionBlock()
value = block.to_python({"title": "Hello", "body": "world"})
copied = copy.copy(value)
# Ensure we have a new object
self.assertIsNot(value, copied)
# Check copy operation
self.assertIsInstance(copied, blocks.StructValue)
self.assertIs(value.block, copied.block)
self.assertEqual(value, copied)
def test_normalize_base_cases(self):
"""Test the trivially recursive and already normalized cases"""
block = blocks.StructBlock([("title", blocks.CharBlock())])
self.assertEqual(
block.normalize({"title": "Foo"}), block._to_struct_value({"title": "Foo"})
)
self.assertEqual(
block.normalize(block._to_struct_value({"title": "Foo"})),
block._to_struct_value({"title": "Foo"}),
)
def test_recursive_normalize(self):
"""StructBlock.normalize should recursively normalize all children"""
block = blocks.StructBlock(
[
(
"inner_stream",
blocks.StreamBlock(
[
("inner_char", blocks.CharBlock()),
("inner_int", blocks.IntegerBlock()),
]
),
),
("list_of_ints", blocks.ListBlock(blocks.IntegerBlock())),
]
)
# A value in the human friendly format
value = {
"inner_stream": [("inner_char", "Hello, world"), ("inner_int", 42)],
"list_of_ints": [5, 6, 7, 8],
}
normalized = block.normalize(value)
self.assertIsInstance(normalized, blocks.StructValue)
self.assertIsInstance(normalized["inner_stream"], blocks.StreamValue)
self.assertIsInstance(
normalized["inner_stream"][0], blocks.StreamValue.StreamChild
)
self.assertIsInstance(
normalized["inner_stream"][1], blocks.StreamValue.StreamChild
)
self.assertIsInstance(normalized["list_of_ints"], blocks.list_block.ListValue)
self.assertIsInstance(normalized["list_of_ints"][0], int)
class TestStructBlockWithCustomStructValue(SimpleTestCase):
def test_initialisation(self):
class CustomStructValue(blocks.StructValue):
def joined(self):
return self.get("title", "") + self.get("link", "")
block = blocks.StructBlock(
[
("title", blocks.CharBlock()),
("link", blocks.URLBlock()),
],
value_class=CustomStructValue,
)
self.assertEqual(list(block.child_blocks.keys()), ["title", "link"])
block_value = block.to_python(
{"title": "Birthday party", "link": "https://myparty.co.uk"}
)
self.assertIsInstance(block_value, CustomStructValue)
default_value = block.get_default()
self.assertIsInstance(default_value, CustomStructValue)
value_from_datadict = block.value_from_datadict(
{"mylink-title": "Torchbox", "mylink-link": "http://www.torchbox.com"},
{},
"mylink",
)
self.assertIsInstance(value_from_datadict, CustomStructValue)
value = block.to_python(
{"title": "Torchbox", "link": "http://www.torchbox.com/"}
)
clean_value = block.clean(value)
self.assertIsInstance(clean_value, CustomStructValue)
self.assertEqual(clean_value["title"], "Torchbox")
value = block.to_python({"title": "Torchbox", "link": "not a url"})
with self.assertRaises(ValidationError):
block.clean(value)
def test_initialisation_from_subclass(self):
class LinkStructValue(blocks.StructValue):
def url(self):
return self.get("page") or self.get("link")
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
page = blocks.PageChooserBlock(required=False)
link = blocks.URLBlock(required=False)
class Meta:
value_class = LinkStructValue
block = LinkBlock()
self.assertEqual(list(block.child_blocks.keys()), ["title", "page", "link"])
block_value = block.to_python(
{"title": "Website", "link": "https://website.com"}
)
self.assertIsInstance(block_value, LinkStructValue)
default_value = block.get_default()
self.assertIsInstance(default_value, LinkStructValue)
def test_initialisation_with_multiple_subclassses(self):
class LinkStructValue(blocks.StructValue):
def url(self):
return self.get("page") or self.get("link")
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
page = blocks.PageChooserBlock(required=False)
link = blocks.URLBlock(required=False)
class Meta:
value_class = LinkStructValue
class StyledLinkBlock(LinkBlock):
classname = blocks.CharBlock()
block = StyledLinkBlock()
self.assertEqual(
list(block.child_blocks.keys()), ["title", "page", "link", "classname"]
)
value_from_datadict = block.value_from_datadict(
{
"queen-title": "Torchbox",
"queen-link": "http://www.torchbox.com",
"queen-classname": "fullsize",
},
{},
"queen",
)
self.assertIsInstance(value_from_datadict, LinkStructValue)
def test_initialisation_with_mixins(self):
class LinkStructValue(blocks.StructValue):
pass
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
class Meta:
value_class = LinkStructValue
class StylingMixin(blocks.StructBlock):
classname = blocks.CharBlock()
class StyledLinkBlock(StylingMixin, LinkBlock):
source = blocks.CharBlock()
block = StyledLinkBlock()
self.assertEqual(
list(block.child_blocks.keys()), ["title", "link", "classname", "source"]
)
block_value = block.to_python(
{
"title": "Website",
"link": "https://website.com",
"source": "google",
"classname": "full-size",
}
)
self.assertIsInstance(block_value, LinkStructValue)
def test_value_property(self):
class SectionStructValue(blocks.StructValue):
@property
def foo(self):
return "bar %s" % self.get("title", "")
class SectionBlock(blocks.StructBlock):
title = blocks.CharBlock()
body = blocks.RichTextBlock()
class Meta:
value_class = SectionStructValue
block = SectionBlock()
struct_value = block.to_python({"title": "hello", "body": "world"})
value = struct_value.foo
self.assertEqual(value, "bar hello")
def test_render_with_template(self):
class SectionStructValue(blocks.StructValue):
def title_with_suffix(self):
title = self.get("title")
if title:
return "SUFFIX %s" % title
return "EMPTY TITLE"
class SectionBlock(blocks.StructBlock):
title = blocks.CharBlock(required=False)
class Meta:
value_class = SectionStructValue
block = SectionBlock(template="tests/blocks/struct_block_custom_value.html")
struct_value = block.to_python({"title": "hello"})
html = block.render(struct_value)
self.assertEqual(html, "
SUFFIX hello
\n")
struct_value = block.to_python({})
html = block.render(struct_value)
self.assertEqual(html, "
EMPTY TITLE
\n")
def test_normalize(self):
"""A normalized StructBlock value should be an instance of the StructBlock's value_class"""
class CustomStructValue(blocks.StructValue):
pass
class CustomStructBlock(blocks.StructBlock):
text = blocks.TextBlock()
class Meta:
value_class = CustomStructValue
self.assertIsInstance(
CustomStructBlock().normalize({"text": "She sells sea shells"}),
CustomStructValue,
)
def test_normalize_incorrect_value_class(self):
"""
If StructBlock.normalize is passed a StructValue instance that doesn't
match the StructBlock's `value_class', it should convert the value
to the correct class.
"""
class CustomStructValue(blocks.StructValue):
pass
class CustomStructBlock(blocks.StructBlock):
text = blocks.TextBlock()
class Meta:
value_class = CustomStructValue
block = CustomStructBlock()
# Not an instance of CustomStructValue, which CustomStructBlock uses.
value = blocks.StructValue(block, {"text": "The quick brown fox"})
self.assertIsInstance(block.normalize(value), CustomStructValue)
class TestListBlock(WagtailTestUtils, SimpleTestCase):
def assert_eq_list_values(self, p, q):
# We can't directly compare ListValue instances yet
self.assertEqual(list(p), list(q))
def test_initialise_with_class(self):
block = blocks.ListBlock(blocks.CharBlock)
# Child block should be initialised for us
self.assertIsInstance(block.child_block, blocks.CharBlock)
def test_initialise_with_instance(self):
child_block = blocks.CharBlock()
block = blocks.ListBlock(child_block)
self.assertEqual(block.child_block, child_block)
def render(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = blocks.ListBlock(LinkBlock())
return block.render(
[
{
"title": "Wagtail",
"link": "http://www.wagtail.org",
},
{
"title": "Django",
"link": "http://www.djangoproject.com",
},
]
)
def test_render_uses_ul(self):
html = self.render()
self.assertIn("
", html)
self.assertIn("
", html)
def test_render_uses_li(self):
html = self.render()
self.assertIn("
", html)
self.assertIn("
", html)
def test_render_calls_block_render_on_children(self):
"""
The default rendering of a ListBlock should invoke the block's render method
on each child, rather than just outputting the child value as a string.
"""
block = blocks.ListBlock(
blocks.CharBlock(template="tests/blocks/heading_block.html")
)
html = block.render(["Hello world!", "Goodbye world!"])
self.assertIn("
Hello world!
", html)
self.assertIn("
Goodbye world!
", html)
def test_render_passes_context_to_children(self):
"""
Template context passed to the render method should be passed on
to the render method of the child block.
"""
block = blocks.ListBlock(
blocks.CharBlock(template="tests/blocks/heading_block.html")
)
html = block.render(
["Bonjour le monde!", "Au revoir le monde!"],
context={
"language": "fr",
},
)
self.assertIn('
Bonjour le monde!
', html)
self.assertIn('
Au revoir le monde!
', html)
def test_get_api_representation_calls_same_method_on_children_with_context(self):
"""
The get_api_representation method of a ListBlock should invoke
the block's get_api_representation method on each child and
the context should be passed on.
"""
class ContextBlock(blocks.CharBlock):
def get_api_representation(self, value, context=None):
return context[value]
block = blocks.ListBlock(ContextBlock())
api_representation = block.get_api_representation(
["en", "fr"], context={"en": "Hello world!", "fr": "Bonjour le monde!"}
)
self.assertEqual(api_representation, ["Hello world!", "Bonjour le monde!"])
def test_adapt(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = blocks.ListBlock(LinkBlock)
block.set_name("test_listblock")
js_args = ListBlockAdapter().js_args(block)
self.assertEqual(js_args[0], "test_listblock")
self.assertIsInstance(js_args[1], LinkBlock)
self.assertEqual(js_args[2], {"title": None, "link": None})
self.assertEqual(
js_args[3],
{
"label": "Test listblock",
"icon": "placeholder",
"classname": None,
"collapsed": False,
"strings": {
"DELETE": "Delete",
"DUPLICATE": "Duplicate",
"MOVE_DOWN": "Move down",
"MOVE_UP": "Move up",
"ADD": "Add",
},
},
)
def test_adapt_with_min_num_max_num(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = blocks.ListBlock(LinkBlock, min_num=2, max_num=5)
block.set_name("test_listblock")
js_args = ListBlockAdapter().js_args(block)
self.assertEqual(js_args[0], "test_listblock")
self.assertIsInstance(js_args[1], LinkBlock)
self.assertEqual(js_args[2], {"title": None, "link": None})
self.assertEqual(
js_args[3],
{
"label": "Test listblock",
"icon": "placeholder",
"classname": None,
"collapsed": False,
"minNum": 2,
"maxNum": 5,
"strings": {
"DELETE": "Delete",
"DUPLICATE": "Duplicate",
"MOVE_DOWN": "Move down",
"MOVE_UP": "Move up",
"ADD": "Add",
},
},
)
def test_searchable_content(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = blocks.ListBlock(LinkBlock())
content = block.get_searchable_content(
[
{
"title": "Wagtail",
"link": "http://www.wagtail.org",
},
{
"title": "Django",
"link": "http://www.djangoproject.com",
},
]
)
self.assertEqual(content, ["Wagtail", "Django"])
def test_value_omitted_from_data(self):
block = blocks.ListBlock(blocks.CharBlock())
# overall value is considered present in the form if the 'count' field is present
self.assertFalse(
block.value_omitted_from_data({"mylist-count": "0"}, {}, "mylist")
)
self.assertFalse(
block.value_omitted_from_data(
{
"mylist-count": "1",
"mylist-0-value": "hello",
"mylist-0-deleted": "",
"mylist-0-order": "0",
},
{},
"mylist",
)
)
self.assertTrue(
block.value_omitted_from_data({"nothing-here": "nope"}, {}, "mylist")
)
def test_id_from_form_submission_is_preserved(self):
block = blocks.ListBlock(blocks.CharBlock())
post_data = {"shoppinglist-count": "3"}
for i in range(0, 3):
post_data.update(
{
"shoppinglist-%d-deleted" % i: "",
"shoppinglist-%d-order" % i: str(i),
"shoppinglist-%d-value" % i: "item %d" % i,
"shoppinglist-%d-id" % i: "0000000%d" % i,
}
)
block_value = block.value_from_datadict(post_data, {}, "shoppinglist")
self.assertEqual(block_value.bound_blocks[1].value, "item 1")
self.assertEqual(block_value.bound_blocks[1].id, "00000001")
def test_ordering_in_form_submission_uses_order_field(self):
block = blocks.ListBlock(blocks.CharBlock())
# check that items are ordered by the 'order' field, not the order they appear in the form
post_data = {"shoppinglist-count": "3"}
for i in range(0, 3):
post_data.update(
{
"shoppinglist-%d-deleted" % i: "",
"shoppinglist-%d-order" % i: str(2 - i),
"shoppinglist-%d-value" % i: "item %d" % i,
"shoppinglist-%d-id" % i: "0000000%d" % i,
}
)
block_value = block.value_from_datadict(post_data, {}, "shoppinglist")
self.assertEqual(block_value[2], "item 0")
def test_ordering_in_form_submission_is_numeric(self):
block = blocks.ListBlock(blocks.CharBlock())
# check that items are ordered by 'order' numerically, not alphabetically
post_data = {"shoppinglist-count": "12"}
for i in range(0, 12):
post_data.update(
{
"shoppinglist-%d-deleted" % i: "",
"shoppinglist-%d-order" % i: str(i),
"shoppinglist-%d-value" % i: "item %d" % i,
"shoppinglist-%d-id" % i: "0000000%d" % i,
}
)
block_value = block.value_from_datadict(post_data, {}, "shoppinglist")
self.assertEqual(block_value[2], "item 2")
def test_can_specify_default(self):
block = blocks.ListBlock(
blocks.CharBlock(), default=["peas", "beans", "carrots"]
)
self.assertEqual(list(block.get_default()), ["peas", "beans", "carrots"])
def test_default_default(self):
"""
if no explicit 'default' is set on the ListBlock, it should fall back on
a single instance of the child block in its default state.
"""
block = blocks.ListBlock(blocks.CharBlock(default="chocolate"))
self.assertEqual(list(block.get_default()), ["chocolate"])
block.set_name("test_shoppinglistblock")
js_args = ListBlockAdapter().js_args(block)
self.assertEqual(js_args[2], "chocolate")
def test_default_value_is_distinct_instance(self):
"""
Whenever the default value of a ListBlock is invoked, it should be a distinct
instance of the list so that modifying it doesn't modify other places where the
default value appears.
"""
class ShoppingListBlock(blocks.StructBlock):
shop = blocks.CharBlock()
items = blocks.ListBlock(blocks.CharBlock(default="chocolate"))
block = ShoppingListBlock()
tesco_shopping = block.to_python(
{"shop": "Tesco"}
) # 'items' will default to ['chocolate']
asda_shopping = block.to_python(
{"shop": "Asda"}
) # 'items' will default to ['chocolate'], but a distinct instance
tesco_shopping["items"].append("cake")
self.assertEqual(list(tesco_shopping["items"]), ["chocolate", "cake"])
# asda_shopping should not be modified
self.assertEqual(list(asda_shopping["items"]), ["chocolate"])
def test_adapt_with_classname_via_kwarg(self):
"""form_classname from kwargs to be used as an additional class when rendering list block"""
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
block = blocks.ListBlock(LinkBlock, form_classname="special-list-class")
block.set_name("test_listblock")
js_args = ListBlockAdapter().js_args(block)
self.assertEqual(
js_args[3],
{
"label": "Test listblock",
"icon": "placeholder",
"classname": "special-list-class",
"collapsed": False,
"strings": {
"DELETE": "Delete",
"DUPLICATE": "Duplicate",
"MOVE_DOWN": "Move down",
"MOVE_UP": "Move up",
"ADD": "Add",
},
},
)
def test_adapt_with_classname_via_class_meta(self):
"""form_classname from meta to be used as an additional class when rendering list block"""
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
link = blocks.URLBlock()
class CustomListBlock(blocks.ListBlock):
class Meta:
form_classname = "custom-list-class"
block = CustomListBlock(LinkBlock)
block.set_name("test_listblock")
js_args = ListBlockAdapter().js_args(block)
self.assertEqual(
js_args[3],
{
"label": "Test listblock",
"icon": "placeholder",
"classname": "custom-list-class",
"collapsed": False,
"strings": {
"DELETE": "Delete",
"DUPLICATE": "Duplicate",
"MOVE_DOWN": "Move down",
"MOVE_UP": "Move up",
"ADD": "Add",
},
},
)
def test_clean_preserves_block_ids(self):
block = blocks.ListBlock(blocks.CharBlock())
block_val = block.to_python(
[
{
"type": "item",
"value": "foo",
"id": "11111111-1111-1111-1111-111111111111",
},
{
"type": "item",
"value": "bar",
"id": "22222222-2222-2222-2222-222222222222",
},
]
)
cleaned_block_val = block.clean(block_val)
self.assertEqual(
cleaned_block_val.bound_blocks[0].id, "11111111-1111-1111-1111-111111111111"
)
def test_min_num_validation_errors(self):
block = blocks.ListBlock(blocks.CharBlock(), min_num=2)
block_val = block.to_python(["foo"])
with self.assertRaises(ValidationError) as catcher:
block.clean(block_val)
self.assertEqual(
catcher.exception.as_json_data(),
{
"messages": ["The minimum number of items is 2"],
},
)
# a value with >= 2 blocks should pass validation
block_val = block.to_python(["foo", "bar"])
self.assertTrue(block.clean(block_val))
def test_max_num_validation_errors(self):
block = blocks.ListBlock(blocks.CharBlock(), max_num=2)
block_val = block.to_python(["foo", "bar", "baz"])
with self.assertRaises(ValidationError) as catcher:
block.clean(block_val)
self.assertEqual(
catcher.exception.as_json_data(),
{
"messages": ["The maximum number of items is 2"],
},
)
# a value with <= 2 blocks should pass validation
block_val = block.to_python(["foo", "bar"])
self.assertTrue(block.clean(block_val))
def test_unpack_old_database_format(self):
block = blocks.ListBlock(blocks.CharBlock())
list_val = block.to_python(["foo", "bar"])
# list_val should behave as a list
self.assertEqual(len(list_val), 2)
self.assertEqual(list_val[0], "foo")
# but also provide a bound_blocks property
self.assertEqual(len(list_val.bound_blocks), 2)
self.assertEqual(list_val.bound_blocks[0].value, "foo")
# Bound blocks should be assigned UUIDs
self.assertRegex(list_val.bound_blocks[0].id, r"[0-9a-f-]+")
def test_bulk_unpack_old_database_format(self):
block = blocks.ListBlock(blocks.CharBlock())
[list_1, list_2] = block.bulk_to_python([["foo", "bar"], ["xxx", "yyy", "zzz"]])
self.assertEqual(len(list_1), 2)
self.assertEqual(len(list_2), 3)
self.assertEqual(list_1[0], "foo")
self.assertEqual(list_2[0], "xxx")
# lists also provide a bound_blocks property
self.assertEqual(len(list_1.bound_blocks), 2)
self.assertEqual(list_1.bound_blocks[0].value, "foo")
# Bound blocks should be assigned UUIDs
self.assertRegex(list_1.bound_blocks[0].id, r"[0-9a-f-]+")
def test_unpack_new_database_format(self):
block = blocks.ListBlock(blocks.CharBlock())
list_val = block.to_python(
[
{
"type": "item",
"value": "foo",
"id": "11111111-1111-1111-1111-111111111111",
},
{
"type": "item",
"value": "bar",
"id": "22222222-2222-2222-2222-222222222222",
},
]
)
# list_val should behave as a list
self.assertEqual(len(list_val), 2)
self.assertEqual(list_val[0], "foo")
# but also provide a bound_blocks property
self.assertEqual(len(list_val.bound_blocks), 2)
self.assertEqual(list_val.bound_blocks[0].value, "foo")
self.assertEqual(
list_val.bound_blocks[0].id, "11111111-1111-1111-1111-111111111111"
)
def test_bulk_unpack_new_database_format(self):
block = blocks.ListBlock(blocks.CharBlock())
[list_1, list_2] = block.bulk_to_python(
[
[
{
"type": "item",
"value": "foo",
"id": "11111111-1111-1111-1111-111111111111",
},
{
"type": "item",
"value": "bar",
"id": "22222222-2222-2222-2222-222222222222",
},
],
[
{
"type": "item",
"value": "baz",
"id": "33333333-3333-3333-3333-333333333333",
},
],
]
)
self.assertEqual(len(list_1), 2)
self.assertEqual(len(list_2), 1)
self.assertEqual(list_1[0], "foo")
self.assertEqual(list_2[0], "baz")
# lists also provide a bound_blocks property
self.assertEqual(len(list_1.bound_blocks), 2)
self.assertEqual(list_1.bound_blocks[0].value, "foo")
self.assertEqual(
list_1.bound_blocks[0].id, "11111111-1111-1111-1111-111111111111"
)
def test_assign_listblock_with_list(self):
stream_block = blocks.StreamBlock(
[
("bullet_list", blocks.ListBlock(blocks.CharBlock())),
]
)
stream_value = stream_block.to_python([])
stream_value.append(("bullet_list", ["foo", "bar"]))
clean_stream_value = stream_block.clean(stream_value)
result = stream_block.get_prep_value(clean_stream_value)
self.assertEqual(result[0]["type"], "bullet_list")
self.assertEqual(len(result[0]["value"]), 2)
self.assertEqual(result[0]["value"][0]["value"], "foo")
def test_normalize_base_case(self):
"""Test normalize when trivially recursive, or already a ListValue"""
block = blocks.ListBlock(blocks.IntegerBlock)
normalized = block.normalize([0, 1, 1, 2, 3])
self.assertIsInstance(normalized, blocks.list_block.ListValue)
self.assert_eq_list_values(normalized, [0, 1, 1, 2, 3])
normalized = block.normalize(
blocks.list_block.ListValue(block, [0, 1, 1, 2, 3])
)
self.assertIsInstance(normalized, blocks.list_block.ListValue)
self.assert_eq_list_values(normalized, [0, 1, 1, 2, 3])
def test_normalize_empty(self):
block = blocks.ListBlock(blocks.IntegerBlock())
normalized = block.normalize([])
self.assertIsInstance(normalized, blocks.list_block.ListValue)
self.assert_eq_list_values(normalized, [])
def test_recursive_normalize(self):
"""
ListBlock.normalize should recursively normalize all values passed to
it, and return a ListValue.
"""
inner_list_block = blocks.ListBlock(blocks.IntegerBlock())
block = blocks.ListBlock(inner_list_block)
values = [
[[1, 2, 3]],
[blocks.list_block.ListValue(block, [1, 2, 3])],
]
for value in values:
normalized = block.normalize(value)
self.assertIsInstance(normalized, blocks.list_block.ListValue)
self.assert_eq_list_values(normalized[0], [1, 2, 3])
class TestListBlockWithFixtures(TestCase):
fixtures = ["test.json"]
def test_calls_child_bulk_to_python_when_available(self):
page_ids = [2, 3, 4, 5]
expected_pages = Page.objects.filter(pk__in=page_ids)
block = blocks.ListBlock(blocks.PageChooserBlock())
with self.assertNumQueries(1):
pages = block.to_python(page_ids)
self.assertSequenceEqual(pages, expected_pages)
def test_bulk_to_python(self):
block = blocks.ListBlock(blocks.PageChooserBlock())
with self.assertNumQueries(1):
result = block.bulk_to_python([[4, 5], [], [2]])
# result will be a list of ListValues - convert to lists for equality check
clean_result = [list(val) for val in result]
self.assertEqual(
clean_result,
[
[Page.objects.get(id=4), Page.objects.get(id=5)],
[],
[Page.objects.get(id=2)],
],
)
def test_extract_references(self):
block = blocks.ListBlock(blocks.PageChooserBlock())
christmas_page = Page.objects.get(slug="christmas")
saint_patrick_page = Page.objects.get(slug="saint-patrick")
self.assertListEqual(
list(
block.extract_references(
block.to_python(
[
{
"id": "block1",
"type": "item",
"value": christmas_page.id,
},
{
"id": "block2",
"type": "item",
"value": saint_patrick_page.id,
},
]
)
)
),
[
(Page, str(christmas_page.id), "item", "block1"),
(Page, str(saint_patrick_page.id), "item", "block2"),
],
)
class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
def test_initialisation(self):
block = blocks.StreamBlock(
[
("heading", blocks.CharBlock()),
("paragraph", blocks.CharBlock()),
]
)
self.assertEqual(list(block.child_blocks.keys()), ["heading", "paragraph"])
def test_initialisation_with_binary_string_names(self):
# migrations will sometimes write out names as binary strings, just to keep us on our toes
block = blocks.StreamBlock(
[
(b"heading", blocks.CharBlock()),
(b"paragraph", blocks.CharBlock()),
]
)
self.assertEqual(list(block.child_blocks.keys()), [b"heading", b"paragraph"])
def test_initialisation_from_subclass(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock()
self.assertEqual(list(block.child_blocks.keys()), ["heading", "paragraph"])
def test_initialisation_from_subclass_with_extra(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock([("intro", blocks.CharBlock())])
self.assertEqual(
list(block.child_blocks.keys()), ["heading", "paragraph", "intro"]
)
def test_initialisation_with_multiple_subclassses(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
class ArticleWithIntroBlock(ArticleBlock):
intro = blocks.CharBlock()
block = ArticleWithIntroBlock()
self.assertEqual(
list(block.child_blocks.keys()), ["heading", "paragraph", "intro"]
)
def test_initialisation_with_mixins(self):
"""
The order of child blocks of a ``StreamBlock`` with multiple parent
classes is slightly surprising at first. Child blocks are inherited in
a bottom-up order, by traversing the MRO in reverse. In the example
below, ``ArticleWithIntroBlock`` will have an MRO of::
[ArticleWithIntroBlock, IntroMixin, ArticleBlock, StreamBlock, ...]
This will result in ``intro`` appearing *after* ``heading`` and
``paragraph`` in ``ArticleWithIntroBlock.child_blocks``, even though
``IntroMixin`` appeared before ``ArticleBlock``.
"""
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
class IntroMixin(blocks.StreamBlock):
intro = blocks.CharBlock()
class ArticleWithIntroBlock(IntroMixin, ArticleBlock):
by_line = blocks.CharBlock()
block = ArticleWithIntroBlock()
self.assertEqual(
list(block.child_blocks.keys()),
["heading", "paragraph", "intro", "by_line"],
)
def test_field_has_changed(self):
block = blocks.StreamBlock([("paragraph", blocks.CharBlock())])
initial_value = blocks.StreamValue(block, [("paragraph", "test")])
initial_value[0].id = "a"
data_value = blocks.StreamValue(block, [("paragraph", "test")])
data_value[0].id = "a"
# identical ids and content, so has_changed should return False
self.assertFalse(
blocks.BlockField(block).has_changed(initial_value, data_value)
)
changed_data_value = blocks.StreamValue(block, [("paragraph", "not a test")])
changed_data_value[0].id = "a"
# identical ids but changed content, so has_changed should return True
self.assertTrue(
blocks.BlockField(block).has_changed(initial_value, changed_data_value)
)
def test_required_raises_an_exception_if_empty(self):
block = blocks.StreamBlock([("paragraph", blocks.CharBlock())], required=True)
value = blocks.StreamValue(block, [])
with self.assertRaises(blocks.StreamBlockValidationError):
block.clean(value)
def test_required_does_not_raise_an_exception_if_not_empty(self):
block = blocks.StreamBlock([("paragraph", blocks.CharBlock())], required=True)
value = block.to_python([{"type": "paragraph", "value": "Hello"}])
try:
block.clean(value)
except blocks.StreamBlockValidationError:
raise self.failureException(
"%s was raised" % blocks.StreamBlockValidationError
)
def test_not_required_does_not_raise_an_exception_if_empty(self):
block = blocks.StreamBlock([("paragraph", blocks.CharBlock())], required=False)
value = blocks.StreamValue(block, [])
try:
block.clean(value)
except blocks.StreamBlockValidationError:
raise self.failureException(
"%s was raised" % blocks.StreamBlockValidationError
)
def test_required_by_default(self):
block = blocks.StreamBlock([("paragraph", blocks.CharBlock())])
value = blocks.StreamValue(block, [])
with self.assertRaises(blocks.StreamBlockValidationError):
block.clean(value)
def render_article(self, data):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.RichTextBlock()
block = ArticleBlock()
value = block.to_python(data)
return block.render(value)
def test_get_api_representation_calls_same_method_on_children_with_context(self):
"""
The get_api_representation method of a StreamBlock should invoke
the block's get_api_representation method on each child and
the context should be passed on.
"""
class ContextBlock(blocks.CharBlock):
def get_api_representation(self, value, context=None):
return context[value]
block = blocks.StreamBlock(
[
("language", ContextBlock()),
("author", ContextBlock()),
]
)
api_representation = block.get_api_representation(
block.to_python(
[
{"type": "language", "value": "en"},
{"type": "author", "value": "wagtail", "id": "111111"},
]
),
context={"en": "English", "wagtail": "Wagtail!"},
)
self.assertListEqual(
api_representation,
[
{"type": "language", "value": "English", "id": None},
{"type": "author", "value": "Wagtail!", "id": "111111"},
],
)
def test_render(self):
html = self.render_article(
[
{
"type": "heading",
"value": "My title",
},
{
"type": "paragraph",
"value": "My first paragraph",
},
{
"type": "paragraph",
"value": "My second paragraph",
},
]
)
self.assertIn('
My title
', html)
self.assertIn(
'
My first paragraph
', html
)
self.assertIn('
My second paragraph
', html)
def test_render_unknown_type(self):
# This can happen if a developer removes a type from their StreamBlock
html = self.render_article(
[
{
"type": "foo",
"value": "Hello",
},
{
"type": "paragraph",
"value": "My first paragraph",
},
]
)
self.assertNotIn("foo", html)
self.assertNotIn("Hello", html)
self.assertIn('
My first paragraph
', html)
def test_render_calls_block_render_on_children(self):
"""
The default rendering of a StreamBlock should invoke the block's render method
on each child, rather than just outputting the child value as a string.
"""
block = blocks.StreamBlock(
[
(
"heading",
blocks.CharBlock(template="tests/blocks/heading_block.html"),
),
("paragraph", blocks.CharBlock()),
]
)
value = block.to_python([{"type": "heading", "value": "Hello"}])
html = block.render(value)
self.assertIn('
Hello
', html)
# calling render_as_block() on value (a StreamValue instance)
# should be equivalent to block.render(value)
html = value.render_as_block()
self.assertIn('
', html
)
# calling render_as_block(context=foo) on value (a StreamValue instance)
# should be equivalent to block.render(value, context=foo)
html = value.render_as_block(
context={
"language": "fr",
}
)
self.assertIn(
'
Bonjour
', html
)
def test_render_on_stream_child_uses_child_template(self):
"""
Accessing a child element of the stream (giving a StreamChild object) and rendering it
should use the block template, not just render the value's string representation
"""
block = blocks.StreamBlock(
[
(
"heading",
blocks.CharBlock(template="tests/blocks/heading_block.html"),
),
("paragraph", blocks.CharBlock()),
]
)
value = block.to_python([{"type": "heading", "value": "Hello"}])
html = value[0].render()
self.assertEqual("
Hello
", html)
# StreamChild.__str__ should do the same
html = str(value[0])
self.assertEqual("
Hello
", html)
# and so should StreamChild.render_as_block
html = value[0].render_as_block()
self.assertEqual("
', html)
# the same functionality should be available through the alias `render_as_block`
html = value[0].render_as_block(context={"language": "fr"})
self.assertEqual('
Bonjour
', html)
def test_adapt(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock()
block.set_name("test_streamblock")
js_args = StreamBlockAdapter().js_args(block)
self.assertEqual(js_args[0], "test_streamblock")
# convert group_by iterable into a list
grouped_blocks = [
(group_name, list(group_iter)) for (group_name, group_iter) in js_args[1]
]
self.assertEqual(len(grouped_blocks), 1)
group_name, block_iter = grouped_blocks[0]
self.assertEqual(group_name, "")
block_list = list(block_iter)
self.assertIsInstance(block_list[0], blocks.CharBlock)
self.assertEqual(block_list[0].name, "heading")
self.assertIsInstance(block_list[1], blocks.CharBlock)
self.assertEqual(block_list[1].name, "paragraph")
self.assertEqual(js_args[2], {"heading": None, "paragraph": None})
self.assertEqual(
js_args[3],
{
"label": "Test streamblock",
"icon": "placeholder",
"classname": None,
"collapsed": False,
"maxNum": None,
"minNum": None,
"blockCounts": {},
"required": True,
"strings": {
"DELETE": "Delete",
"DUPLICATE": "Duplicate",
"MOVE_DOWN": "Move down",
"MOVE_UP": "Move up",
"ADD": "Add",
},
},
)
def test_value_omitted_from_data(self):
block = blocks.StreamBlock(
[
("heading", blocks.CharBlock()),
]
)
# overall value is considered present in the form if the 'count' field is present
self.assertFalse(
block.value_omitted_from_data({"mystream-count": "0"}, {}, "mystream")
)
self.assertFalse(
block.value_omitted_from_data(
{
"mystream-count": "1",
"mystream-0-type": "heading",
"mystream-0-value": "hello",
"mystream-0-deleted": "",
"mystream-0-order": "0",
},
{},
"mystream",
)
)
self.assertTrue(
block.value_omitted_from_data({"nothing-here": "nope"}, {}, "mystream")
)
def test_validation_errors(self):
class ValidatedBlock(blocks.StreamBlock):
char = blocks.CharBlock()
url = blocks.URLBlock()
block = ValidatedBlock()
value = blocks.StreamValue(
block,
[
("char", ""),
("char", "foo"),
("url", "http://example.com/"),
("url", "not a url"),
],
)
with self.assertRaises(ValidationError) as catcher:
block.clean(value)
self.assertEqual(
catcher.exception.as_json_data(),
{
"blockErrors": {
0: {"messages": ["This field is required."]},
3: {"messages": ["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.as_json_data(),
{"messages": ["The minimum number of items is 1"]},
)
# a value with >= 1 blocks should pass validation
value = blocks.StreamValue(block, [("char", "foo")])
self.assertTrue(block.clean(value))
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.as_json_data(),
{"messages": ["The maximum number of items is 1"]},
)
# a value with 1 block should pass validation
value = blocks.StreamValue(block, [("char", "foo")])
self.assertTrue(block.clean(value))
def test_block_counts_min_validation_errors(self):
class ValidatedBlock(blocks.StreamBlock):
char = blocks.CharBlock()
url = blocks.URLBlock()
block = ValidatedBlock(block_counts={"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.as_json_data(),
{"messages": ["Char: The minimum number of items is 1"]},
)
# a value with 1 char block should pass validation
value = blocks.StreamValue(
block,
[
("url", "http://example.com/"),
("char", "foo"),
("url", "http://example.com/"),
],
)
self.assertTrue(block.clean(value))
def test_block_counts_max_validation_errors(self):
class ValidatedBlock(blocks.StreamBlock):
char = blocks.CharBlock()
url = blocks.URLBlock()
block = ValidatedBlock(block_counts={"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.as_json_data(),
{"messages": ["Char: The maximum number of items is 1"]},
)
# a value with 1 char block should pass validation
value = blocks.StreamValue(
block,
[
("char", "foo"),
("url", "http://example.com/"),
("url", "http://example.com/"),
],
)
self.assertTrue(block.clean(value))
def test_ordering_in_form_submission_uses_order_field(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock()
# check that items are ordered by the 'order' field, not the order they appear in the form
post_data = {"article-count": "3"}
for i in range(0, 3):
post_data.update(
{
"article-%d-deleted" % i: "",
"article-%d-order" % i: str(2 - i),
"article-%d-type" % i: "heading",
"article-%d-value" % i: "heading %d" % i,
"article-%d-id" % i: "000%d" % i,
}
)
block_value = block.value_from_datadict(post_data, {}, "article")
self.assertEqual(block_value[2].value, "heading 0")
self.assertEqual(block_value[2].id, "0000")
def test_ordering_in_form_submission_is_numeric(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock()
# check that items are ordered by 'order' numerically, not alphabetically
post_data = {"article-count": "12"}
for i in range(0, 12):
post_data.update(
{
"article-%d-deleted" % i: "",
"article-%d-order" % i: str(i),
"article-%d-type" % i: "heading",
"article-%d-value" % i: "heading %d" % i,
}
)
block_value = block.value_from_datadict(post_data, {}, "article")
self.assertEqual(block_value[2].value, "heading 2")
def test_searchable_content(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock()
value = block.to_python(
[
{
"type": "heading",
"value": "My title",
},
{
"type": "paragraph",
"value": "My first paragraph",
},
{
"type": "paragraph",
"value": "My second paragraph",
},
]
)
content = block.get_searchable_content(value)
self.assertEqual(
content,
[
"My title",
"My first paragraph",
"My second paragraph",
],
)
def test_meta_default(self):
"""Test that we can specify a default value in the Meta of a StreamBlock"""
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
class Meta:
default = [("heading", "A default heading")]
# to access the default value, we retrieve it through a StructBlock
# from a struct value that's missing that key
class ArticleContainerBlock(blocks.StructBlock):
author = blocks.CharBlock()
article = ArticleBlock()
block = ArticleContainerBlock()
struct_value = block.to_python({"author": "Bob"})
stream_value = struct_value["article"]
self.assertIsInstance(stream_value, blocks.StreamValue)
self.assertEqual(len(stream_value), 1)
self.assertEqual(stream_value[0].block_type, "heading")
self.assertEqual(stream_value[0].value, "A default heading")
def test_constructor_default(self):
"""Test that we can specify a default value in the constructor of a StreamBlock"""
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
class Meta:
default = [("heading", "A default heading")]
# to access the default value, we retrieve it through a StructBlock
# from a struct value that's missing that key
class ArticleContainerBlock(blocks.StructBlock):
author = blocks.CharBlock()
article = ArticleBlock(default=[("heading", "A different default heading")])
block = ArticleContainerBlock()
struct_value = block.to_python({"author": "Bob"})
stream_value = struct_value["article"]
self.assertIsInstance(stream_value, blocks.StreamValue)
self.assertEqual(len(stream_value), 1)
self.assertEqual(stream_value[0].block_type, "heading")
self.assertEqual(stream_value[0].value, "A different default heading")
def test_stream_value_equality(self):
block = blocks.StreamBlock(
[
("text", blocks.CharBlock()),
]
)
value1 = block.to_python([{"type": "text", "value": "hello"}])
value2 = block.to_python([{"type": "text", "value": "hello"}])
value3 = block.to_python([{"type": "text", "value": "goodbye"}])
self.assertEqual(value1, value2)
self.assertNotEqual(value1, value3)
def test_adapt_considers_group_attribute(self):
"""If group attributes are set in Block Meta classes, make sure the blocks are grouped together"""
class Group1Block1(blocks.CharBlock):
class Meta:
group = "group1"
class Group1Block2(blocks.CharBlock):
class Meta:
group = "group1"
class Group2Block1(blocks.CharBlock):
class Meta:
group = "group2"
class Group2Block2(blocks.CharBlock):
class Meta:
group = "group2"
class NoGroupBlock(blocks.CharBlock):
pass
block = blocks.StreamBlock(
[
("b1", Group1Block1()),
("b2", Group1Block2()),
("b3", Group2Block1()),
("b4", Group2Block2()),
("ngb", NoGroupBlock()),
]
)
block.set_name("test_streamblock")
js_args = StreamBlockAdapter().js_args(block)
blockdefs_dict = dict(js_args[1])
self.assertEqual(blockdefs_dict.keys(), {"", "group1", "group2"})
def test_value_from_datadict(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock()
value = block.value_from_datadict(
{
"foo-count": "3",
"foo-0-deleted": "",
"foo-0-order": "2",
"foo-0-type": "heading",
"foo-0-id": "0000",
"foo-0-value": "this is my heading",
"foo-1-deleted": "1",
"foo-1-order": "1",
"foo-1-type": "heading",
"foo-1-id": "0001",
"foo-1-value": "a deleted heading",
"foo-2-deleted": "",
"foo-2-order": "0",
"foo-2-type": "paragraph",
"foo-2-id": "",
"foo-2-value": "
")
self.assertEqual(value[1].block_type, "heading")
self.assertEqual(value[1].id, "0000")
self.assertEqual(value[1].value, "this is my heading")
def check_get_prep_value(self, stream_data, is_lazy):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
block = ArticleBlock()
value = blocks.StreamValue(block, stream_data, is_lazy=is_lazy)
jsonish_value = block.get_prep_value(value)
self.assertEqual(len(jsonish_value), 2)
self.assertEqual(
jsonish_value[0],
{"type": "heading", "value": "this is my heading", "id": "0000"},
)
self.assertEqual(jsonish_value[1]["type"], "paragraph")
self.assertEqual(jsonish_value[1]["value"], "
this is a paragraph
")
# get_prep_value should assign a new (random and non-empty)
# ID to this block, as it didn't have one already.
self.assertTrue(jsonish_value[1]["id"])
# Calling get_prep_value again should preserve existing IDs, including the one
# just assigned to block 1
jsonish_value_again = block.get_prep_value(value)
self.assertEqual(jsonish_value[0]["id"], jsonish_value_again[0]["id"])
self.assertEqual(jsonish_value[1]["id"], jsonish_value_again[1]["id"])
def test_get_prep_value_not_lazy(self):
stream_data = [
("heading", "this is my heading", "0000"),
("paragraph", "
", result)
class TestIncludeBlockTag(TestCase):
def test_include_block_tag_with_boundblock(self):
"""
The include_block tag should be able to render a BoundBlock's template
while keeping the parent template's context
"""
block = blocks.CharBlock(template="tests/blocks/heading_block.html")
bound_block = block.bind("bonjour")
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": bound_block,
"language": "fr",
},
)
self.assertIn('
bonjour
', result)
def test_include_block_tag_with_structvalue(self):
"""
The include_block tag should be able to render a StructValue's template
while keeping the parent template's context
"""
block = SectionBlock()
struct_value = block.to_python(
{"title": "Bonjour", "body": "monde italique"}
)
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": struct_value,
"language": "fr",
},
)
self.assertIn(
"""
Bonjour
monde italique""", result
)
def test_include_block_tag_with_streamvalue(self):
"""
The include_block tag should be able to render a StreamValue's template
while keeping the parent template's context
"""
block = blocks.StreamBlock(
[
(
"heading",
blocks.CharBlock(template="tests/blocks/heading_block.html"),
),
("paragraph", blocks.CharBlock()),
],
template="tests/blocks/stream_with_language.html",
)
stream_value = block.to_python([{"type": "heading", "value": "Bonjour"}])
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": stream_value,
"language": "fr",
},
)
self.assertIn(
'
Bonjour
', result
)
def test_include_block_tag_with_plain_value(self):
"""
The include_block tag should be able to render a value without a render_as_block method
by just rendering it as a string
"""
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": 42,
},
)
self.assertIn("42", result)
def test_include_block_tag_with_filtered_value(self):
"""
The block parameter on include_block tag should support complex values including filters,
e.g. {% include_block foo|default:123 %}
"""
block = blocks.CharBlock(template="tests/blocks/heading_block.html")
bound_block = block.bind("bonjour")
result = render_to_string(
"tests/blocks/include_block_test_with_filter.html",
{
"test_block": bound_block,
"language": "fr",
},
)
self.assertIn('
bonjour
', result)
result = render_to_string(
"tests/blocks/include_block_test_with_filter.html",
{
"test_block": None,
"language": "fr",
},
)
self.assertIn("999", result)
def test_include_block_tag_with_extra_context(self):
"""
Test that it's possible to pass extra context on an include_block tag using
{% include_block foo with classname="bar" %}
"""
block = blocks.CharBlock(template="tests/blocks/heading_block.html")
bound_block = block.bind("bonjour")
result = render_to_string(
"tests/blocks/include_block_with_test.html",
{
"test_block": bound_block,
"language": "fr",
},
)
self.assertIn(
'
bonjour
', result
)
def test_include_block_tag_with_only_flag(self):
"""
A tag such as {% include_block foo with classname="bar" only %}
should not inherit the parent context
"""
block = blocks.CharBlock(template="tests/blocks/heading_block.html")
bound_block = block.bind("bonjour")
result = render_to_string(
"tests/blocks/include_block_only_test.html",
{
"test_block": bound_block,
"language": "fr",
},
)
self.assertIn('
bonjour
', result)
def test_include_block_html_escaping(self):
"""
Output of include_block should be escaped as per Django autoescaping rules
"""
block = blocks.CharBlock()
bound_block = block.bind(block.to_python("some evil HTML"))
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": bound_block,
},
)
self.assertIn("some <em>evil</em> HTML", result)
# {% autoescape off %} should be respected
result = render_to_string(
"tests/blocks/include_block_autoescape_off_test.html",
{
"test_block": bound_block,
},
)
self.assertIn("some evil HTML", result)
# The same escaping should be applied when passed a plain value rather than a BoundBlock -
# a typical situation where this would occur would be rendering an item of a StructBlock,
# e.g. {% include_block person_block.first_name %} as opposed to
# {% include_block person_block.bound_blocks.first_name %}
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": "some evil HTML",
},
)
self.assertIn("some <em>evil</em> HTML", result)
result = render_to_string(
"tests/blocks/include_block_autoescape_off_test.html",
{
"test_block": "some evil HTML",
},
)
self.assertIn("some evil HTML", result)
# Blocks that explicitly return 'safe HTML'-marked values (such as RawHTMLBlock) should
# continue to produce unescaped output
block = blocks.RawHTMLBlock()
bound_block = block.bind(block.to_python("some evil HTML"))
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": bound_block,
},
)
self.assertIn("some evil HTML", result)
# likewise when applied to a plain 'safe HTML' value rather than a BoundBlock
result = render_to_string(
"tests/blocks/include_block_test.html",
{
"test_block": mark_safe("some evil HTML"),
},
)
self.assertIn("some evil HTML", result)
class TestOverriddenGetTemplateBlockTag(TestCase):
def test_get_template_old_signature(self):
class BlockUsingGetTemplateMethod(blocks.Block):
my_new_template = "tests/blocks/heading_block.html"
def get_template(self, context=None):
return self.my_new_template
block = BlockUsingGetTemplateMethod(
template="tests/blocks/this_shouldnt_be_used.html"
)
with self.assertWarnsMessage(
RemovedInWagtail70Warning,
"BlockUsingGetTemplateMethod.get_template should accept a 'value' argument as first argument",
):
html = block.render("Hello World")
self.assertEqual(html, "
Hello World
")
def test_block_render_passes_the_value_argument_to_get_template(self):
"""verifies Block.render() passes the value to get_template"""
class BlockChoosingTemplateBasedOnValue(blocks.Block):
def get_template(self, value=None, context=None):
if value == "HEADING":
return "tests/blocks/heading_block.html"
return None # using render_basic
block = BlockChoosingTemplateBasedOnValue()
html = block.render("Hello World")
self.assertEqual(html, "Hello World")
html = block.render("HEADING")
self.assertEqual(html, "
HEADING
")
class TestValidationErrorAsJsonData(TestCase):
def test_plain_validation_error(self):
error = ValidationError("everything is broken")
self.assertEqual(
get_error_json_data(error), {"messages": ["everything is broken"]}
)
def test_validation_error_with_multiple_messages(self):
error = ValidationError(
[
ValidationError("everything is broken"),
ValidationError("even more broken than before"),
]
)
self.assertEqual(
get_error_json_data(error),
{"messages": ["everything is broken", "even more broken than before"]},
)
def test_structblock_validation_error(self):
error = StructBlockValidationError(
block_errors={
"name": ErrorList(
[
ValidationError("This field is required."),
]
)
},
non_block_errors=ErrorList(
[ValidationError("Either email or telephone number must be specified.")]
),
)
self.assertEqual(
get_error_json_data(error),
{
"blockErrors": {
"name": {"messages": ["This field is required."]},
},
"messages": [
"Either email or telephone number must be specified.",
],
},
)
def test_structblock_validation_error_with_no_block_errors(self):
error = StructBlockValidationError(
non_block_errors=[
ValidationError("Either email or telephone number must be specified.")
]
)
self.assertEqual(
get_error_json_data(error),
{
"messages": [
"Either email or telephone number must be specified.",
],
},
)
def test_structblock_validation_error_with_no_non_block_errors(self):
error = StructBlockValidationError(
block_errors={
"name": ValidationError("This field is required."),
},
)
self.assertEqual(
get_error_json_data(error),
{
"blockErrors": {
"name": {"messages": ["This field is required."]},
},
},
)
def test_streamblock_validation_error(self):
error = StreamBlockValidationError(
block_errors={
2: ErrorList(
[
StructBlockValidationError(
non_block_errors=ErrorList(
[
ValidationError(
"Either email or telephone number must be specified."
)
]
)
)
]
),
4: ErrorList([ValidationError("This field is required.")]),
6: ErrorList(
[
StructBlockValidationError(
block_errors={
"name": ErrorList(
[ValidationError("This field is required.")]
),
}
)
]
),
},
non_block_errors=ErrorList(
[
ValidationError("The minimum number of items is 2"),
ValidationError("The maximum number of items is 5"),
]
),
)
self.assertEqual(
get_error_json_data(error),
{
"blockErrors": {
2: {
"messages": [
"Either email or telephone number must be specified."
]
},
4: {"messages": ["This field is required."]},
6: {
"blockErrors": {
"name": {
"messages": ["This field is required."],
}
}
},
},
"messages": [
"The minimum number of items is 2",
"The maximum number of items is 5",
],
},
)
def test_streamblock_validation_error_with_no_block_errors(self):
error = StreamBlockValidationError(
non_block_errors=[
ValidationError("The minimum number of items is 2"),
],
)
self.assertEqual(
get_error_json_data(error),
{
"messages": [
"The minimum number of items is 2",
],
},
)
def test_streamblock_validation_error_with_no_non_block_errors(self):
error = StreamBlockValidationError(
block_errors={
4: [ValidationError("This field is required.")],
6: ValidationError("This field is required."),
},
)
self.assertEqual(
get_error_json_data(error),
{
"blockErrors": {
4: {"messages": ["This field is required."]},
6: {"messages": ["This field is required."]},
}
},
)
def test_listblock_validation_error_constructed_with_list(self):
# test the pre-Wagtail-5.0 constructor format for ListBlockValidationError:
# block_errors passed as a list with None for 'no error', and
# a single-item ErrorList for validation errors
error = ListBlockValidationError(
block_errors=[
None,
ErrorList(
[
StructBlockValidationError(
non_block_errors=ErrorList(
[
ValidationError(
"Either email or telephone number must be specified."
)
]
)
)
]
),
ErrorList(
[
StructBlockValidationError(
block_errors={
"name": ErrorList(
[ValidationError("This field is required.")]
),
}
)
]
),
],
non_block_errors=ErrorList(
[
ValidationError("The minimum number of items is 2"),
ValidationError("The maximum number of items is 5"),
]
),
)
self.assertEqual(
get_error_json_data(error),
{
"blockErrors": {
1: {
"messages": [
"Either email or telephone number must be specified."
]
},
2: {
"blockErrors": {
"name": {
"messages": ["This field is required."],
}
}
},
},
"messages": [
"The minimum number of items is 2",
"The maximum number of items is 5",
],
},
)
def test_listblock_validation_error_constructed_with_dict(self):
# test the Wagtail >=5.0 constructor format for ListBlockValidationError:
# block_errors passed as a dict keyed by block index, where values can be
# ValidationErrors and plain single-item lists as well as single-item ErrorLists
error = ListBlockValidationError(
block_errors={
1: [
StructBlockValidationError(
non_block_errors=ErrorList(
[
ValidationError(
"Either email or telephone number must be specified."
)
]
)
)
],
2: StructBlockValidationError(
block_errors={
"name": ErrorList([ValidationError("This field is required.")]),
}
),
},
non_block_errors=ErrorList(
[
ValidationError("The minimum number of items is 2"),
ValidationError("The maximum number of items is 5"),
]
),
)
self.assertEqual(
get_error_json_data(error),
{
"blockErrors": {
1: {
"messages": [
"Either email or telephone number must be specified."
]
},
2: {
"blockErrors": {
"name": {
"messages": ["This field is required."],
}
}
},
},
"messages": [
"The minimum number of items is 2",
"The maximum number of items is 5",
],
},
)
def test_listblock_validation_error_with_no_non_block_errors(self):
error = ListBlockValidationError(
block_errors={2: ValidationError("This field is required.")},
)
self.assertEqual(
get_error_json_data(error),
{
"blockErrors": {
2: {"messages": ["This field is required."]},
},
},
)
def test_listblock_validation_error_with_no_block_errors(self):
error = ListBlockValidationError(
non_block_errors=[
ValidationError("The minimum number of items is 2"),
]
)
self.assertEqual(
get_error_json_data(error),
{
"messages": [
"The minimum number of items is 2",
],
},
)
class TestBlockDefinitionLookup(TestCase):
def test_simple_lookup(self):
lookup = BlockDefinitionLookup(
{
0: ("wagtail.blocks.CharBlock", [], {"required": True}),
1: ("wagtail.blocks.RichTextBlock", [], {}),
}
)
char_block = lookup.get_block(0)
char_block.set_name("title")
self.assertIsInstance(char_block, blocks.CharBlock)
self.assertTrue(char_block.required)
rich_text_block = lookup.get_block(1)
self.assertIsInstance(rich_text_block, blocks.RichTextBlock)
# A subsequent call to get_block with the same index should return a new instance;
# this ensures that state changes such as set_name are independent of other blocks
char_block_2 = lookup.get_block(0)
char_block_2.set_name("subtitle")
self.assertIsInstance(char_block, blocks.CharBlock)
self.assertTrue(char_block.required)
self.assertIsNot(char_block, char_block_2)
self.assertEqual(char_block.name, "title")
self.assertEqual(char_block_2.name, "subtitle")
def test_structblock_lookup(self):
lookup = BlockDefinitionLookup(
{
0: ("wagtail.blocks.CharBlock", [], {"required": True}),
1: ("wagtail.blocks.RichTextBlock", [], {}),
2: (
"wagtail.blocks.StructBlock",
[
[
("title", 0),
("description", 1),
],
],
{},
),
}
)
struct_block = lookup.get_block(2)
self.assertIsInstance(struct_block, blocks.StructBlock)
title_block = struct_block.child_blocks["title"]
self.assertIsInstance(title_block, blocks.CharBlock)
self.assertTrue(title_block.required)
description_block = struct_block.child_blocks["description"]
self.assertIsInstance(description_block, blocks.RichTextBlock)
def test_streamblock_lookup(self):
lookup = BlockDefinitionLookup(
{
0: ("wagtail.blocks.CharBlock", [], {"required": True}),
1: ("wagtail.blocks.RichTextBlock", [], {}),
2: (
"wagtail.blocks.StreamBlock",
[
[
("heading", 0),
("paragraph", 1),
],
],
{},
),
}
)
stream_block = lookup.get_block(2)
self.assertIsInstance(stream_block, blocks.StreamBlock)
title_block = stream_block.child_blocks["heading"]
self.assertIsInstance(title_block, blocks.CharBlock)
self.assertTrue(title_block.required)
description_block = stream_block.child_blocks["paragraph"]
self.assertIsInstance(description_block, blocks.RichTextBlock)
def test_listblock_lookup(self):
lookup = BlockDefinitionLookup(
{
0: ("wagtail.blocks.CharBlock", [], {"required": True}),
1: ("wagtail.blocks.ListBlock", [0], {}),
}
)
list_block = lookup.get_block(1)
self.assertIsInstance(list_block, blocks.ListBlock)
list_item_block = list_block.child_block
self.assertIsInstance(list_item_block, blocks.CharBlock)
self.assertTrue(list_item_block.required)
# Passing a class as the child block is still valid; this is not converted
# to a reference
lookup = BlockDefinitionLookup(
{
0: ("wagtail.blocks.ListBlock", [blocks.CharBlock], {}),
}
)
list_block = lookup.get_block(0)
self.assertIsInstance(list_block, blocks.ListBlock)
list_item_block = list_block.child_block
self.assertIsInstance(list_item_block, blocks.CharBlock)