kopia lustrzana https://github.com/wagtail/wagtail
Telepath: Validation
rodzic
7477ce1e1c
commit
87cdc215bd
|
@ -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);
|
||||
|
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue