kopia lustrzana https://github.com/wagtail/wagtail
Add ability to set custom value_class on StructBlock
rodzic
7a7b6748db
commit
4ea8365203
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~
|
||||
|
|
|
@ -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
|
||||
------------------
|
||||
|
||||
|
|
|
@ -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()
|
||||
])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<div>{{ value.title_with_suffix }}</div>
|
Ładowanie…
Reference in New Issue