Add ability to set custom value_class on StructBlock

pull/4128/head
LB 2017-12-11 13:34:56 +08:00 zatwierdzone przez Matt Westcott
rodzic 7a7b6748db
commit 4ea8365203
6 zmienionych plików z 283 dodań i 29 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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
~~~~~~~~~

Wyświetl plik

@ -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 %}
<ul>
{% for link in page.quick_links %}
<li><a href="{{ link.value.url }}">{{ link.value.text }}</a></li>
{% endfor %}
</ul>
<div>
{% for quotation in page.quotations %}
<blockquote cite="{{ quotation.value.url }}">
{{ quotation.value.quote }}
</blockquote>
{% endfor %}
</div>
Custom block types
------------------

Wyświetl plik

@ -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()
])

Wyświetl plik

@ -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, """<h1 lang="fr">Bonjour</h1><div class="rich-text">monde <i>italique</i></div>""")
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': '<b>world</b>'})
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, '<div>SUFFIX hello</div>\n')
struct_value = block.to_python({})
html = block.render(struct_value)
self.assertEqual(html, '<div>EMPTY TITLE</div>\n')
class TestListBlock(WagtailTestUtils, SimpleTestCase):
def test_initialise_with_class(self):
block = blocks.ListBlock(blocks.CharBlock)

Wyświetl plik

@ -0,0 +1 @@
<div>{{ value.title_with_suffix }}</div>