diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ed59537c16..90c7027c8f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -14,6 +14,7 @@ Changelog * Page chooser now opens at the deepest ancestor page that covers all the pages of the required page type (Tim Heap) * `PageChooserBlock` now accepts a `target_model` option to specify the required page type (Tim Heap) * Modeladmin forms now respect `fields` / `exclude` options passed on custom model forms (Thejaswi Puthraya) + * Added new StreamField block type `StaticBlock` (Benoît Vogel) * Fix: `AbstractForm` now respects custom `get_template` methods on the page model (Gagaro) * Fix: Use specific page model for the parent page in the explore index (Gagaro) * Fix: Remove responsive styles in embed when there is no ratio available (Gagaro) diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 1cd3f72ecc..0a0a9df991 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -182,6 +182,7 @@ Contributors * Gary Krige * Hammy Goonan * Thejaswi Puthraya +* Benoît Vogel Translators =========== diff --git a/docs/releases/1.8.rst b/docs/releases/1.8.rst index 62a47406db..b55d271493 100644 --- a/docs/releases/1.8.rst +++ b/docs/releases/1.8.rst @@ -40,6 +40,7 @@ Minor features * Page chooser now opens at the deepest ancestor page that covers all the pages of the required page type (Tim Heap) * ``PageChooserBlock`` now accepts a ``target_model`` option to specify the required page type (Tim Heap) * Modeladmin forms now respect ``fields`` / ``exclude`` options passed on custom model forms (Thejaswi Puthraya) + * Added new StreamField block type ``StaticBlock`` for blocks that occupy a position in a stream but otherwise have no configuration; see :ref:`streamfield_staticblock` (Benoît Vogel) Bug fixes diff --git a/docs/topics/streamfield.rst b/docs/topics/streamfield.rst index d9d3694732..db8266005e 100644 --- a/docs/topics/streamfield.rst +++ b/docs/topics/streamfield.rst @@ -277,6 +277,36 @@ EmbedBlock A field for the editor to enter a URL to a media item (such as a YouTube video) to appear as embedded media on the page. The keyword arguments ``required``, ``max_length``, ``min_length`` and ``help_text`` are accepted. +.. _streamfield_staticblock: + +StaticBlock +~~~~~~~~~~~ + +``wagtail.wagtailcore.blocks.StaticBlock`` + +A block which doesn't have any fields, thus passes no particular values to its template during rendering. This can be useful if you need the editor to be able to insert some content which is always the same or doesn't need to be configured within the page editor, such as an address, embed code from third-party services, or more complex pieces of code if the template uses template tags. + +By default, some default text (which contains the ``label`` keyword argument if you pass it) will be displayed in the editor interface, so that the block doesn't look empty. But you can also customise it entirely by passing a text string as the ``admin_text`` keyword argument instead: + +.. code-block:: python + + blocks.StaticBlock( + admin_text='Latest posts: no configuration needed.', + # or admin_text=mark_safe('Latest posts: no configuration needed.'), + template='latest_posts.html') + +``StaticBlock`` can also be subclassed to produce a reusable block with the same configuration everywhere it is used: + +.. code-block:: python + + class LatestPostsStaticBlock(blocks.StaticBlock): + class Meta: + icon = 'user' + label = 'Latest posts' + admin_text = '{label}: configured elsewhere'.format(label=label) + template = 'latest_posts.html' + + Structural block types ---------------------- diff --git a/wagtail/tests/testapp/templates/tests/blocks/posts_static_block.html b/wagtail/tests/testapp/templates/tests/blocks/posts_static_block.html new file mode 100644 index 0000000000..6850e05614 --- /dev/null +++ b/wagtail/tests/testapp/templates/tests/blocks/posts_static_block.html @@ -0,0 +1 @@ +
PostsStaticBlock template
\ No newline at end of file diff --git a/wagtail/wagtailcore/blocks/__init__.py b/wagtail/wagtailcore/blocks/__init__.py index 62c9302747..1b7a1740cf 100644 --- a/wagtail/wagtailcore/blocks/__init__.py +++ b/wagtail/wagtailcore/blocks/__init__.py @@ -6,3 +6,4 @@ from .field_block import * # NOQA from .struct_block import * # NOQA from .list_block import * # NOQA from .stream_block import * # NOQA +from .static_block import * # NOQA diff --git a/wagtail/wagtailcore/blocks/static_block.py b/wagtail/wagtailcore/blocks/static_block.py new file mode 100644 index 0000000000..f785e42f8a --- /dev/null +++ b/wagtail/wagtailcore/blocks/static_block.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from .base import Block + +__all__ = ['StaticBlock'] + + +class StaticBlock(Block): + """ + A block that just 'exists' and has no fields. + """ + def render_form(self, value, prefix='', errors=None): + if self.meta.admin_text is None: + if self.label: + return _('{label}: this block has no options.').format(label=self.label) + else: + return _('This block has no options.') + return self.meta.admin_text + + def value_from_datadict(self, data, files, prefix): + return None + + class Meta: + admin_text = None + default = None diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index d070f0d485..1e5095facf 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -2090,6 +2090,74 @@ class TestPageChooserBlock(TestCase): (), {'target_model': 'tests.SimplePage'})) +class TestStaticBlock(unittest.TestCase): + def test_render_form_with_constructor(self): + block = blocks.StaticBlock( + admin_text="Latest posts - This block doesn't need to be configured, it will be displayed automatically", + template='tests/blocks/posts_static_block.html') + rendered_html = block.render_form(None) + + self.assertEqual(rendered_html, "Latest posts - This block doesn't need to be configured, it will be displayed automatically") + + def test_render_form_with_subclass(self): + class PostsStaticBlock(blocks.StaticBlock): + class Meta: + admin_text = "Latest posts - This block doesn't need to be configured, it will be displayed automatically" + template = "tests/blocks/posts_static_block.html" + + block = PostsStaticBlock() + rendered_html = block.render_form(None) + + self.assertEqual(rendered_html, "Latest posts - This block doesn't need to be configured, it will be displayed automatically") + + def test_render_form_with_subclass_displays_default_text_if_no_admin_text(self): + class LabelOnlyStaticBlock(blocks.StaticBlock): + class Meta: + label = "Latest posts" + + block = LabelOnlyStaticBlock() + rendered_html = block.render_form(None) + + self.assertEqual(rendered_html, "Latest posts: this block has no options.") + + def test_render_form_with_subclass_displays_default_text_if_no_admin_text_and_no_label(self): + class NoMetaStaticBlock(blocks.StaticBlock): + pass + + block = NoMetaStaticBlock() + rendered_html = block.render_form(None) + + self.assertEqual(rendered_html, "This block has no options.") + + def test_render_form_works_with_mark_safe(self): + block = blocks.StaticBlock( + admin_text=mark_safe("Latest posts - This block doesn't need to be configured, it will be displayed automatically"), + template='tests/blocks/posts_static_block.html') + rendered_html = block.render_form(None) + + self.assertEqual(rendered_html, "Latest posts - This block doesn't need to be configured, it will be displayed automatically") + + def test_get_default(self): + block = blocks.StaticBlock() + default_value = block.get_default() + self.assertEqual(default_value, None) + + def test_render(self): + block = blocks.StaticBlock(template='tests/blocks/posts_static_block.html') + result = block.render(None) + self.assertEqual(result, 'PostsStaticBlock template
') + + def test_serialize(self): + block = blocks.StaticBlock() + result = block.get_prep_value(None) + self.assertEqual(result, None) + + def test_deserialize(self): + block = blocks.StaticBlock() + result = block.to_python(None) + self.assertEqual(result, None) + + class TestSystemCheck(TestCase): def test_name_must_be_nonempty(self): block = blocks.StreamBlock([