Telepath: StaticBlock

pull/6931/head
Karl Hobley 2021-02-02 15:57:59 +00:00 zatwierdzone przez Matt Westcott
rodzic 0e45188aa8
commit 2e55191a75
6 zmienionych plików z 238 dodań i 20 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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 = '<div id="placeholder"></div>';
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 <script>somethingBad();</script>',
icon: 'icon',
label: 'The label',
}
);
// Render it
document.body.innerHTML = '<div id="placeholder"></div>';
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 <script>somethingBad();</script>',
icon: 'icon',
label: 'The label',
}
);
// Render it
document.body.innerHTML = '<div id="placeholder"></div>';
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);
});
});

Wyświetl plik

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`telepath: wagtail.blocks.StaticBlock HTML escaping it renders correctly 1`] = `"<div>The admin text &lt;script&gt;somethingBad();&lt;/script&gt;</div>"`;
exports[`telepath: wagtail.blocks.StaticBlock allows safe HTML it renders correctly 1`] = `"<div>The admin text <script>somethingBad();</script></div>"`;
exports[`telepath: wagtail.blocks.StaticBlock it renders correctly 1`] = `"<div>The admin text</div>"`;

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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("<b>Latest posts</b> - 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, "<b>Latest posts</b> - 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': "<b>Latest posts</b> - 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()