22
22
23
23
24
24
# TODO-5139: replace `data` type with `FormioData`?
25
+ # TODO-5139: replace all DataContainer type hints with FormioData
25
26
@elasticapm .capture_span (span_type = "app.submissions.logic" )
26
27
def evaluate_form_logic (
27
28
submission : "Submission" ,
@@ -94,47 +95,36 @@ def evaluate_form_logic(
94
95
# 5.1 - if the action type is to set a variable, update the variable state. This
95
96
# happens inside of iter_evaluate_rules. This is the ONLY operation that is allowed
96
97
# 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 ()
97
105
with elasticapm .capture_span (
98
106
name = "collect_logic_operations" , span_type = "app.submissions.logic"
99
107
):
100
- for operation in iter_evaluate_rules (
108
+ for operation , mutations in iter_evaluate_rules (
101
109
rules ,
102
110
data_for_evaluation ,
103
111
submission = submission ,
104
112
):
105
113
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 )
127
116
128
117
# 7. finally, apply the dynamic configuration
129
118
130
119
# we need to apply the context-specific configurations before we can apply
131
120
# 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
133
123
config_wrapper = get_dynamic_configuration (
134
124
config_wrapper ,
135
125
request = None ,
136
126
submission = submission ,
137
- data = data_for_evaluation ,
127
+ data = data_for_evaluation . data ,
138
128
)
139
129
140
130
# 7.1 Apply the component mutation operations
@@ -145,18 +135,16 @@ def evaluate_form_logic(
145
135
# (eventually) hidden BEFORE we do any further processing. This is only a bandaid
146
136
# fix, as the (stale) data has potentially been input for other logic rules.
147
137
# 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 ()
152
138
for component in config_wrapper :
153
139
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
155
142
is_visible = config_wrapper .is_visible_in_frontend (key , data_for_evaluation .data )
156
143
if is_visible :
157
144
continue
158
145
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
160
148
original_value = initial_data .get (key , empty )
161
149
empty_value = get_component_empty_value (component )
162
150
if original_value is empty or original_value == empty_value :
@@ -166,32 +154,76 @@ def evaluate_form_logic(
166
154
continue
167
155
168
156
# 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
171
159
172
160
# 7.2 Interpolate the component configuration with the variables.
161
+ # TODO-5139: rewrite `inject_variables` and its callees to work with FormioData
162
+ # instance
173
163
inject_variables (config_wrapper , data_for_evaluation .data )
174
164
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 )
177
201
178
202
# process the output for logic checks with dirty data
179
203
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)
181
207
for component in config_wrapper :
182
208
key = component ["key" ]
183
209
# 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 :
185
211
continue
186
212
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...
187
217
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
188
220
original_value = initial_data .get (key , default = empty )
189
221
if new_value is empty or new_value == original_value :
190
222
continue
191
- data_diff [key ] = new_value
223
+ data_diff_dirty [key ] = new_value
192
224
193
225
# only return the 'overrides'
194
- step .data = DirtyData (data_diff .data )
226
+ step .data = DirtyData (data_diff_dirty .data )
195
227
196
228
step ._form_logic_evaluated = True
197
229
@@ -220,10 +252,13 @@ def check_submission_logic(
220
252
data_for_evaluation = submission_variables_state .to_python ()
221
253
222
254
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 ):
224
257
mutation_operations .append (operation )
258
+ if mutations :
259
+ data_diff .update (mutations )
225
260
226
- submission_variables_state .set_values (data_for_evaluation . updates )
261
+ submission_variables_state .set_values (data_diff )
227
262
228
263
# we loop over all steps because we have validations that ensure unique component
229
264
# keys across multiple steps for the whole form.
0 commit comments