Add formal support for customising the form rendering of StructBlocks

The `form_template` attribute was mentioned in passing in the docs, but was missing various things
to make it fully useful:

- context passed to form_template now includes 'prefix' and 'block_definition'
- context for the form is now populated in a separate overrideable `get_form_context` method
- full documentation and tests for form_template and get_form_context added
pull/2274/merge
Matt Westcott 2016-02-24 11:57:07 +02:00
rodzic 9f72e12159
commit 9961455c6a
7 zmienionych plików z 119 dodań i 24 usunięć
docs
wagtail
tests/testapp
templates/tests/block_forms
wagtailcore

Wyświetl plik

@ -18,6 +18,7 @@ Changelog
* Added a new FloatBlock, DecimalBlock and a RegexBlock (Oktay Altay, Andy Babic)
* Wagtail version number is now shown on the settings menu (Chris Rogers)
* Added a system check to validate that fields listed in `search_fields` are defined on the model (Josh Schneier)
* Added formal APIs for customising the display of StructBlock forms within the page editor (Matt Westcott)
* Fix: Email templates and document uploader now support custom `STATICFILES_STORAGE` (Jonny Scholes)
* Fix: Removed alignment options (deprecated in HTML and not rendered by Wagtail) from `TableBlock` context menu (Moritz Pfeiffer)
* Fix: Fixed incorrect CSS path on ModelAdmin's "choose a parent page" view

Wyświetl plik

@ -33,6 +33,7 @@ Minor features
* Added a new FloatBlock, DecimalBlock and a RegexBlock (Oktay Altay, Andy Babic)
* Wagtail version number is now shown on the settings menu (Chris Rogers)
* Added a system check to validate that fields listed in ``search_fields`` are defined on the model (Josh Schneier)
* Added formal APIs for customising the display of StructBlock forms within the page editor - see :ref:`custom_editing_interfaces_for_structblock` (Matt Westcott)
Bug fixes

Wyświetl plik

