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" ,
@@ -80,6 +81,7 @@ def evaluate_form_logic(
80
81
81
82
# 3. Load the (variables) state
82
83
submission_variables_state = submission .load_submission_value_variables_state ()
84
+ actual_initial_data = submission_variables_state .to_python ()
83
85
84
86
# 4. Apply the (dirty) data to the variable state.
85
87
submission_variables_state .set_values (data )
@@ -94,47 +96,36 @@ def evaluate_form_logic(
94
96
# 5.1 - if the action type is to set a variable, update the variable state. This
95
97
# happens inside of iter_evaluate_rules. This is the ONLY operation that is allowed
96
98
# to execute while we're looping through the rules.
99
+ # TODO-5139: I first put this tracking of data difference inside FormioData, but
100
+ # that doesn't seem like it should be its responsibility. Then yielded a complete
101
+ # difference in iter_evaluate_rules, but that fails when there is no rules to be
102
+ # applied: `data_diff` would be undefined. So decided to track the difference
103
+ # outside iter_evaluate_rules, as we also track the mutation operations here.
104
+ # Still not sure about it, though
105
+ data_diff_total = FormioData ()
97
106
with elasticapm .capture_span (
98
107
name = "collect_logic_operations" , span_type = "app.submissions.logic"
99
108
):
100
- for operation in iter_evaluate_rules (
109
+ for operation , mutations in iter_evaluate_rules (
101
110
rules ,
102
111
data_for_evaluation ,
103
112
submission = submission ,
104
113
):
105
114
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 )
115
+ if mutations :
116
+ data_diff_total .update (mutations )
127
117
128
118
# 7. finally, apply the dynamic configuration
129
119
130
120
# we need to apply the context-specific configurations before we can apply
131
121
# mutations based on logic, which is then in turn passed to the serializer(s)
132
- # TODO: refactor this to rely on variables state
122
+ # TODO-5139: rewrite `get_dynamic_configuration` and its callees to work with
123
+ # FormioData instance
133
124
config_wrapper = get_dynamic_configuration (
134
125
config_wrapper ,
135
126
request = None ,
136
127
submission = submission ,
137
- data = data_for_evaluation ,
128
+ data = data_for_evaluation . data ,
138
129
)
139
130
140
131
# 7.1 Apply the component mutation operations
@@ -145,18 +136,16 @@ def evaluate_form_logic(
145
136
# (eventually) hidden BEFORE we do any further processing. This is only a bandaid
146
137
# fix, as the (stale) data has potentially been input for other logic rules.
147
138
# 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
139
for component in config_wrapper :
153
140
key = component ["key" ]
154
- # TODO-5139: is_visible_in_frontend must accept a FormioData instance instead
141
+ # TODO-5139: rewrite `is_visible_in_frontend` and its callees to work with
142
+ # FormioData instance
155
143
is_visible = config_wrapper .is_visible_in_frontend (key , data_for_evaluation .data )
156
144
if is_visible :
157
145
continue
158
146
159
- # Reset the value of any field that may have become hidden again after evaluating the logic
147
+ # Reset the value of any field that may have become hidden again after
148
+ # evaluating the logic
160
149
original_value = initial_data .get (key , empty )
161
150
empty_value = get_component_empty_value (component )
162
151
if original_value is empty or original_value == empty_value :
@@ -166,32 +155,75 @@ def evaluate_form_logic(
166
155
continue
167
156
168
157
# clear the value
169
- data_for_evaluation . update ({ key : empty_value })
170
- data_diff [key ] = empty_value
158
+ data_for_evaluation [ key ] = empty_value
159
+ data_diff_total [key ] = empty_value
171
160
172
161
# 7.2 Interpolate the component configuration with the variables.
162
+ # TODO-5139: rewrite `inject_variables` and its callees to work with FormioData
163
+ # instance
173
164
inject_variables (config_wrapper , data_for_evaluation .data )
174
165
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.
166
+
167
+ # ---------------------------------------------------------------------------------------
168
+
169
+
170
+ # 8. All the processing is now complete, so we can update the state.
171
+ # TODO-5139: when setting a value to SubmissionValueVariable.value, it doesn't
172
+ # automatically get encoded to JSON with the encoder. Probably only happens when
173
+ # saving the model. This means we have to do it manually here or in
174
+ # SubmissionValueVariablesState.set_values. Or do we want to always convert to
175
+ # python objects, like already suggested in
176
+ # SubmissionValueVariableState.set_values? Issue 2324 is related to this
177
+ submission_variables_state .set_values (data_diff_total )
178
+
179
+ # 8.1 build a difference in data for the step. It is important that we only keep the
180
+ # changes in the data, so that old values do not overwrite otherwise debounced
181
+ # client-side data changes
182
+
183
+ # Calculate which data has changed from the initial, for the step.
184
+ relevant_variables = submission_variables_state .get_variables_in_submission_step (
185
+ step , include_unsaved = True
186
+ )
187
+
188
+ # TODO-5139: the problem is that currently `variable.value` can be a json or python
189
+ # object, as `iter_evaluate_rules` now returns python objects. This means that a
190
+ # comparison with `initial_value` will fail, because it's a json value. We could
191
+ # perform this comparison in the python-type domain, but that would mean that
192
+ # invalid data will not be in step.data. Is this a bad thing?
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. Or is it?
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