general: add support for conditions in block fields (#54761)

This commit is contained in:
Frédéric Péters 2022-04-19 07:53:49 +02:00
parent 41ecc47858
commit bc64d54502
9 changed files with 305 additions and 15 deletions

View File

@ -1672,3 +1672,220 @@ def test_removed_block_in_form_page(pub):
assert '(id:%s)' % logged_error.id in resp.text
logged_error = pub.loggederror_class.select()[0]
assert logged_error.occurences_count == 2
def test_block_with_static_condition(pub):
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.StringField(id='123', required=True, label='One', type='string', varname='one'),
fields.StringField(
id='234',
required=True,
label='Two',
type='string',
condition={'type': 'django', 'value': 'False'},
),
fields.StringField(
id='345',
required=True,
label='Three',
type='string',
condition={'type': 'django', 'value': 'True'},
),
]
block.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [
fields.BlockField(id='1', label='test', type='block:foobar'),
]
formdef.store()
formdef.data_class().wipe()
resp = get_app(pub).get(formdef.get_url())
assert 'f1$element0$f123' in resp.form.fields
assert 'f1$element0$f234' in resp.form.fields
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style == 'display: none'
assert 'f1$element0$f345' in resp.form.fields
assert resp.pyquery('[data-widget-name="f1$element0$f345"]').attr.style is None
resp.form['f1$element0$f123'] = 'foo'
resp.form['f1$element0$f345'] = 'bar'
resp = resp.form.submit('submit') # -> validation page
assert resp.form['f1$element0$f123'].attrs['readonly']
assert resp.form['f1$element0$f123'].value == 'foo'
assert resp.form['f1$element0$f234'].value == ''
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style == 'display: none'
assert resp.form['f1$element0$f345'].value == 'bar'
resp = resp.form.submit('submit') # -> end page
formdata = formdef.data_class().select()[0]
assert formdata.data == {
'1': {
'data': [{'123': 'foo', '345': 'bar'}],
'schema': {'123': 'string', '234': 'string', '345': 'string'},
},
'1_display': 'foobar',
}
def test_block_with_block_field_condition(pub):
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.StringField(id='123', required=True, label='One', type='string', varname='one'),
fields.StringField(
id='234',
required=True,
label='Two',
type='string',
condition={'type': 'django', 'value': 'block_var_one == "test"'},
),
]
block.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [
fields.BlockField(id='1', label='test', type='block:foobar'),
]
formdef.store()
formdef.data_class().wipe()
app = get_app(pub)
resp = app.get(formdef.get_url())
assert resp.pyquery('[data-widget-name="f1$element0$f123"]').attr['data-live-source'] == 'true'
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style == 'display: none'
resp.form['f1$element0$f123'] = 'foo'
live_resp = app.post(
formdef.get_url() + 'live?modified_field_id=123&modified_block_id=1&modified_block_row=0',
params=resp.form.submit_fields(),
)
assert live_resp.json['result']['1-123-0']['visible'] is True
assert live_resp.json['result']['1-234-0']['visible'] is False
resp.form['f1$element0$f123'] = 'test'
live_resp = app.post(
formdef.get_url() + 'live?modified_field_id=123&modified_block_id=1&modified_block_row=0',
params=resp.form.submit_fields(),
)
assert live_resp.json['result']['1-123-0']['visible'] is True
assert live_resp.json['result']['1-234-0']['visible'] is True
resp = resp.form.submit('submit') # -> error as 1-234-0 is required
assert 'There were errors processing the form' in resp
resp.form['f1$element0$f234'] = 'test'
resp = resp.form.submit('submit') # validation
assert 'There were errors processing the form' not in resp
resp = resp.form.submit('previous') # -> 1st page
resp.form['f1$element0$f123'] = 'foo'
resp.form['f1$element0$f234'] = ''
resp = resp.form.submit('submit') # validation
assert 'There were errors processing the form' not in resp
resp = resp.form.submit('submit') # -> end page
formdata = formdef.data_class().select()[0]
assert formdata.data == {
'1': {
'data': [{'123': 'foo'}],
'schema': {'123': 'string', '234': 'string'},
},
'1_display': 'foobar',
}
formdef.data_class().wipe()
# check with repetition
formdef.fields[0].max_items = 3
formdef.store()
resp = app.get(formdef.get_url())
resp = resp.form.submit('f1$add_element')
assert 'There were errors processing the form' not in resp
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style == 'display: none'
assert resp.pyquery('[data-widget-name="f1$element1$f234"]').attr.style == 'display: none'
resp.form['f1$element0$f123'] = 'test'
live_resp = app.post(
formdef.get_url() + 'live?modified_field_id=123&modified_block_id=1&modified_block_row=0',
params=resp.form.submit_fields(),
)
assert live_resp.json['result']['1-123-0']['visible'] is True
assert live_resp.json['result']['1-234-0']['visible'] is True
assert live_resp.json['result']['1-123-1']['visible'] is True
assert live_resp.json['result']['1-234-1']['visible'] is False
resp.form['f1$element0$f234'] = 'foo'
resp.form['f1$element1$f123'] = 'xxx'
resp = resp.form.submit('submit') # validation
resp = resp.form.submit('submit') # -> end page
formdata = formdef.data_class().select()[0]
assert formdata.data == {
'1': {
'data': [{'123': 'test', '234': 'foo'}, {'123': 'xxx'}],
'schema': {'123': 'string', '234': 'string'},
},
'1_display': 'foobar, foobar',
}
def test_block_with_block_counter_condition(pub):
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.StringField(id='123', required=True, label='One', type='string', varname='one'),
fields.StringField(
id='234',
required=True,
label='Two',
type='string',
condition={'type': 'django', 'value': 'block_counter.index == 1'},
),
]
block.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
]
formdef.store()
formdef.data_class().wipe()
app = get_app(pub)
resp = app.get(formdef.get_url())
assert resp.pyquery('[data-widget-name="f1$element0$f234"]').attr.style is None
resp = resp.form.submit('f1$add_element')
assert resp.pyquery('[data-widget-name="f1$element1$f234"]').attr.style == 'display: none'
resp = resp.form.submit('f1$add_element')
assert resp.pyquery('[data-widget-name="f1$element2$f234"]').attr.style == 'display: none'
resp.form['f1$element0$f123'] = 'foo'
resp.form['f1$element0$f234'] = 'foo'
resp.form['f1$element1$f123'] = 'bar'
live_resp = app.post(
formdef.get_url() + 'live',
params=resp.form.submit_fields(),
)
assert live_resp.json['result']['1-123-0']['visible'] is True
assert live_resp.json['result']['1-234-0']['visible'] is True
assert live_resp.json['result']['1-123-1']['visible'] is True
assert live_resp.json['result']['1-234-1']['visible'] is False
assert live_resp.json['result']['1-123-2']['visible'] is True
assert live_resp.json['result']['1-234-2']['visible'] is False
resp = resp.form.submit('submit') # validation
resp = resp.form.submit('submit') # -> end page
formdata = formdef.data_class().select()[0]
assert formdata.data == {
'1': {
'data': [{'123': 'foo', '234': 'foo'}, {'123': 'bar'}],
'schema': {'123': 'string', '234': 'string'},
},
'1_display': 'foobar, foobar',
}
formdef.data_class().wipe()

