Telepath: Validation

pull/6931/head
Karl Hobley 2021-01-13 16:23:47 +00:00 zatwierdzone przez Matt Westcott
rodzic 7477ce1e1c
commit 87cdc215bd
6 zmienionych plików z 214 dodań i 23 usunięć

Wyświetl plik

@ -2,6 +2,9 @@
/* global $ */
import { escapeHtml } from '../../../utils/text';
function initBlockWidget(id) {
/*
Initialises the top-level element of a BlockWidget
@ -19,15 +22,16 @@ function initBlockWidget(id) {
const blockDefData = JSON.parse(body.dataset.block);
const blockDef = window.telepath.unpack(blockDefData);
const blockValue = JSON.parse(body.dataset.value);
const blockErrors = window.telepath.unpack(JSON.parse(body.dataset.errors));
// replace the 'body' element with the populated HTML structure for the block
blockDef.render(body, id, blockValue);
blockDef.render(body, id, blockValue, blockErrors);
}
window.initBlockWidget = initBlockWidget;
class FieldBlock {
constructor(blockDef, placeholder, prefix, initialState) {
constructor(blockDef, placeholder, prefix, initialState, initialError) {
this.blockDef = blockDef;
this.type = blockDef.name;
@ -43,13 +47,33 @@ class FieldBlock {
`);
$(placeholder).replaceWith(dom);
const widgetElement = dom.find('[data-streamfield-widget]').get(0);
this.element = dom[0];
this.widget = this.blockDef.widget.render(widgetElement, prefix, prefix, initialState);
if (initialError) {
this.setError(initialError);
}
}
setState(state) {
this.widget.setState(state);
}
setError(errorList) {
this.element.querySelectorAll(':scope > .field-content > .error-message').forEach(element => element.remove());
if (errorList) {
this.element.classList.add('error');
const errorElement = document.createElement('p');
errorElement.classList.add('error-message');
errorElement.innerHTML = errorList.map(error => `<span>${escapeHtml(error[0])}</span>`).join('');
this.element.querySelector('.field-content').appendChild(errorElement);
} else {
this.element.classList.remove('error');
}
}
getState() {
return this.widget.getState();
}
@ -70,15 +94,15 @@ class FieldBlockDefinition {
this.meta = meta;
}
render(placeholder, prefix, initialState) {
return new FieldBlock(this, placeholder, prefix, initialState);
render(placeholder, prefix, initialState, initialError) {
return new FieldBlock(this, placeholder, prefix, initialState, initialError);
}
}
window.telepath.register('wagtail.blocks.FieldBlock', FieldBlockDefinition);
class StructBlock {
constructor(blockDef, placeholder, prefix, initialState) {
constructor(blockDef, placeholder, prefix, initialState, initialError) {
const state = initialState || {};
this.blockDef = blockDef;
this.type = blockDef.name;
@ -100,7 +124,10 @@ class StructBlock {
dom.append(childDom);
const childBlockElement = childDom.find('[data-streamfield-block]').get(0);
const childBlock = childBlockDef.render(
childBlockElement, prefix + '-' + childBlockDef.name, state[childBlockDef.name]
childBlockElement,
prefix + '-' + childBlockDef.name,
state[childBlockDef.name],
initialError?.blockErrors[childBlockDef.name]
);
this.childBlocks[childBlockDef.name] = childBlock;
@ -114,6 +141,20 @@ class StructBlock {
}
}
setError(errorList) {
if (errorList.length !== 1) {
return;
}
const error = errorList[0];
// eslint-disable-next-line no-restricted-syntax
for (const blockName in error.blockErrors) {
if (error.blockErrors.hasOwnProperty(blockName)) {
this.childBlocks[blockName].setError(error.blockErrors[blockName]);
}
}
}
getState() {
const state = {};
// eslint-disable-next-line guard-for-in, no-restricted-syntax
@ -147,15 +188,23 @@ class StructBlockDefinition {
this.meta = meta;
}
render(placeholder, prefix, initialState) {
return new StructBlock(this, placeholder, prefix, initialState);
render(placeholder, prefix, initialState, initialError) {
return new StructBlock(this, placeholder, prefix, initialState, initialError);
}
}
window.telepath.register('wagtail.blocks.StructBlock', StructBlockDefinition);
class StructBlockValidationError {
constructor(blockErrors) {
this.blockErrors = blockErrors;
}
}
window.telepath.register('wagtail.blocks.StructBlockValidationError', StructBlockValidationError);
class ListBlock {
constructor(blockDef, placeholder, prefix, initialState) {
constructor(blockDef, placeholder, prefix, initialState, initialError) {
this.blockDef = blockDef;
this.type = blockDef.name;
this.prefix = prefix;
@ -176,6 +225,10 @@ class ListBlock {
this.countInput = dom.find('[data-streamfield-list-count]');
this.listContainer = dom.find('[data-streamfield-list-container]');
this.setState(initialState || []);
if (initialError) {
this.setError(initialError);
}
}
clear() {
@ -240,6 +293,20 @@ class ListBlock {
});
}
setError(errorList) {
if (errorList.length !== 1) {
return;
}
const error = errorList[0];
// eslint-disable-next-line no-restricted-syntax
for (const blockIndex in error.blockErrors) {
if (error.blockErrors.hasOwnProperty(blockIndex)) {
this.childBlocks[blockIndex].setError(error.blockErrors[blockIndex]);
}
}
}
getState() {
return this.childBlocks.map((block) => block.getState());
}
@ -263,12 +330,20 @@ class ListBlockDefinition {
this.meta = meta;
}
render(placeholder, prefix, initialState) {
return new ListBlock(this, placeholder, prefix, initialState);
render(placeholder, prefix, initialState, initialError) {
return new ListBlock(this, placeholder, prefix, initialState, initialError);
}
}
window.telepath.register('wagtail.blocks.ListBlock', ListBlockDefinition);
class ListBlockValidationError {
constructor(blockErrors) {
this.blockErrors = blockErrors;
}
}
window.telepath.register('wagtail.blocks.ListBlockValidationError', ListBlockValidationError);
class StreamChild {
/*
@ -357,6 +432,10 @@ class StreamChild {
this.indexInput.val(newIndex);
}
setError(errorList) {
this.block.setError(errorList);
}
getState() {
return {
type: this.type,
@ -475,7 +554,7 @@ class StreamBlockMenu {
}
class StreamBlock {
constructor(blockDef, placeholder, prefix, initialState) {
constructor(blockDef, placeholder, prefix, initialState, initialError) {
this.blockDef = blockDef;
this.type = blockDef.name;
this.prefix = prefix;
@ -505,6 +584,11 @@ class StreamBlock {
// server-side form handler knows to skip it)
this.streamContainer = dom.find('[data-streamfield-stream-container]');
this.setState(initialState || []);
this.container = dom;
if (initialError) {
this.setError(initialError);
}
}
clear() {
@ -612,6 +696,36 @@ class StreamBlock {
}
}
setError(errorList) {
if (errorList.length !== 1) {
return;
}
const error = errorList[0];
// Non block errors
const container = this.container[0];
container.querySelectorAll(':scope > .help-block .help-critical').forEach(element => element.remove());
if (error.nonBlockErrors.length > 0) {
// Add a help block for each error raised
error.nonBlockErrors.forEach(errorText => {
const errorElement = document.createElement('p');
errorElement.classList.add('help-block');
errorElement.classList.add('help-critical');
errorElement.innerText = errorText;
container.insertBefore(errorElement, container.childNodes[0]);
});
}
// Block errors
// eslint-disable-next-line no-restricted-syntax
for (const blockIndex in error.blockErrors) {
if (error.blockErrors.hasOwnProperty(blockIndex)) {
this.children[blockIndex].setError(error.blockErrors[blockIndex]);
}
}
}
getState() {
return this.children.map(child => child.getState());
}
@ -642,8 +756,17 @@ class StreamBlockDefinition {
this.meta = meta;
}
render(placeholder, prefix, initialState) {
return new StreamBlock(this, placeholder, prefix, initialState);
render(placeholder, prefix, initialState, initialError) {
return new StreamBlock(this, placeholder, prefix, initialState, initialError);
}
}
window.telepath.register('wagtail.blocks.StreamBlock', StreamBlockDefinition);
class StreamBlockValidationError {
constructor(nonBlockErrors, blockErrors) {
this.nonBlockErrors = nonBlockErrors;
this.blockErrors = blockErrors;
}
}
window.telepath.register('wagtail.blocks.StreamBlockValidationError', StreamBlockValidationError);

Wyświetl plik

@ -0,0 +1,9 @@
// https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript
export function escapeHtml(unsafe: string): string {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

Wyświetl plik

@ -479,14 +479,20 @@ class BlockWidget(forms.Widget):
def render_with_errors(self, name, value, attrs=None, errors=None, renderer=None):
value_json = json.dumps(self.block_def.get_form_state(value))
if errors:
errors_json = json.dumps(self.js_context.pack(errors.as_data()))
else:
errors_json = '[]'
return format_html(
"""
<div id="{id}" data-block="{block_json}" data-value="{value_json}"></div>
<div id="{id}" data-block="{block_json}" data-value="{value_json}" data-errors="{errors_json}"></div>
<script>
initBlockWidget('{id}');
</script>
""",
id=name, block_json=self.block_json, value_json=value_json
id=name, block_json=self.block_json, value_json=value_json, errors_json=errors_json
)
def render(self, name, value, attrs=None, renderer=None):

Wyświetl plik

@ -10,7 +10,26 @@ from wagtail.core.telepath import Adapter, register
from .base import Block
__all__ = ['ListBlock']
__all__ = ['ListBlock', 'ListBlockValidationError']
class ListBlockValidationError(ValidationError):
def __init__(self, block_errors):
self.block_errors = block_errors
super().__init__('Validation error in ListBlock', params=block_errors)
class ListBlockValidationErrorAdapter(Adapter):
js_constructor = 'wagtail.blocks.ListBlockValidationError'
def js_args(self, error):
return [[elist.as_data() if elist is not None else elist for elist in error.block_errors]]
class Media:
js = [versioned_static('wagtailadmin/js/telepath/blocks.js')]
register(ListBlockValidationErrorAdapter(), ListBlockValidationError)
class ListBlock(Block):
@ -63,9 +82,7 @@ class ListBlock(Block):
errors.append(None)
if any(errors):
# The message here is arbitrary - outputting error messages is delegated to the child blocks,
# which only involves the 'params' list
raise ValidationError('Validation error in ListBlock', params=errors)
raise ListBlockValidationError(errors)
return result

Wyświetl plik

@ -24,6 +24,9 @@ __all__ = ['BaseStreamBlock', 'StreamBlock', 'StreamValue', 'StreamBlockValidati
class StreamBlockValidationError(ValidationError):
def __init__(self, block_errors=None, non_block_errors=None):
self.non_block_errors = non_block_errors
self.block_errors = block_errors
params = {}
if block_errors:
params.update(block_errors)
@ -33,6 +36,22 @@ class StreamBlockValidationError(ValidationError):
'Validation error in StreamBlock', params=params)
class StreamBlockValidationErrorAdapter(Adapter):
js_constructor = 'wagtail.blocks.StreamBlockValidationError'
def js_args(self, error):
return [error.non_block_errors, {
block_id: child_errors.as_data()
for block_id, child_errors in error.block_errors.items()
}]
class Media:
js = [versioned_static('wagtailadmin/js/telepath/blocks.js')]
register(StreamBlockValidationErrorAdapter(), StreamBlockValidationError)
class BaseStreamBlock(Block):
def __init__(self, local_blocks=None, **kwargs):

Wyświetl plik

@ -14,6 +14,25 @@ from .base import Block, DeclarativeSubBlocksMetaclass
__all__ = ['BaseStructBlock', 'StructBlock', 'StructValue']
class StructBlockValidationError(ValidationError):
def __init__(self, block_errors=None):
self.block_errors = block_errors
super().__init__('Validation error in StructBlock', params=block_errors)
class StructBlockValidationErrorAdapter(Adapter):
js_constructor = 'wagtail.blocks.StructBlockValidationError'
def js_args(self, error):
return [error.block_errors]
class Media:
js = [versioned_static('wagtailadmin/js/telepath/blocks.js')]
register(StructBlockValidationErrorAdapter(), StructBlockValidationError)
class StructValue(collections.OrderedDict):
""" A class that generates a StructBlock value from provided sub-blocks """
def __init__(self, block, *args):
@ -84,9 +103,7 @@ class BaseStructBlock(Block):
errors[name] = ErrorList([e])
if errors:
# The message here is arbitrary - client-side form rendering will suppress it
# and delegate the errors contained in the 'params' dict to the child blocks instead
raise ValidationError('Validation error in StructBlock', params=errors)
raise StructBlockValidationError(errors)
return self._to_struct_value(result)