diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index ab8093a80a..4f40b873f9 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -22,6 +22,7 @@ Changelog
* Optimized preview data storage (Bertrand Bordage)
* Added `render_landing_page` method to `AbstractForm` to be easily overridden and pass `form_submission` to landing page context (Stein Strindhaug)
* Added `heading` kwarg to `InlinePanel` to allow heading to be set independently of button label (Adrian Turjak)
+ * The value type returned from a `StructBlock` can now be customised (LB (Ben Johnston))
* Fix: Do not remove stopwords when generating slugs from non-ASCII titles, to avoid issues with incorrect word boundaries (Sævar Öfjörð Magnússon)
* Fix: The PostgreSQL search backend now preserves ordering of the `QuerySet` when searching with `order_by_relevance=False` (Bertrand Bordage)
* Fix: Using `modeladmin_register` as a decorator no longer replaces the decorated class with `None` (Tim Heap)
diff --git a/docs/releases/2.0.rst b/docs/releases/2.0.rst
index 2835d701b1..42c0480b0b 100644
--- a/docs/releases/2.0.rst
+++ b/docs/releases/2.0.rst
@@ -35,6 +35,7 @@ Other features
* Optimized preview data storage (Bertrand Bordage)
* Added ``render_landing_page`` method to ``AbstractForm`` to be easily overridden and pass ``form_submission`` to landing page context (Stein Strindhaug)
* Added ``heading`` kwarg to ``InlinePanel`` to allow heading to be set independently of button label (Adrian Turjak)
+ * The value type returned from a ``StructBlock`` can now be customised. See :ref:`custom_value_class_for_structblock` (LB (Ben Johnston))
Bug fixes
~~~~~~~~~
diff --git a/docs/topics/streamfield.rst b/docs/topics/streamfield.rst
index 935d753297..4e1ae11876 100644
--- a/docs/topics/streamfield.rst
+++ b/docs/topics/streamfield.rst
@@ -377,6 +377,9 @@ This defines ``PersonBlock()`` as a block type that can be re-used as many times
Further options are available for customising the display of a ``StructBlock`` within the page editor - see :ref:`custom_editing_interfaces_for_structblock`.
+You can also customise how the value of a ``StructBlock`` is prepared for using in templates - see :ref:`custom_value_class_for_structblock`.
+
+
ListBlock
~~~~~~~~~
@@ -848,6 +851,81 @@ To add additional variables, you can override the block's ``get_form_context`` m
form_template = 'myapp/block_forms/person.html'
+.. _custom_value_class_for_structblock:
+
+Custom value class for ``StructBlock``
+--------------------------------------
+
+To customise the methods available for a ``StructBlock`` value, you can specify a ``value_class`` attribute (either as a keyword argument to the ``StructBlock`` constructor, or in a subclass's ``Meta``) to override how the value is prepared.
+
+This ``value_class`` must be a subclass of ``StructValue``, any additional methods can access the value from sub-blocks via the block key on ``self`` (e.g. ``self.get('my_block')``).
+
+Example:
+
+.. code-block:: python
+
+ from wagtail.core.models import Page
+ from wagtail.core.blocks import (
+ CharBlock, PageChooserBlock, StructValue, StructBlock, TextBlock, URLBlock)
+
+
+ class LinkStructValue(StructValue):
+ def url(self):
+ external_url = self.get('external_url')
+ page = self.get('page')
+ if external_url:
+ return external_url
+ elif page:
+ return page.url
+
+
+ class QuickLinkBlock(StructBlock):
+ text = CharBlock(label="link text", required=True)
+ page = PageChooserBlock(label="page", required=False)
+ external_url = URLBlock(label="external URL", required=False)
+
+ class Meta:
+ icon = 'site'
+ value_class = LinkStructValue
+
+
+ class MyPage(Page):
+ quick_links = StreamField([('links', QuickLinkBlock())], blank=True)
+ quotations = StreamField([('quote', StructBlock([
+ ('quote', TextBlock(required=True)),
+ ('page', PageChooserBlock(required=False)),
+ ('external_url', URLBlock(required=False)),
+ ], icon='openquote', value_class=LinkStructValue))], blank=True)
+
+ content_panels = Page.content_panels + [
+ StreamFieldPanel('quick_links'),
+ StreamFieldPanel('quotations'),
+ ]
+
+
+
+Your extended value class methods will be available in your template:
+
+.. code-block:: html+django
+
+ {% load wagtailcore_tags %}
+
+
+
+
+ {% for quotation in page.quotations %}
+
+ {{ quotation.value.quote }}
+
+ {% endfor %}
+
+
+
+
Custom block types
------------------
diff --git a/wagtail/core/blocks/struct_block.py b/wagtail/core/blocks/struct_block.py
index 53d514677d..f771effb92 100644
--- a/wagtail/core/blocks/struct_block.py
+++ b/wagtail/core/blocks/struct_block.py
@@ -14,6 +14,26 @@ from .utils import js_dict
__all__ = ['BaseStructBlock', 'StructBlock', 'StructValue']
+class StructValue(collections.OrderedDict):
+ """ A class that generates a StructBlock value from provded sub-blocks """
+ def __init__(self, block, *args):
+ super().__init__(*args)
+ self.block = block
+
+ def __html__(self):
+ return self.block.render(self)
+
+ def render_as_block(self, context=None):
+ return self.block.render(self, context=context)
+
+ @cached_property
+ def bound_blocks(self):
+ return collections.OrderedDict([
+ (name, block.bind(self.get(name)))
+ for name, block in self.block.child_blocks.items()
+ ])
+
+
class BaseStructBlock(Block):
def __init__(self, local_blocks=None, **kwargs):
@@ -42,7 +62,7 @@ class BaseStructBlock(Block):
rather than a StructValue; for consistency, we need to convert it to a StructValue
for StructBlock to work with
"""
- return StructValue(self, self.meta.default.items())
+ return self._to_struct_value(self.meta.default.items())
def js_initializer(self):
# skip JS setup entirely if no children have js_initializers
@@ -88,7 +108,7 @@ class BaseStructBlock(Block):
return render_to_string(self.meta.form_template, context)
def value_from_datadict(self, data, files, prefix):
- return StructValue(self, [
+ return self._to_struct_value([
(name, block.value_from_datadict(data, files, '%s-%s' % (prefix, name)))
for name, block in self.child_blocks.items()
])
@@ -113,11 +133,11 @@ class BaseStructBlock(Block):
# and delegate the errors contained in the 'params' dict to the child blocks instead
raise ValidationError('Validation error in StructBlock', params=errors)
- return StructValue(self, result)
+ return self._to_struct_value(result)
def to_python(self, value):
- # recursively call to_python on children and return as a StructValue
- return StructValue(self, [
+ """ Recursively call to_python on children and return as a StructValue """
+ return self._to_struct_value([
(
name,
(child_block.to_python(value[name]) if name in value else child_block.get_default())
@@ -127,15 +147,19 @@ class BaseStructBlock(Block):
for name, child_block in self.child_blocks.items()
])
+ def _to_struct_value(self, block_items):
+ """ Return a Structvalue representation of the sub-blocks in this block """
+ return self.meta.value_class(self, block_items)
+
def get_prep_value(self, value):
- # recursively call get_prep_value on children and return as a plain dict
+ """ Recursively call get_prep_value on children and return as a plain dict """
return dict([
(name, self.child_blocks[name].get_prep_value(val))
for name, val in value.items()
])
def get_api_representation(self, value, context=None):
- # recursively call get_api_representation on children and return as a plain dict
+ """ Recursively call get_api_representation on children and return as a plain dict """
return dict([
(name, self.child_blocks[name].get_api_representation(val, context=context))
for name, val in value.items()
@@ -179,6 +203,7 @@ class BaseStructBlock(Block):
default = {}
form_classname = 'struct-block'
form_template = 'wagtailadmin/block_forms/struct.html'
+ value_class = StructValue
# No icon specified here, because that depends on the purpose that the
# block is being used for. Feel encouraged to specify an icon in your
# descendant block type
@@ -187,22 +212,3 @@ class BaseStructBlock(Block):
class StructBlock(BaseStructBlock, metaclass=DeclarativeSubBlocksMetaclass):
pass
-
-
-class StructValue(collections.OrderedDict):
- def __init__(self, block, *args):
- super().__init__(*args)
- self.block = block
-
- def __html__(self):
- return self.block.render(self)
-
- def render_as_block(self, context=None):
- return self.block.render(self, context=context)
-
- @cached_property
- def bound_blocks(self):
- return collections.OrderedDict([
- (name, block.bind(self.get(name)))
- for name, block in self.block.child_blocks.items()
- ])
diff --git a/wagtail/core/tests/test_blocks.py b/wagtail/core/tests/test_blocks.py
index d6f23801c3..858f7d22ec 100644
--- a/wagtail/core/tests/test_blocks.py
+++ b/wagtail/core/tests/test_blocks.py
@@ -16,13 +16,13 @@ from django.utils.html import format_html
from django.utils.safestring import SafeData, mark_safe
from django.utils.translation import ugettext_lazy as __
+from wagtail.core import blocks
+from wagtail.core.models import Page
+from wagtail.core.rich_text import RichText
from wagtail.tests.testapp.blocks import LinkBlock as CustomLinkBlock
from wagtail.tests.testapp.blocks import SectionBlock
from wagtail.tests.testapp.models import EventPage, SimplePage
from wagtail.tests.utils import WagtailTestUtils
-from wagtail.core import blocks
-from wagtail.core.models import Page
-from wagtail.core.rich_text import RichText
class FooStreamBlock(blocks.StreamBlock):
@@ -1348,6 +1348,173 @@ class TestStructBlock(SimpleTestCase):
self.assertEqual(result, """Bonjour
monde italique
""")
+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.assertTrue(isinstance(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 StylingMixinStructValue(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')
+
+
class TestListBlock(WagtailTestUtils, SimpleTestCase):
def test_initialise_with_class(self):
block = blocks.ListBlock(blocks.CharBlock)
diff --git a/wagtail/tests/testapp/templates/tests/blocks/struct_block_custom_value.html b/wagtail/tests/testapp/templates/tests/blocks/struct_block_custom_value.html
new file mode 100644
index 0000000000..7fff5d7127
--- /dev/null
+++ b/wagtail/tests/testapp/templates/tests/blocks/struct_block_custom_value.html
@@ -0,0 +1 @@
+{{ value.title_with_suffix }}