Skip to content

Commit 01f2285

Browse files
committed
🚧 [#5139] More WIP
1 parent e16ae3f commit 01f2285

File tree

5 files changed

+126
-47
lines changed

5 files changed

+126
-47
lines changed

src/openforms/formio/service.py

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def normalize_value_for_component(component: Component, value: Any) -> Any:
6161
return register.normalize(component, value)
6262

6363

64+
# TODO-5139: remove request as an argument from this
6465
@elasticapm.capture_span(span_type="app.formio")
6566
def get_dynamic_configuration(
6667
config_wrapper: FormioConfigurationWrapper,

src/openforms/submissions/form_logic.py

+75-40
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323

2424
# TODO-5139: replace `data` type with `FormioData`?
25+
# TODO-5139: replace all DataContainer type hints with FormioData
2526
@elasticapm.capture_span(span_type="app.submissions.logic")
2627
def evaluate_form_logic(
2728
submission: "Submission",
@@ -94,47 +95,36 @@ def evaluate_form_logic(
9495
# 5.1 - if the action type is to set a variable, update the variable state. This
9596
# happens inside of iter_evaluate_rules. This is the ONLY operation that is allowed
9697
# to execute while we're looping through the rules.
98+
# TODO-5139: I first put this tracking of data difference inside FormioData, but
99+
# that doesn't seem like it should be its responsibility. Then yielded a complete
100+
# difference in iter_evaluate_rules, but that fails when there is no rules to be
101+
# applied: `data_diff` would be undefined. So decided to track the difference
102+
# outside iter_evaluate_rules, as we also track the mutation operations here.
103+
# Still not sure about it, though
104+
data_diff_total = FormioData()
97105
with elasticapm.capture_span(
98106
name="collect_logic_operations", span_type="app.submissions.logic"
99107
):
100-
for operation in iter_evaluate_rules(
108+
for operation, mutations in iter_evaluate_rules(
101109
rules,
102110
data_for_evaluation,
103111
submission=submission,
104112
):
105113
mutation_operations.append(operation)
106-
107-
submission_variables_state.set_values(data_for_evaluation.updates)
108-
109-
# 6. The variable state is now completely resolved - we can start processing the
110-
# dynamic configuration and side effects.
111-
112-
# Calculate which data has changed from the initial, for the step.
113-
# TODO-5139: this should ideally be built up from mutation returns inside
114-
# iter_evaluate_rules
115-
relevant_variables = submission_variables_state.get_variables_in_submission_step(
116-
step, include_unsaved=True
117-
)
118-
119-
updated_step_data = FormioData()
120-
for key, variable in relevant_variables.items():
121-
if (
122-
not variable.form_variable
123-
or variable.value != variable.form_variable.initial_value
124-
):
125-
updated_step_data[key] = variable.value
126-
step.data = DirtyData(updated_step_data.data)
114+
if mutations:
115+
data_diff_total.update(mutations)
127116

128117
# 7. finally, apply the dynamic configuration
129118

130119
# we need to apply the context-specific configurations before we can apply
131120
# mutations based on logic, which is then in turn passed to the serializer(s)
132-
# TODO: refactor this to rely on variables state
121+
# TODO-5139: rewrite `get_dynamic_configuration` and its callees to work with
122+
# FormioData instance
133123
config_wrapper = get_dynamic_configuration(
134124
config_wrapper,
135125
request=None,
136126
submission=submission,
137-
data=data_for_evaluation,
127+
data=data_for_evaluation.data,
138128
)
139129

140130
# 7.1 Apply the component mutation operations
@@ -145,18 +135,16 @@ def evaluate_form_logic(
145135
# (eventually) hidden BEFORE we do any further processing. This is only a bandaid
146136
# fix, as the (stale) data has potentially been input for other logic rules.
147137
# Note that only the dirty data logic check acts on these differences.
148-
149-
# only keep the changes in the data, so that old values do not overwrite otherwise
150-
# debounced client-side data changes
151-
data_diff = FormioData()
152138
for component in config_wrapper:
153139
key = component["key"]
154-
# TODO-5139: is_visible_in_frontend must accept a FormioData instance instead
140+
# TODO-5139: rewrite `is_visible_in_frontend` and its callees to work with
141+
# FormioData instance
155142
is_visible = config_wrapper.is_visible_in_frontend(key, data_for_evaluation.data)
156143
if is_visible:
157144
continue
158145

159-
# Reset the value of any field that may have become hidden again after evaluating the logic
146+
# Reset the value of any field that may have become hidden again after
147+
# evaluating the logic
160148
original_value = initial_data.get(key, empty)
161149
empty_value = get_component_empty_value(component)
162150
if original_value is empty or original_value == empty_value:
@@ -166,32 +154,76 @@ def evaluate_form_logic(
166154
continue
167155

168156
# clear the value
169-
data_for_evaluation.update({key: empty_value})
170-
data_diff[key] = empty_value
157+
data_for_evaluation[key] = empty_value
158+
data_diff_total[key] = empty_value
171159

172160
# 7.2 Interpolate the component configuration with the variables.
161+
# TODO-5139: rewrite `inject_variables` and its callees to work with FormioData
162+
# instance
173163
inject_variables(config_wrapper, data_for_evaluation.data)
174164

175-
# 7.3 Handle custom formio types - TODO: this needs to be lifted out of
176-
# :func:`get_dynamic_configuration` so that it can use variables.
165+
166+
# ---------------------------------------------------------------------------------------
167+
168+
169+
# 8. All the processing is now complete, so we can update the state.
170+
# TODO-5139: when setting a value to SubmissionValueVariable.value, it doesn't
171+
# automatically get encoded to JSON with the encoder. Probably only happens when
172+
# saving the model. This means we have to do it manually here or in
173+
# SubmissionValueVariablesState.set_values. Or do we want to always convert to
174+
# python objects, like already suggested in
175+
# SubmissionValueVariableState.set_values? Issue 2324 is related to this
176+
submission_variables_state.set_values(data_diff_total)
177+
178+
# 8.1 build a difference in data for the step. It is important that we only keep the
179+
# changes in the data, so that old values do not overwrite otherwise debounced
180+
# client-side data changes
181+
182+
# Calculate which data has changed from the initial, for the step.
183+
relevant_variables = submission_variables_state.get_variables_in_submission_step(
184+
step, include_unsaved=True
185+
)
186+
187+
# TODO-5139: We can't compare `initial_data` with `data_for_evaluation` because
188+
# invalid data that is converted to python objects will be present as `None`, so
189+
# there might not be a change. If we compare `variable.initial_value` with
190+
# `variable.value`, data changes for invalid data are detected, as the invalid
191+
# value is saved to `variable.value` anyway. This check also doesn't work, as
192+
# variable.value is updated with python objects, and initial value is not
193+
updated_step_data = FormioData()
194+
for key, variable in relevant_variables.items():
195+
if (
196+
not variable.form_variable
197+
or variable.value != variable.form_variable.initial_value
198+
):
199+
updated_step_data[key] = variable.value
200+
step.data = DirtyData(updated_step_data.data)
177201

178202
# process the output for logic checks with dirty data
179203
if dirty:
180-
# Iterate over all components instead of `step.data`, to take hidden fields into account (See: #1755)
204+
data_diff_dirty = FormioData()
205+
# Iterate over all components instead of `step.data`, to take hidden fields into
206+
# account (See: #1755)
181207
for component in config_wrapper:
182208
key = component["key"]
183209
# already processed, don't process it again
184-
if data_diff.get(key, default=empty) is not empty:
210+
if data_diff_dirty.get(key, default=empty) is not empty:
185211
continue
186212

213+
# TODO-5139: currently, this might return a python object (because we pass
214+
# python objects to the SubmissionValueVariables.set_values) whereas
215+
# previously it would return a json value. Not sure if this breaks
216+
# anything...
187217
new_value = updated_step_data.get(key, default=empty)
218+
# TODO-5139: previously these were python objects, and they still are, so
219+
# that's ok
188220
original_value = initial_data.get(key, default=empty)
189221
if new_value is empty or new_value == original_value:
190222
continue
191-
data_diff[key] = new_value
223+
data_diff_dirty[key] = new_value
192224

193225
# only return the 'overrides'
194-
step.data = DirtyData(data_diff.data)
226+
step.data = DirtyData(data_diff_dirty.data)
195227

196228
step._form_logic_evaluated = True
197229

@@ -220,10 +252,13 @@ def check_submission_logic(
220252
data_for_evaluation = submission_variables_state.to_python()
221253

222254
mutation_operations: list[ActionOperation] = []
223-
for operation in iter_evaluate_rules(rules, data_for_evaluation, submission):
255+
data_diff = FormioData({})
256+
for operation, mutations in iter_evaluate_rules(rules, data_for_evaluation, submission):
224257
mutation_operations.append(operation)
258+
if mutations:
259+
data_diff.update(mutations)
225260

226-
submission_variables_state.set_values(data_for_evaluation.updates)
261+
submission_variables_state.set_values(data_diff)
227262

228263
# we loop over all steps because we have validations that ensure unique component
229264
# keys across multiple steps for the whole form.

src/openforms/submissions/logic/rules.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Iterable, Iterator
22

33
import elasticapm
4+
from glom import mutation
45
from json_logic import jsonLogic
56

67
from openforms.formio.datastructures import FormioData
@@ -110,7 +111,7 @@ def iter_evaluate_rules(
110111
rules: Iterable[FormLogic],
111112
data_container: FormioData,
112113
submission: Submission,
113-
) -> Iterator[ActionOperation]:
114+
) -> Iterator[tuple[ActionOperation, dict]]:
114115
"""
115116
Iterate over the rules and evaluate the trigger, yielding action operations.
116117
@@ -147,8 +148,5 @@ def iter_evaluate_rules(
147148
if mutations := operation.eval(
148149
data_container, submission=submission
149150
):
150-
# TODO-5139: not sure if FormioData should track the changes, seems
151-
# like a task for iter_evaluate_rules itself.
152151
data_container.update(mutations)
153-
data_container.track_updates(mutations)
154-
yield operation
152+
yield operation, mutations

src/openforms/submissions/models/submission_value_variable.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def save_prefill_data(self, data: dict[str, Any]) -> None:
233233

234234
SubmissionValueVariable.objects.bulk_create(variables_to_create)
235235

236-
def set_values(self, data: DataMapping) -> None:
236+
def set_values(self, data: FormioData | DataMapping) -> None:
237237
"""
238238
Apply the values from ``data`` to the current state of the variables.
239239
@@ -246,7 +246,7 @@ def set_values(self, data: DataMapping) -> None:
246246
.. todo:: apply variable.datatype/format to obtain python objects? This also
247247
needs to properly serialize back to JSON though!
248248
"""
249-
formio_data = FormioData(data)
249+
formio_data = data if isinstance(data, FormioData) else FormioData(data)
250250
for key, variable in self.variables.items():
251251
new_value = formio_data.get(key, default=empty)
252252
if new_value is empty:

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

+45
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,51 @@ def test_component_value_set_to_today(self):
11211121
new_value = response.json()["step"]["data"]["date"]
11221122
self.assertEqual(new_value, "2024-03-18")
11231123

1124+
def test_asdf(self):
1125+
form = FormFactory.create()
1126+
form_step = FormStepFactory.create(
1127+
form=form,
1128+
form_definition__configuration={
1129+
"components": [
1130+
{
1131+
"type": "date",
1132+
"key": "date",
1133+
"defaultValue": "2025-01-01"
1134+
},
1135+
]
1136+
},
1137+
)
1138+
1139+
FormLogicFactory.create(
1140+
form=form,
1141+
json_logic_trigger=True,
1142+
actions=[
1143+
{
1144+
"variable": "date",
1145+
"action": {
1146+
"type": "variable",
1147+
"value": "2025-01-01",
1148+
},
1149+
}
1150+
],
1151+
)
1152+
1153+
submission = SubmissionFactory.create(form=form)
1154+
1155+
self._add_submission_to_session(submission)
1156+
logic_check_endpoint = reverse(
1157+
"api:submission-steps-logic-check",
1158+
kwargs={
1159+
"submission_uuid": submission.uuid,
1160+
"step_uuid": form_step.uuid,
1161+
},
1162+
)
1163+
response = self.client.post(logic_check_endpoint, {"data": {}})
1164+
1165+
data = response.json()
1166+
1167+
self.assertNotIn("date", data["step"]["data"])
1168+
11241169

11251170
def is_valid_expression(expr: dict):
11261171
try:

0 commit comments

Comments
 (0)