View File

@ -1087,7 +1087,14 @@ def test_field_live_block_string_prefill(pub, http_requests):
assert live_resp.json['result'] == {
'1': {'visible': True},
'2': {'visible': True},
'2-123': {'block_id': '2', 'content': 'hello', 'field_id': '123', 'visible': True},
'2-123-0': {
'block_id': '2',
'block_row': 'element0',
'row': 0,
'content': 'hello',
'field_id': '123',
'visible': True,
},
}

View File

@ -31,8 +31,6 @@ from wcs.qommon.form import FileWidget, Form, HtmlWidget, SingleSelectWidget, Sl
class BlockFieldDefPage(FieldDefPage):
blacklisted_attributes = ['condition']
def redirect_field_anchor(self, field):
anchor = '#itemId_%s' % field.id if field else ''
return redirect('../%s' % anchor)

View File

@ -17,6 +17,7 @@
import itertools
import uuid
import xml.etree.ElementTree as ET
from contextlib import contextmanager
from quixote import get_publisher, get_request
from quixote.html import htmltag, htmltext
@ -43,6 +44,7 @@ class BlockDef(StorableObject):
xml_root_node = 'block'
verbose_name = _('Field block')
verbose_name_plural = _('Field blocks')
var_prefixes = ['block']
name = None
slug = None
@ -230,6 +232,15 @@ class BlockDef(StorableObject):
def is_used(self):
return any(self.get_usage_formdefs())
@contextmanager
def visibility_context(self, value, row_index):
from .variables import LazyBlockDataVar
context = self.get_substitution_counter_variables(row_index)
context['block_var'] = LazyBlockDataVar(self.fields, value)
with get_publisher().substitutions.temporary_feed(context):
yield
class BlockSubWidget(CompositeWidget):
template_name = 'qommon/forms/widgets/block_sub.html'
@ -246,24 +257,49 @@ class BlockSubWidget(CompositeWidget):
field_value = None
if value is not None:
field_value = value.get(field.id)
field.add_to_view_form(form=self, value=field_value)
return field.add_to_view_form(form=self, value=field_value)
else:
field.add_to_form(form=self)
widget = self.get_widget('f%s' % field.id)
if widget:
widget.div_id = None
widget.prefill_attributes = field.get_prefill_attributes()
return widget
self.fields = {}
live_sources = []
for field in self.block.fields:
context = self.block.get_substitution_counter_variables(self.index)
if field.type in ['title', 'subtitle', 'comment']:
with get_publisher().substitutions.temporary_feed(context):
add_to_form(field)
widget = add_to_form(field)
else:
add_to_form(field)
widget = add_to_form(field)
if field.condition:
live_sources.extend(field.get_condition_varnames(formdef=self.block))
field.widget = widget
self.fields[field.id] = widget
for field in self.block.fields:
if field.varname in live_sources:
field.widget.live_condition_source = True
if value:
self.set_value(value)
self.set_visibility(value)
def set_visibility(self, value):
with self.block.visibility_context(value, self.index):
for field in self.block.fields:
widget = self.fields.get(field.id)
if not widget:
continue
visible = field.is_visible({}, formdef=None)
widget.is_hidden = not (visible)
def set_value(self, value):
self.value = value
for widget in self.get_widgets():
@ -278,8 +314,13 @@ class BlockSubWidget(CompositeWidget):
def _parse(self, request):
value = {}
empty = True
for widget in self.get_widgets():
widget_value = self.get_field_data(widget.field, widget)
with self.block.visibility_context(value, self.index):
if not widget.field.is_visible({}, formdef=None):
widget.clear_error()
continue
value.update(widget_value)
if widget_value.get(widget.field.id) is not None:
empty = False

