blocks: post conditions (#71778) #1255
|
@ -102,6 +102,9 @@ def test_i18n_page(pub):
|
|||
block = BlockDef(name='test')
|
||||
# check strings will be stripped
|
||||
block.fields = [StringField(id='1', label='text field ')]
|
||||
block.post_conditions = [
|
||||
{'condition': {'type': 'django', 'value': 'blah1'}, 'error_message': 'block post condition error'},
|
||||
]
|
||||
block.store()
|
||||
|
||||
carddef = CardDef()
|
||||
|
@ -145,6 +148,9 @@ def test_i18n_page(pub):
|
|||
# check custom validation message
|
||||
assert TranslatableMessage.count([Equal('string', 'Custom Error')]) == 1
|
||||
|
||||
# check block post condition
|
||||
assert TranslatableMessage.count([Equal('string', 'block post condition error')]) == 1
|
||||
|
||||
# check table
|
||||
assert resp.pyquery('tr').length == TranslatableMessage.count()
|
||||
|
||||
|
|
|
@ -2881,3 +2881,108 @@ def test_block_multiple_rows_single_draft(pub, logged_user, tracking_code):
|
|||
|
||||
assert formdef.data_class().count() == 1
|
||||
assert formdef.data_class().select()[0].status == 'wf-new'
|
||||
|
||||
|
||||
def test_block_field_post_condition(pub):
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', label='Foo', varname='foo'),
|
||||
fields.StringField(id='234', label='Bar', varname='bar'),
|
||||
]
|
||||
block.post_conditions = [
|
||||
{
|
||||
'condition': {'type': 'django', 'value': 'block_var_foo|startswith:"b"'},
|
||||
'error_message': 'foo must start with a b.',
|
||||
},
|
||||
{
|
||||
'condition': {'type': 'django', 'value': 'block_var_foo == block_var_bar'},
|
||||
'error_message': 'foo and bar must be identical.',
|
||||
},
|
||||
]
|
||||
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', block_slug='foobar'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> error
|
||||
assert (
|
||||
resp.pyquery('.widget-with-error .error').text()
|
||||
== 'foo must start with a b. foo and bar must be identical.'
|
||||
)
|
||||
|
||||
resp.form['f1$element0$f123'] = 'baz'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> error
|
||||
assert resp.pyquery('.widget-with-error .error').text() == 'foo and bar must be identical.'
|
||||
|
||||
resp.form['f1$element0$f123'] = 'baz'
|
||||
resp.form['f1$element0$f234'] = 'baz'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
|
||||
assert resp.form['f1$element0$f123'].attrs['readonly']
|
||||
resp = resp.form.submit('submit') # -> end page
|
||||
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.status == 'wf-new'
|
||||
assert formdata.data == {
|
||||
'1': {'data': [{'123': 'baz', '234': 'baz'}], 'schema': {'123': 'string', '234': 'string'}},
|
||||
'1_display': 'foobar',
|
||||
}
|
||||
|
||||
# multiple rows
|
||||
formdef.fields[0].max_items = 3
|
||||
formdef.store()
|
||||
formdef.data_class().wipe()
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'baz'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert not resp.pyquery('.widget-with-error')
|
||||
|
||||
resp.form['f1$element1$f123'] = 'bar'
|
||||
resp.form['f1$element1$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> error
|
||||
assert (
|
||||
resp.pyquery('.widget-with-error[data-block-row="element0"] .error').text()
|
||||
== 'foo and bar must be identical.'
|
||||
)
|
||||
assert resp.pyquery('.widget-with-error[data-block-row="element1"] .error').text() == ''
|
||||
|
||||
resp.form['f1$element1$f234'] = 'baz'
|
||||
resp = resp.form.submit('submit') # -> error
|
||||
assert (
|
||||
resp.pyquery('.widget-with-error[data-block-row="element0"] .error').text()
|
||||
== 'foo and bar must be identical.'
|
||||
)
|
||||
assert (
|
||||
resp.pyquery('.widget-with-error[data-block-row="element1"] .error').text()
|
||||
== 'foo and bar must be identical.'
|
||||
)
|
||||
|
||||
resp.form['f1$element0$f123'] = 'bar'
|
||||
resp.form['f1$element1$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert resp.form['f1$element0$f123'].attrs['readonly']
|
||||
resp = resp.form.submit('submit') # -> end page
|
||||
formdata = formdef.data_class().select()[0]
|
||||
assert formdata.status == 'wf-new'
|
||||
assert formdata.data == {
|
||||
'1': {
|
||||
'data': [{'123': 'bar', '234': 'bar'}, {'123': 'bar', '234': 'bar'}],
|
||||
'schema': {'123': 'string', '234': 'string'},
|
||||
},
|
||||
'1_display': 'foobar, foobar',
|
||||
}
|
||||
|
|
|
@ -62,3 +62,24 @@ def test_import_blockdef_multiple_errors(pub):
|
|||
assert excinfo.value.details == (
|
||||
'Unknown datasources: carddef:foo:unknown, carddef:unknown, foobar; Unknown field types: foobaz'
|
||||
)
|
||||
|
||||
|
||||
def test_import_blockdef_post_conditions(pub):
|
||||
BlockDef.wipe()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'foo'
|
||||
carddef.fields = []
|
||||
carddef.store()
|
||||
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'foo'
|
||||
blockdef.fields = []
|
||||
blockdef.post_conditions = [
|
||||
{'condition': {'type': 'django', 'value': 'blah1'}, 'error_message': 'bar1'},
|
||||
{'condition': {'type': 'django', 'value': 'blah2'}, 'error_message': 'bar2'},
|
||||
]
|
||||
|
||||
export = ET.tostring(export_to_indented_xml(blockdef))
|
||||
blockdef2 = BlockDef.import_from_xml(io.BytesIO(export))
|
||||
assert blockdef.post_conditions == blockdef2.post_conditions
|
||||
|
|
|
@ -26,6 +26,7 @@ from wcs.backoffice.deprecations import DeprecationsDirectory
|
|||
from wcs.backoffice.snapshots import SnapshotsDirectory
|
||||
from wcs.blocks import BlockDef, BlockdefImportError
|
||||
from wcs.categories import BlockCategory
|
||||
from wcs.fields.page import PostConditionsTableWidget
|
||||
from wcs.formdef import UpdateStatisticsDataAfterJob
|
||||
from wcs.qommon import _, misc, template
|
||||
from wcs.qommon.errors import AccessForbiddenError, TraversalError
|
||||
|
@ -115,7 +116,7 @@ class BlockDirectory(FieldsDirectory):
|
|||
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
|
||||
r += htmltext('</ul>')
|
||||
r += self.get_documentable_button()
|
||||
r += htmltext('<a href="settings" rel="popup" role="button">%s</a>') % _('Settings')
|
||||
r += htmltext('<a href="settings" role="button">%s</a>') % _('Settings')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
r += utils.last_modification_block(obj=self.objectdef)
|
||||
|
@ -297,6 +298,13 @@ class BlockDirectory(FieldsDirectory):
|
|||
size=50,
|
||||
hint=_('Use block_var_... to refer to fields.'),
|
||||
)
|
||||
form.add(
|
||||
PostConditionsTableWidget,
|
||||
'post_conditions',
|
||||
title=_('Validation conditions'),
|
||||
value=self.objectdef.post_conditions,
|
||||
)
|
||||
|
||||
if not self.objectdef.is_readonly():
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
@ -314,6 +322,7 @@ class BlockDirectory(FieldsDirectory):
|
|||
form.get_widget('slug').set_error(_('This identifier is already used.'))
|
||||
if form.get_widget('category_id'):
|
||||
self.objectdef.category_id = form.get_widget('category_id').parse()
|
||||
self.objectdef.post_conditions = form.get_widget('post_conditions').parse()
|
||||
widget_template = form.get_widget('digest_template')
|
||||
if widget_template.parse() and 'form_var_' in widget_template.parse():
|
||||
widget_template.set_error(
|
||||
|
|
|
@ -24,7 +24,7 @@ from contextlib import contextmanager
|
|||
from quixote import get_publisher, get_request, get_response
|
||||
from quixote.html import htmltag, htmltext
|
||||
|
||||
from . import data_sources, fields
|
||||
from . import conditions, data_sources, fields
|
||||
from .categories import BlockCategory
|
||||
from .formdata import FormData
|
||||
from .qommon import _, misc
|
||||
|
@ -33,6 +33,7 @@ from .qommon.form import CompositeWidget, SingleSelectHintWidget, WidgetList
|
|||
from .qommon.storage import Equal, StorableObject
|
||||
from .qommon.substitution import CompatibilityNamesDict
|
||||
from .qommon.template import Template
|
||||
from .qommon.xml_storage import PostConditionsXmlMixin
|
||||
|
||||
|
||||
class BlockdefImportError(Exception):
|
||||
|
@ -46,7 +47,7 @@ class BlockdefImportUnknownReferencedError(UnknownReferencedErrorMixin, Blockdef
|
|||
pass
|
||||
|
||||
|
||||
class BlockDef(StorableObject):
|
||||
class BlockDef(StorableObject, PostConditionsXmlMixin):
|
||||
_names = 'blockdefs'
|
||||
_indexes = ['slug']
|
||||
backoffice_class = 'wcs.admin.blocks.BlockDirectory'
|
||||
|
@ -61,6 +62,7 @@ class BlockDef(StorableObject):
|
|||
digest_template = None
|
||||
category_id = None
|
||||
documentation = None
|
||||
post_conditions = None
|
||||
|
||||
SLUG_DASH = '_'
|
||||
|
||||
|
@ -177,6 +179,8 @@ class BlockDef(StorableObject):
|
|||
for field in self.fields or []:
|
||||
fields.append(field.export_to_xml(include_id=True))
|
||||
|
||||
self.post_conditions_export_to_xml(root, include_id=include_id)
|
||||
|
||||
return root
|
||||
|
||||
@classmethod
|
||||
|
@ -247,6 +251,9 @@ class BlockDef(StorableObject):
|
|||
|
||||
BlockCategory.object_category_xml_import(blockdef, tree, include_id=include_id)
|
||||
|
||||
post_conditions_node = tree.find('post_conditions')
|
||||
blockdef.post_conditions_init_with_xml(post_conditions_node, include_id=include_id)
|
||||
|
||||
unknown_datasources = set()
|
||||
if check_datasources:
|
||||
from wcs.carddef import CardDef
|
||||
|
@ -333,6 +340,8 @@ class BlockDef(StorableObject):
|
|||
location = 'forms/blocks/%s/' % self.id
|
||||
for field in self.fields or []:
|
||||
yield from field.i18n_scan(base_location=location)
|
||||
for post_condition in self.post_conditions or []:
|
||||
yield location, None, post_condition.get('error_message')
|
||||
|
||||
def get_all_fields(self):
|
||||
return self.fields
|
||||
|
@ -446,6 +455,22 @@ class BlockSubWidget(CompositeWidget):
|
|||
all_lists = False
|
||||
if widget_value.get(widget.field.id) not in empty_values:
|
||||
empty = False
|
||||
|
||||
if not empty and self.block.post_conditions:
|
||||
error_messages = []
|
||||
with self.block.visibility_context(value, self.index):
|
||||
for i, post_condition in enumerate(self.block.post_conditions):
|
||||
condition = post_condition.get('condition')
|
||||
try:
|
||||
if conditions.Condition(condition, record_errors=False).evaluate():
|
||||
continue
|
||||
except RuntimeError:
|
||||
pass
|
||||
error_message = post_condition.get('error_message')
|
||||
error_messages.append(get_publisher().translate(error_message))
|
||||
if error_messages:
|
||||
self.set_error(' '.join(error_messages))
|
||||
|
||||
if empty and not all_lists and not get_publisher().keep_all_block_rows_mode:
|
||||
value = None
|
||||
for widget in self.get_widgets(): # reset "required" errors
|
||||
|
@ -550,6 +575,10 @@ class BlockWidget(WidgetList):
|
|||
self._parse(request)
|
||||
if self.required and self.value is None:
|
||||
self.set_error(_(self.REQUIRED_ERROR))
|
||||
for widget in self.widgets:
|
||||
# mark required rows with a special attribute, to avoid doubling the
|
||||
# error messages in the template.
|
||||
widget.is_required_error = bool(widget.error == self.REQUIRED_ERROR)
|
||||
return self.value
|
||||
|
||||
def add_media(self):
|
||||
|
|
|
@ -15,16 +15,15 @@
|
|||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import copy
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from django.utils.encoding import force_str
|
||||
from quixote import get_publisher
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.conditions import Condition
|
||||
from wcs.qommon import _
|
||||
from wcs.qommon.form import CompositeWidget, ConditionWidget, StringWidget, VarnameWidget, WidgetListAsTable
|
||||
from wcs.qommon.misc import get_dependencies_from_template, xml_node_text
|
||||
from wcs.qommon.misc import get_dependencies_from_template
|
||||
from wcs.qommon.xml_storage import PostConditionsXmlMixin
|
||||
|
||||
from .base import Field, register_field_class
|
||||
|
||||
|
@ -138,55 +137,13 @@ class PageCondition(Condition):
|
|||
return data
|
||||
|
||||
|
||||
class PageField(Field):
|
||||
class PageField(Field, PostConditionsXmlMixin):
|
||||
key = 'page'
|
||||
description = _('Page')
|
||||
is_no_data_field = True
|
||||
|
||||
post_conditions = None
|
||||
|
||||
def post_conditions_init_with_xml(self, node, include_id=False, snapshot=False):
|
||||
if node is None:
|
||||
return
|
||||
self.post_conditions = []
|
||||
for post_condition_node in node.findall('post_condition'):
|
||||
if post_condition_node.findall('condition/type'):
|
||||
condition = {
|
||||
'type': xml_node_text(post_condition_node.find('condition/type')),
|
||||
'value': xml_node_text(post_condition_node.find('condition/value')),
|
||||
}
|
||||
elif post_condition_node.find('condition').text:
|
||||
condition = {
|
||||
'type': 'python',
|
||||
'value': xml_node_text(post_condition_node.find('condition')),
|
||||
}
|
||||
else:
|
||||
continue
|
||||
self.post_conditions.append(
|
||||
{
|
||||
'condition': condition,
|
||||
'error_message': xml_node_text(post_condition_node.find('error_message')),
|
||||
}
|
||||
)
|
||||
|
||||
def post_conditions_export_to_xml(self, node, include_id=False):
|
||||
if not self.post_conditions:
|
||||
return
|
||||
|
||||
conditions_node = ET.SubElement(node, 'post_conditions')
|
||||
for post_condition in self.post_conditions:
|
||||
post_condition_node = ET.SubElement(conditions_node, 'post_condition')
|
||||
condition_node = ET.SubElement(post_condition_node, 'condition')
|
||||
ET.SubElement(condition_node, 'type').text = force_str(
|
||||
(post_condition['condition'] or {}).get('type') or ''
|
||||
)
|
||||
ET.SubElement(condition_node, 'value').text = force_str(
|
||||
(post_condition['condition'] or {}).get('value') or ''
|
||||
)
|
||||
ET.SubElement(post_condition_node, 'error_message').text = force_str(
|
||||
post_condition['error_message'] or ''
|
||||
)
|
||||
|
||||
def fill_admin_form(self, form):
|
||||
form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
|
||||
form.add(
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block widget-content %}
|
||||
{% if not widget.readonly and widget.error and not widget.is_required_error %}<div class="error"><p>{{ widget.error }}</p></div>{% endif %}
|
||||
{% for subwidget in widget.get_widgets %}
|
||||
{% if widget.readonly and not subwidget.field.include_in_validation_page %}<div style="display: none">{% endif %}
|
||||
{{ subwidget.render|safe }}
|
||||
|
|
|
@ -177,3 +177,47 @@ class XmlStorableObject(StorableObject):
|
|||
sub.attrib['role-id'] = role.id # always include id
|
||||
sub.attrib['role-slug'] = role.slug
|
||||
sub.text = role.name
|
||||
|
||||
|
||||
class PostConditionsXmlMixin:
|
||||
def post_conditions_init_with_xml(self, node, include_id=False, snapshot=False):
|
||||
if node is None:
|
||||
return
|
||||
self.post_conditions = []
|
||||
for post_condition_node in node.findall('post_condition'):
|
||||
if post_condition_node.findall('condition/type'):
|
||||
condition = {
|
||||
'type': xml_node_text(post_condition_node.find('condition/type')),
|
||||
'value': xml_node_text(post_condition_node.find('condition/value')),
|
||||
}
|
||||
elif post_condition_node.find('condition').text:
|
||||
condition = {
|
||||
'type': 'python',
|
||||
'value': xml_node_text(post_condition_node.find('condition')),
|
||||
}
|
||||
else:
|
||||
continue
|
||||
self.post_conditions.append(
|
||||
{
|
||||
'condition': condition,
|
||||
'error_message': xml_node_text(post_condition_node.find('error_message')),
|
||||
}
|
||||
)
|
||||
|
||||
def post_conditions_export_to_xml(self, node, include_id=False):
|
||||
if not self.post_conditions:
|
||||
return
|
||||
|
||||
conditions_node = ET.SubElement(node, 'post_conditions')
|
||||
for post_condition in self.post_conditions:
|
||||
post_condition_node = ET.SubElement(conditions_node, 'post_condition')
|
||||
condition_node = ET.SubElement(post_condition_node, 'condition')
|
||||
ET.SubElement(condition_node, 'type').text = str(
|
||||
(post_condition['condition'] or {}).get('type') or ''
|
||||
)
|
||||
ET.SubElement(condition_node, 'value').text = str(
|
||||
(post_condition['condition'] or {}).get('value') or ''
|
||||
)
|
||||
ET.SubElement(post_condition_node, 'error_message').text = str(
|
||||
post_condition['error_message'] or ''
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue