diff --git a/tests/form_pages/test_block.py b/tests/form_pages/test_block.py index 1a64d6731..9edcf5138 100644 --- a/tests/form_pages/test_block.py +++ b/tests/form_pages/test_block.py @@ -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() diff --git a/tests/form_pages/test_live.py b/tests/form_pages/test_live.py index f8f94d92b..fab8f5221 100644 --- a/tests/form_pages/test_live.py +++ b/tests/form_pages/test_live.py @@ -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, + }, } diff --git a/wcs/admin/blocks.py b/wcs/admin/blocks.py index 3243962e3..1121d4b9c 100644 --- a/wcs/admin/blocks.py +++ b/wcs/admin/blocks.py @@ -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) diff --git a/wcs/blocks.py b/wcs/blocks.py index f10b4971c..291c6d88c 100644 --- a/wcs/blocks.py +++ b/wcs/blocks.py @@ -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 diff --git a/wcs/fields.py b/wcs/fields.py index c937c5850..02b52d9b2 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -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()} diff --git a/wcs/forms/common.py b/wcs/forms/common.py index 43dcfc18e..04ba96c2e 100644 --- a/wcs/forms/common.py +++ b/wcs/forms/common.py @@ -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': diff --git a/wcs/qommon/static/js/qommon.forms.js b/wcs/qommon/static/js/qommon.forms.js index ab1be7151..095d2df74 100644 --- a/wcs/qommon/static/js/qommon.forms.js +++ b/wcs/qommon/static/js/qommon.forms.js @@ -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'}); diff --git a/wcs/qommon/templates/qommon/forms/widget.html b/wcs/qommon/templates/qommon/forms/widget.html index 10293c7f6..3189d4ae7 100644 --- a/wcs/qommon/templates/qommon/forms/widget.html +++ b/wcs/qommon/templates/qommon/forms/widget.html @@ -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}} diff --git a/wcs/qommon/templates/qommon/forms/widgets/block_sub.html b/wcs/qommon/templates/qommon/forms/widgets/block_sub.html index b9cb06828..c221fa77d 100644 --- a/wcs/qommon/templates/qommon/forms/widgets/block_sub.html +++ b/wcs/qommon/templates/qommon/forms/widgets/block_sub.html @@ -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 %}
{% endif %}