View File

@ -880,6 +880,7 @@ class WidgetField(Field):
widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
else:
widget.extra_css_class = self.extra_css_class
return widget
def get_display_locations_options(self):
options = [
@ -2733,7 +2734,7 @@ class PageCondition(Condition):
live_data = {}
form_live_data = {}
if dict_vars is not None:
if dict_vars is not None and formdef:
live_data = get_dict_with_varnames(formdef.fields, dict_vars)
form_live_data = {'form_' + x: y for x, y in live_data.items()}

View File

@ -804,19 +804,30 @@ class FormStatusPage(Directory, FormTemplateMixin):
for widget in form.widgets:
if not getattr(widget, 'field', None):
continue
yield (None, widget.field, widget)
yield (None, None, widget.field, widget)
if isinstance(widget, BlockWidget):
block_row = 0
for subwidget in widget.widgets:
if isinstance(subwidget, BlockSubWidget):
for field_widget in subwidget.widgets:
yield (widget.field, field_widget.field, field_widget)
yield (widget.field, block_row, field_widget.field, field_widget)
block_row += 1
for block, field, widget in get_all_field_widgets(form):
for block, block_row, field, widget in get_all_field_widgets(form):
if block:
entry = {'visible': True}
result['%s-%s' % (block.id, field.id)] = entry
try:
block_data = formdata.data.get(block.id)['data'][block_row]
except (IndexError, TypeError):
block_data = {}
with block.block.visibility_context(block_data, block_row):
is_visible = field.is_visible({}, formdef=None)
entry = {'visible': is_visible, 'row': block_row, 'field_id': field.id, 'block_id': block.id}
result['%s-%s-%s' % (block.id, field.id, block_row)] = entry
entry['block_id'] = block.id
entry['field_id'] = field.id
entry['block_row'] = 'element%s' % block_row
else:
entry = result[field.id]
if field.key == 'comment':

View File

@ -452,6 +452,8 @@ $(function() {
var new_data = $(this).serialize();
if (data && data.modified_field) {
new_data += '&modified_field_id=' + data.modified_field;
if (data.modified_block) new_data += '&modified_block_id=' + data.modified_block;
if (data.modified_block_row) new_data += '&modified_block_row=' + data.modified_block_row;
}
$('.widget-prefilled').each(function(idx, elem) {
new_data += '&prefilled_' + $(elem).data('field-id') + '=true';
@ -465,7 +467,9 @@ $(function() {
headers: {'accept': 'application/json'},
success: function(json) {
$.each(json.result, function(key, value) {
if (value.block_id) {
if (value.block_id && value.block_row) {
var $widget = $('[data-field-id="' + value.block_id + '"] [data-block-row="' + value.block_row + '"] [data-field-id="' + value.field_id + '"]');
} else if (value.block_id) {
var $widget = $('[data-field-id="' + value.block_id + '"] [data-field-id="' + value.field_id + '"]');
} else {
var $widget = $('[data-field-id="' + key + '"]');
@ -567,8 +571,13 @@ $(function() {
$('form').on('change input paste wcs:change',
'div[data-live-source] input:not([type=file]), div[data-live-source] select, div[data-live-source] textarea',
function(ev) {
var modified_field = $(this).parents('[data-field-id]').data('field-id');
$(this).parents('form').trigger('wcs:change', {modified_field: modified_field});
var params = {};
params.modified_field = $(this).closest('[data-field-id]').data('field-id');
if ($(this).parents('.BlockWidget').length) {
params.modified_block = $(this).closest('.BlockWidget').data('field-id');
params.modified_block_row = $(this).closest('.BlockSubWidget').data('block-row');
}
$(this).parents('form').trigger('wcs:change', params);
});
}
$('form div[data-live-source]').parents('form').trigger('wcs:change', {modified_field: 'init'});

View File

@ -3,6 +3,7 @@
{% if widget.get_error %}widget-with-error{% endif %}
{% if widget.is_required %}widget-required{% else %}widget-optional{% endif %}
{% if widget.is_prefilled %}widget-prefilled{% endif %}{% endblock %}"
{% block widget-attrs %}
{% if widget.is_hidden %}style="display: none"{% endif %}
{% if widget.field %}data-field-id="{{ widget.field.id }}"{% endif %}
data-widget-name="{{ widget.name }}"
@ -22,6 +23,7 @@
data-dynamic-display-value-in="{{widget.attrs|get:"data-dynamic-display-value-in"}}"
{% endif %}
{% if widget.live_condition_source %}data-live-source="true"{% endif %}
{% endblock %}
>
{% block widget-title %}
{{widget.rendered_title}}

View File

@ -1,6 +1,10 @@
{% extends "qommon/forms/widget.html" %}
{% load i18n %}
{% block widget-attrs %}
{{ block.super }} data-block-row="element{{ widget.index }}"
{% endblock %}
{% block widget-content %}
{% for subwidget in widget.get_widgets %}
{% if widget.readonly and not subwidget.field.include_in_validation_page %}<div style="display: none">{% endif %}