Skip to content

Commit e5db653

Browse files
committed
🚧 [#5139] Remove DataContainer usage from iter_evaluate_rules
It's a quick test to see what happens if we were to remove the DataContainer from iter_evaluate_rules, and replace it with a FormioData instance. Also added support for FormioData in recursive_apply to fix several failing tests. test_two_actions_on_the_same_variable illustrates what happens when two actions are performed on the same variable.
1 parent 1fc9168 commit e5db653

File tree

6 files changed

+79
-7
lines changed

6 files changed

+79
-7
lines changed

src/openforms/formio/datastructures.py

+5
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,14 @@ class FormioData(UserDict):
162162

163163
data: dict[str, JSONValue]
164164
_keys: set[str]
165+
updates: dict[str, JSONValue]
165166
"""
166167
A collection of flattened key names, for quicker __contains__ access
167168
"""
168169

169170
def __init__(self, *args, **kwargs):
170171
self._keys = set()
172+
self.updates = {}
171173
super().__init__(*args, **kwargs)
172174

173175
def __getitem__(self, key: str):
@@ -206,3 +208,6 @@ def __contains__(self, key: object) -> bool:
206208
return True
207209
except KeyError:
208210
return False
211+
212+
def track_updates(self, m: dict[str, JSONValue]) -> None:
213+
self.updates.update(m)

src/openforms/formio/utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ def recursive_apply(
447447
JSON serialization unless transform_leaf flag is set to True where func is
448448
applied to the nested value as well.
449449
"""
450+
from openforms.formio.datastructures import FormioData
450451
match input:
451452
# string primitive - we can throw it into the template engine
452453
case str():
@@ -460,7 +461,7 @@ def recursive_apply(
460461
]
461462

462463
# mapping - map every key/value pair recursively
463-
case dict():
464+
case dict() | FormioData():
464465
return {
465466
key: recursive_apply(nested_bit, func, transform_leaf, *args, **kwargs)
466467
for key, nested_bit in input.items()

src/openforms/submissions/form_logic.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,21 @@ def evaluate_form_logic(
9292
# 5.1 - if the action type is to set a variable, update the variable state. This
9393
# happens inside of iter_evaluate_rules. This is the ONLY operation that is allowed
9494
# to execute while we're looping through the rules.
95+
# TODO-5139: just a simple test by wrapping the data property of the container with
96+
# a FormioData instance
97+
data_for_logic = FormioData(data_container.data_without_to_python)
9598
with elasticapm.capture_span(
9699
name="collect_logic_operations", span_type="app.submissions.logic"
97100
):
98101
for operation in iter_evaluate_rules(
99102
rules,
100-
data_container,
103+
data_for_logic,
101104
submission=submission,
102105
):
103106
mutation_operations.append(operation)
104107

108+
submission_variables_state.set_values(data_for_logic.updates)
109+
105110
# 6. The variable state is now completely resolved - we can start processing the
106111
# dynamic configuration and side effects.
107112

@@ -209,9 +214,12 @@ def check_submission_logic(
209214
data_container = DataContainer(state=submission_variables_state)
210215

211216
mutation_operations: list[ActionOperation] = []
212-
for operation in iter_evaluate_rules(rules, data_container, submission):
217+
data_for_logic = FormioData(data_container.data_without_to_python)
218+
for operation in iter_evaluate_rules(rules, data_for_logic, submission):
213219
mutation_operations.append(operation)
214220

221+
submission_variables_state.set_values(data_for_logic.updates)
222+
215223
# we loop over all steps because we have validations that ensure unique component
216224
# keys across multiple steps for the whole form.
217225
#

src/openforms/submissions/logic/datastructures.py

+11
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,24 @@ def data(self) -> DataMapping:
3737
:return: A datamapping (key: variable key, value: variable value) ready for
3838
(template context) evaluation.
3939
"""
40+
# TODO: this .to_python is needed for conversion of date/datetimes. Not sure yet
41+
# if logic evaluation uses this in any way, though. Maybe we can get rid of it.
4042
dynamic_values = {
4143
key: variable.to_python() for key, variable in self.state.variables.items()
4244
}
4345
static_values = self.state.static_data()
4446
nested_data = FormioData({**dynamic_values, **static_values})
4547
return nested_data.data
4648

49+
@property
50+
def data_without_to_python(self):
51+
dynamic_values = {
52+
key: variable.value for key, variable in self.state.variables.items()
53+
}
54+
static_values = self.state.static_data()
55+
nested_data = FormioData({**dynamic_values, **static_values})
56+
return nested_data.data
57+
4758
def update(self, updates: DataMapping) -> None:
4859
"""
4960
Update the dynamic data state.

src/openforms/submissions/logic/rules.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import elasticapm
44
from json_logic import jsonLogic
55

6+
from openforms.formio.datastructures import FormioData
67
from openforms.forms.models import FormLogic, FormStep
78

89
from ..models import Submission, SubmissionStep
910
from .actions import ActionOperation
10-
from .datastructures import DataContainer
1111
from .log_utils import log_errors
1212

1313

@@ -108,7 +108,7 @@ def get_current_step(submission: Submission) -> SubmissionStep | None:
108108

109109
def iter_evaluate_rules(
110110
rules: Iterable[FormLogic],
111-
data_container: DataContainer,
111+
data_container: FormioData,
112112
submission: Submission,
113113
) -> Iterator[ActionOperation]:
114114
"""
@@ -137,15 +137,18 @@ def iter_evaluate_rules(
137137
triggered = False
138138
with log_errors(rule.json_logic_trigger, rule):
139139
triggered = bool(
140-
jsonLogic(rule.json_logic_trigger, data_container.data)
140+
jsonLogic(rule.json_logic_trigger, data_container)
141141
)
142142

143143
if not triggered:
144144
continue
145145

146146
for operation in rule.action_operations:
147147
if mutations := operation.eval(
148-
data_container.data, submission=submission
148+
data_container, submission=submission
149149
):
150+
# TODO-5139: not sure if FormioData should track the changes, seems
151+
# like a task for iter_evaluate_rules itself.
150152
data_container.update(mutations)
153+
data_container.track_updates(mutations)
151154
yield operation

src/openforms/submissions/tests/form_logic/test_modify_variables.py

+44
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,50 @@
1919

2020

2121
class VariableModificationTests(TestCase):
22+
def test_two_actions_on_the_same_variable(self):
23+
form = FormFactory.create()
24+
form_step = FormStepFactory.create(
25+
form=form,
26+
form_definition__configuration={
27+
"components": [
28+
{"type": "date", "key": "date"},
29+
]
30+
},
31+
)
32+
submission = SubmissionFactory.create(form=form)
33+
submission_step = SubmissionStepFactory.create(
34+
submission=submission,
35+
form_step=form_step,
36+
data={},
37+
)
38+
39+
FormLogicFactory.create(
40+
form=form,
41+
json_logic_trigger=True,
42+
actions=[
43+
{
44+
"variable": "date",
45+
"action": {
46+
"type": "variable",
47+
"value": "2025-06-06",
48+
},
49+
},
50+
{
51+
"variable": "date",
52+
"action": {
53+
"type": "variable",
54+
"value": {"+": [{"var": "date"}, {"duration": "P1M"}]},
55+
},
56+
}
57+
],
58+
)
59+
60+
evaluate_form_logic(submission, submission_step, submission.data)
61+
62+
variables_state = submission.load_submission_value_variables_state()
63+
64+
self.assertEqual(str(variables_state.variables["date"].value), "2025-07-06")
65+
2266
def test_modify_variable_related_to_step_being_edited(self):
2367
form = FormFactory.create()
2468
step1 = FormStepFactory.create(

0 commit comments

Comments
 (0)