@ -111,7 +111,7 @@ FloatBlock
A single-line Float input that validates that the value is a valid floating point number. The keyword arguments ``required``, ``max_value`` and ``min_value`` are accepted.
DecimalBlock
~~~~~~~~~~
~~~~~~~~~~~~
``wagtail.wagtailcore.blocks.DecimalBlock``
@ -317,23 +317,7 @@ This defines ``PersonBlock()`` as a block type that can be re-used as many times
('person', PersonBlock()),
])
To customise the styling of the block as it appears in the page editor, your subclass can specify a ``form_classname`` attribute in ``Meta`` to override the default value of ``struct-block``:
.. code-block:: python
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock(required=True)
surname = blocks.CharBlock(required=True)
photo = ImageChooserBlock()
biography = blocks.RichTextBlock()
class Meta:
icon = 'user'
form_classname = 'person-block struct-block'
You can then provide custom CSS for this block, targeted at the specified classname, by using the ``insert_editor_css`` hook (see :doc:`Hooks </reference/hooks>`). For more extensive customisations that require changes to the HTML markup as well, you can override the ``form_template`` attribute in ``Meta``.
Further options are available for customising the display of a ``StructBlock`` within the page editor - see :ref:`custom_editing_interfaces_for_structblock`.
ListBlock
@ -636,6 +620,65 @@ This is possible because the HTML rendering behaviour of these blocks does not i
then writing ``{{ value.guest_speaker }}`` within the EventBlock's template will use the template rendering from ``blocks/speaker.html`` for that field.
.. _custom_editing_interfaces_for_structblock:
Custom editing interfaces for ``StructBlock``
---------------------------------------------
To customise the styling of a ``StructBlock`` as it appears in the page editor, you can specify a ``form_classname`` attribute (either as a keyword argument to the ``StructBlock`` constructor, or in a subclass's ``Meta``) to override the default value of ``struct-block``:
.. code-block:: python
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock(required=True)
surname = blocks.CharBlock(required=True)
photo = ImageChooserBlock()
biography = blocks.RichTextBlock()
class Meta:
icon = 'user'
form_classname = 'person-block struct-block'
You can then provide custom CSS for this block, targeted at the specified classname, by using the :ref:`insert_editor_css` hook.
For more extensive customisations that require changes to the HTML markup as well, you can override the ``form_template`` attribute in ``Meta`` to specify your own template path. The following variables are available on this template:
``children``
An ``OrderedDict`` of ``BoundBlock``\s for all of the child blocks making up this ``StructBlock``; typically your template will call ``render_form`` on each of these.
``help_text``
The help text for this block, if specified.
``classname``
The class name passed as ``form_classname`` (defaults to ``struct-block``).
``block_definition``
The ``StructBlock`` instance that defines this block.
``prefix``
The prefix used on form fields for this block instance, guaranteed to be unique across the form.
To add additional variables, you can override the block's ``get_form_context`` method:
.. code-block:: python
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock(required=True)
surname = blocks.CharBlock(required=True)
photo = ImageChooserBlock()
biography = blocks.RichTextBlock()
def get_form_context(self, value, prefix='', errors=None):
context = super(PersonBlock, self).get_form_context(value, prefix=prefix, errors=errors)
context['suggested_first_names'] = ['John', 'Paul', 'George', 'Ringo']
return context
class Meta:
icon = 'user'
form_template = 'myapp/block_forms/person.html'
Custom block types
------------------

Wyświetl plik

@ -12,9 +12,15 @@ class LinkBlock(blocks.StructBlock):
context['classname'] = 'important' if value['title'] == 'Torchbox' else 'normal'
return context
def get_form_context(self, value, prefix='', errors=None):
context = super(LinkBlock, self).get_form_context(value, prefix=prefix, errors=errors)
context['extra_var'] = "Hello from get_form_context!"
return context
class Meta:
icon = "site"
template = 'tests/blocks/link_block.html'
form_template = 'tests/block_forms/link_block.html'
class SectionBlock(blocks.StructBlock):

Wyświetl plik

@ -0,0 +1,9 @@
<div class="{{ classname }}" data-prefix="{{ prefix }}">
<p>{{ extra_var }}</p>
<ul class="fields">
{% for child in children.values %}
<li>{{ child.render_form }}</li>
{% endfor %}
</ul>
</div>

Wyświetl plik

@ -59,7 +59,7 @@ class BaseStructBlock(Block):
def media(self):
return forms.Media(js=[static('wagtailadmin/js/blocks/struct.js')])
def render_form(self, value, prefix='', errors=None):
def get_form_context(self, value, prefix='', errors=None):
if errors:
if len(errors) > 1:
# We rely on StructBlock.clean throwing a single ValidationError with a specially crafted
@ -78,11 +78,18 @@ class BaseStructBlock(Block):
for name, block in self.child_blocks.items()
])
return render_to_string(self.meta.form_template, {
return {
'children': bound_child_blocks,
'help_text': getattr(self.meta, 'help_text', None),
'classname': self.meta.form_classname,
})
'block_definition': self,
'prefix': prefix,
}
def render_form(self, value, prefix='', errors=None):
context = self.get_form_context(value, prefix=prefix, errors=errors)
return render_to_string(self.meta.form_template, context)
def value_from_datadict(self, data, files, prefix):
return StructValue(self, [

Wyświetl plik

@ -2,6 +2,7 @@
from __future__ import absolute_import, unicode_literals
import base64
import collections
import unittest
from decimal import Decimal
@ -12,6 +13,7 @@ from django.test import SimpleTestCase, TestCase
from django.utils.html import format_html
from django.utils.safestring import SafeData, mark_safe
from wagtail.tests.testapp.blocks import LinkBlock as CustomLinkBlock
from wagtail.tests.testapp.blocks import SectionBlock
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.models import Page
@ -824,6 +826,26 @@ class TestStructBlock(SimpleTestCase):
expected = '<div class="rich-text"><b>world</b></div>'
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.io',
}), prefix='mylink')
self.assertTrue(isinstance(context['children'], collections.OrderedDict))
self.assertEqual(len(context['children']), 2)
self.assertTrue(isinstance(context['children']['title'], blocks.BoundBlock))
self.assertEqual(context['children']['title'].value, "Wagtail site")
self.assertTrue(isinstance(context['children']['link'], blocks.BoundBlock))
self.assertEqual(context['children']['link'].value, 'http://www.wagtail.io')
self.assertEqual(context['block_definition'], block)
self.assertEqual(context['prefix'], 'mylink')
def test_render_form(self):
class LinkBlock(blocks.StructBlock):
title = blocks.CharBlock()
@ -1915,10 +1937,16 @@ class TestSystemCheck(TestCase):
class TestTemplateRendering(TestCase):
def test_render_with_custom_context(self):
from wagtail.tests.testapp.blocks import LinkBlock
block = LinkBlock()
block = CustomLinkBlock()
value = block.to_python({'title': 'Torchbox', 'url': 'http://torchbox.com/'})
result = block.render(value)
self.assertEqual(result, '<a href="http://torchbox.com/" class="important">Torchbox</a>')
def test_render_with_custom_form_context(self):
block = CustomLinkBlock()
value = block.to_python({'title': 'Torchbox', 'url': 'http://torchbox.com/'})
result = block.render_form(value, prefix='my-link-block')
self.assertIn('data-prefix="my-link-block"', result)
self.assertIn('<p>Hello from get_form_context!</p>', result)