diff --git a/client/src/components/StreamField/blocks/StaticBlock.js b/client/src/components/StreamField/blocks/StaticBlock.js
new file mode 100644
index 0000000000..c9fd290ed1
--- /dev/null
+++ b/client/src/components/StreamField/blocks/StaticBlock.js
@@ -0,0 +1,46 @@
+/* global $ */
+
+import { escapeHtml as h } from '../../../utils/text';
+
+export class StaticBlock {
+ constructor(blockDef, placeholder) {
+ this.blockDef = blockDef;
+
+ const element = document.createElement('div');
+
+ if (this.blockDef.meta.html) {
+ element.innerHTML = this.blockDef.meta.html;
+ } else {
+ element.innerHTML = h(this.blockDef.meta.text);
+ }
+
+ placeholder.replaceWith(element);
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ setState(_state) {}
+
+ // eslint-disable-next-line no-unused-vars
+ setError(_errorList) {}
+
+ getState() {
+ return null;
+ }
+
+ getValue() {
+ return null;
+ }
+
+ focus() {}
+}
+
+export class StaticBlockDefinition {
+ constructor(name, meta) {
+ this.name = name;
+ this.meta = meta;
+ }
+
+ render(placeholder) {
+ return new StaticBlock(this, placeholder);
+ }
+}
diff --git a/client/src/components/StreamField/blocks/StaticBlock.test.js b/client/src/components/StreamField/blocks/StaticBlock.test.js
new file mode 100644
index 0000000000..a0429dde0d
--- /dev/null
+++ b/client/src/components/StreamField/blocks/StaticBlock.test.js
@@ -0,0 +1,90 @@
+/* eslint-disable no-unused-vars */
+
+import { StaticBlockDefinition } from './StaticBlock';
+
+import $ from 'jquery';
+window.$ = $;
+
+describe('telepath: wagtail.blocks.StaticBlock', () => {
+ let boundBlock;
+
+ beforeEach(() => {
+ // Define a test block
+ const blockDef = new StaticBlockDefinition(
+ 'test_field',
+ {
+ text: 'The admin text',
+ icon: 'icon',
+ label: 'The label',
+ }
+ );
+
+ // Render it
+ document.body.innerHTML = '
';
+ boundBlock = blockDef.render($('#placeholder'));
+ });
+
+ test('it renders correctly', () => {
+ expect(document.body.innerHTML).toMatchSnapshot();
+ });
+});
+
+describe('telepath: wagtail.blocks.StaticBlock HTML escaping', () => {
+ let boundBlock;
+
+ beforeEach(() => {
+ window.somethingBad = jest.fn();
+
+ // Define a test block
+ const blockDef = new StaticBlockDefinition(
+ 'test_field',
+ {
+ text: 'The admin text ',
+ icon: 'icon',
+ label: 'The label',
+ }
+ );
+
+ // Render it
+ document.body.innerHTML = '';
+ boundBlock = blockDef.render($('#placeholder'));
+ });
+
+ test('it renders correctly', () => {
+ expect(document.body.innerHTML).toMatchSnapshot();
+ });
+
+ test('javascript cant execute', () => {
+ expect(window.somethingBad.mock.calls.length).toBe(0);
+ });
+});
+
+describe('telepath: wagtail.blocks.StaticBlock allows safe HTML', () => {
+ let boundBlock;
+
+ beforeEach(() => {
+ window.somethingBad = jest.fn();
+
+ // Define a test block
+ const blockDef = new StaticBlockDefinition(
+ 'test_field',
+ {
+ html: 'The admin text ',
+ icon: 'icon',
+ label: 'The label',
+ }
+ );
+
+ // Render it
+ document.body.innerHTML = '';
+ boundBlock = blockDef.render($('#placeholder'));
+ });
+
+ test('it renders correctly', () => {
+ expect(document.body.innerHTML).toMatchSnapshot();
+ });
+
+ test('javascript can execute', () => {
+ expect(window.somethingBad.mock.calls.length).toBe(1);
+ });
+});
diff --git a/client/src/components/StreamField/blocks/__snapshots__/StaticBlock.test.js.snap b/client/src/components/StreamField/blocks/__snapshots__/StaticBlock.test.js.snap
new file mode 100644
index 0000000000..a25477aedd
--- /dev/null
+++ b/client/src/components/StreamField/blocks/__snapshots__/StaticBlock.test.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`telepath: wagtail.blocks.StaticBlock HTML escaping it renders correctly 1`] = `"The admin text <script>somethingBad();</script>
"`;
+
+exports[`telepath: wagtail.blocks.StaticBlock allows safe HTML it renders correctly 1`] = `"The admin text
"`;
+
+exports[`telepath: wagtail.blocks.StaticBlock it renders correctly 1`] = `"The admin text
"`;
diff --git a/client/src/entrypoints/admin/telepath/blocks.js b/client/src/entrypoints/admin/telepath/blocks.js
index b3018dca95..a0bde9960c 100644
--- a/client/src/entrypoints/admin/telepath/blocks.js
+++ b/client/src/entrypoints/admin/telepath/blocks.js
@@ -3,6 +3,7 @@
/* global $ */
import { FieldBlockDefinition } from '../../../components/StreamField/blocks/FieldBlock';
+import { StaticBlockDefinition } from '../../../components/StreamField/blocks/StaticBlock';
import { StructBlockDefinition, StructBlockValidationError } from '../../../components/StreamField/blocks/StructBlock';
import { ListBlockDefinition, ListBlockValidationError } from '../../../components/StreamField/blocks/ListBlock';
import { StreamBlockDefinition, StreamBlockValidationError } from '../../../components/StreamField/blocks/StreamBlock';
@@ -32,6 +33,7 @@ function initBlockWidget(id) {
window.initBlockWidget = initBlockWidget;
window.telepath.register('wagtail.blocks.FieldBlock', FieldBlockDefinition);
+window.telepath.register('wagtail.blocks.StaticBlock', StaticBlockDefinition);
window.telepath.register('wagtail.blocks.StructBlock', StructBlockDefinition);
window.telepath.register('wagtail.blocks.StructBlockValidationError', StructBlockValidationError);
window.telepath.register('wagtail.blocks.ListBlock', ListBlockDefinition);
diff --git a/wagtail/core/blocks/static_block.py b/wagtail/core/blocks/static_block.py
index c4a150f653..4f959bb7ae 100644
--- a/wagtail/core/blocks/static_block.py
+++ b/wagtail/core/blocks/static_block.py
@@ -1,3 +1,9 @@
+from django.utils.safestring import SafeString
+from django.utils.translation import gettext as _
+
+from wagtail.admin.staticfiles import versioned_static
+from wagtail.core.telepath import Adapter, register
+
from .base import Block
@@ -8,9 +14,45 @@ class StaticBlock(Block):
"""
A block that just 'exists' and has no fields.
"""
+ def get_admin_text(self):
+ if self.meta.admin_text is None:
+ if self.label:
+ return _('%(label)s: this block has no options.') % {'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
+
+
+class StaticBlockAdapter(Adapter):
+ js_constructor = 'wagtail.blocks.StaticBlock'
+
+ def js_args(self, block):
+ admin_text = block.get_admin_text()
+
+ if isinstance(admin_text, SafeString):
+ text_or_html = 'html'
+ else:
+ text_or_html = 'text'
+
+ return [
+ block.name,
+ {
+ text_or_html: admin_text,
+ 'icon': block.meta.icon,
+ 'label': block.label,
+ },
+ ]
+
+ class Media:
+ js = [versioned_static('wagtailadmin/js/telepath/blocks.js')]
+
+
+register(StaticBlockAdapter(), StaticBlock)
diff --git a/wagtail/core/tests/test_blocks.py b/wagtail/core/tests/test_blocks.py
index b1a5611e6d..617a0b1a75 100644
--- a/wagtail/core/tests/test_blocks.py
+++ b/wagtail/core/tests/test_blocks.py
@@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as __
from wagtail.core import blocks
from wagtail.core.blocks.field_block import FieldBlockAdapter
from wagtail.core.blocks.list_block import ListBlockAdapter
+from wagtail.core.blocks.static_block import StaticBlockAdapter
from wagtail.core.blocks.stream_block import StreamBlockAdapter
from wagtail.core.blocks.struct_block import StructBlockAdapter
from wagtail.core.models import Page
@@ -3517,56 +3518,86 @@ class TestPageChooserBlock(TestCase):
class TestStaticBlock(unittest.TestCase):
- @unittest.expectedFailure # TODO(telepath)
- def test_render_form_with_constructor(self):
+ def test_adapt_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")
+ block.set_name('posts_static_block')
+ js_args = StaticBlockAdapter().js_args(block)
- @unittest.expectedFailure # TODO(telepath)
- def test_render_form_with_subclass(self):
+ self.assertEqual(js_args[0], 'posts_static_block')
+ self.assertEqual(js_args[1], {
+ 'text': "Latest posts - This block doesn't need to be configured, it will be displayed automatically",
+ 'icon': 'placeholder',
+ 'label': 'Posts static block'
+ })
+
+ def test_adapt_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")
+ block.set_name('posts_static_block')
+ js_args = StaticBlockAdapter().js_args(block)
- @unittest.expectedFailure # TODO(telepath)
- def test_render_form_with_subclass_displays_default_text_if_no_admin_text(self):
+ self.assertEqual(js_args[0], 'posts_static_block')
+ self.assertEqual(js_args[1], {
+ 'text': "Latest posts - This block doesn't need to be configured, it will be displayed automatically",
+ 'icon': 'placeholder',
+ 'label': 'Posts static block'
+ })
+
+ def test_adapt_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.")
+ block.set_name('posts_static_block')
+ js_args = StaticBlockAdapter().js_args(block)
- @unittest.expectedFailure # TODO(telepath)
- def test_render_form_with_subclass_displays_default_text_if_no_admin_text_and_no_label(self):
+ self.assertEqual(js_args[0], 'posts_static_block')
+ self.assertEqual(js_args[1], {
+ 'text': "Latest posts: this block has no options.",
+ 'icon': 'placeholder',
+ 'label': 'Latest posts'
+ })
+
+ def test_adapt_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.")
+ block.set_name('posts_static_block')
+ js_args = StaticBlockAdapter().js_args(block)
- @unittest.expectedFailure # TODO(telepath)
- def test_render_form_works_with_mark_safe(self):
+ self.assertEqual(js_args[0], 'posts_static_block')
+ self.assertEqual(js_args[1], {
+ 'text': "Posts static block: this block has no options.",
+ 'icon': 'placeholder',
+ 'label': 'Posts static block'
+ })
+
+ def test_adapt_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")
+ block.set_name('posts_static_block')
+ js_args = StaticBlockAdapter().js_args(block)
+
+ self.assertEqual(js_args[0], 'posts_static_block')
+ self.assertEqual(js_args[1], {
+ 'html': "Latest posts - This block doesn't need to be configured, it will be displayed automatically",
+ 'icon': 'placeholder',
+ 'label': 'Posts static block'
+ })
def test_get_default(self):
block = blocks.StaticBlock()