|
1 |
| -from typing import TYPE_CHECKING |
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from copy import deepcopy |
| 4 | +from typing import TYPE_CHECKING, ClassVar |
2 | 5 |
|
3 | 6 | from django.db import models, transaction
|
4 | 7 | from django.db.models import CheckConstraint, Q
|
|
31 | 34 | USER_DEFINED = Q(source=FormVariableSources.user_defined)
|
32 | 35 |
|
33 | 36 |
|
34 |
| -class FormVariableManager(models.Manager): |
| 37 | +class FormVariableManager(models.Manager["FormVariable"]): |
35 | 38 | use_in_migrations = True
|
36 | 39 |
|
37 | 40 | @transaction.atomic
|
38 | 41 | def create_for_form(self, form: "Form") -> None:
|
39 | 42 | form_steps = form.formstep_set.select_related("form_definition")
|
40 | 43 |
|
41 | 44 | for form_step in form_steps:
|
42 |
| - self.create_for_formstep(form_step) |
| 45 | + self.synchronize_for(form_step.form_definition) |
43 | 46 |
|
44 |
| - def create_for_formstep(self, form_step: "FormStep") -> list["FormVariable"]: |
| 47 | + def create_for_formstep(self, form_step: FormStep) -> list[FormVariable]: |
45 | 48 | form_definition_configuration = form_step.form_definition.configuration
|
46 | 49 | component_keys = [
|
47 | 50 | component["key"]
|
@@ -101,6 +104,109 @@ def create_for_formstep(self, form_step: "FormStep") -> list["FormVariable"]:
|
101 | 104 |
|
102 | 105 | return self.bulk_create(form_variables)
|
103 | 106 |
|
| 107 | + @transaction.atomic |
| 108 | + def synchronize_for(self, form_definition: FormDefinition): |
| 109 | + """ |
| 110 | + Synchronize the form variables for a given form definition. |
| 111 | +
|
| 112 | + This creates, updates and/or removes form variables related to the provided |
| 113 | + :class:`FormDefinition` instance. It needs to be called whenever the Formio |
| 114 | + configuration of a form definition is changed so that our form variables in each |
| 115 | + form making use of the form definition accurately reflect the configuration. |
| 116 | +
|
| 117 | + Note that we *don't* remove variables related to other form definitions, as |
| 118 | + multiple isolated transactions for different form definitions can happen at the |
| 119 | + same time. |
| 120 | + """ |
| 121 | + # Build the desired state |
| 122 | + desired_variables: list[FormVariable] = [] |
| 123 | + # XXX: looping over the configuration_wrapper is not (yet) viable because it |
| 124 | + # also yields the components nested inside edit grids, which we need to ignore. |
| 125 | + # So, we stick to iter_components. Performance wise this should be okay since we |
| 126 | + # only need to do one pass. |
| 127 | + configuration = form_definition.configuration |
| 128 | + for component in iter_components(configuration=configuration, recursive=True): |
| 129 | + # we need to ignore components that don't actually hold any values - there's |
| 130 | + # no point to create variables for those. |
| 131 | + if is_layout_component(component): |
| 132 | + continue |
| 133 | + if component["type"] in ("content", "softRequiredErrors"): |
| 134 | + continue |
| 135 | + if component_in_editgrid(configuration, component): |
| 136 | + continue |
| 137 | + |
| 138 | + # extract options from the component |
| 139 | + prefill_plugin = glom(component, "prefill.plugin", default="") or "" |
| 140 | + prefill_attribute = glom(component, "prefill.attribute", default="") or "" |
| 141 | + prefill_identifier_role = glom( |
| 142 | + component, "prefill.identifierRole", default=IdentifierRoles.main |
| 143 | + ) |
| 144 | + |
| 145 | + desired_variables.append( |
| 146 | + self.model( |
| 147 | + form=None, # will be set later when visiting all affected forms |
| 148 | + key=component["key"], |
| 149 | + form_definition=form_definition, |
| 150 | + prefill_plugin=prefill_plugin, |
| 151 | + prefill_attribute=prefill_attribute, |
| 152 | + prefill_identifier_role=prefill_identifier_role, |
| 153 | + name=component.get("label") or component["key"], |
| 154 | + is_sensitive_data=component.get("isSensitiveData", False), |
| 155 | + source=FormVariableSources.component, |
| 156 | + data_type=get_component_datatype(component), |
| 157 | + initial_value=get_component_default_value(component), |
| 158 | + ) |
| 159 | + ) |
| 160 | + |
| 161 | + desired_keys = [variable.key for variable in desired_variables] |
| 162 | + |
| 163 | + # if the Formio configuration of the form definition itself is updated and |
| 164 | + # components have been removed or their keys have changed, we know for certain |
| 165 | + # we can discard those old form variables - it doesn't matter which form they |
| 166 | + # belong to. |
| 167 | + stale_variables = self.filter(form_definition=form_definition).exclude( |
| 168 | + key__in=desired_keys |
| 169 | + ) |
| 170 | + stale_variables.delete() |
| 171 | + |
| 172 | + # check which form (steps) are affected and patch them up. It is irrelevant whether |
| 173 | + # the form definition is re-usable or not, though semantically at most one form step |
| 174 | + # should be found for single-use form definitions. |
| 175 | + # fmt: off |
| 176 | + affected_form_steps = ( |
| 177 | + form_definition |
| 178 | + .formstep_set # pyright: ignore[reportAttributeAccessIssue] |
| 179 | + .select_related("form") |
| 180 | + ) |
| 181 | + # fmt: on |
| 182 | + |
| 183 | + # Finally, collect all the instances and efficiently upsert them - creating missing |
| 184 | + # variables and updating existing variables in a single query. |
| 185 | + to_upsert: list[FormVariable] = [] |
| 186 | + for step in affected_form_steps: |
| 187 | + for variable in desired_variables: |
| 188 | + form_specific_variable = deepcopy(variable) |
| 189 | + form_specific_variable.form = step.form |
| 190 | + to_upsert.append(form_specific_variable) |
| 191 | + |
| 192 | + self.bulk_create( |
| 193 | + to_upsert, |
| 194 | + # enables UPSERT behaviour so that existing records get updated and missing |
| 195 | + # records inserted |
| 196 | + update_conflicts=True, |
| 197 | + update_fields=( |
| 198 | + "prefill_plugin", |
| 199 | + "prefill_attribute", |
| 200 | + "prefill_identifier_role", |
| 201 | + "name", |
| 202 | + "is_sensitive_data", |
| 203 | + "source", |
| 204 | + "data_type", |
| 205 | + "initial_value", |
| 206 | + ), |
| 207 | + unique_fields=("form", "key"), |
| 208 | + ) |
| 209 | + |
104 | 210 |
|
105 | 211 | class FormVariable(models.Model):
|
106 | 212 | form = models.ForeignKey(
|
@@ -198,7 +304,9 @@ class FormVariable(models.Model):
|
198 | 304 | null=True,
|
199 | 305 | )
|
200 | 306 |
|
201 |
| - objects = FormVariableManager() |
| 307 | + objects: ClassVar[ # pyright: ignore[reportIncompatibleVariableOverride] |
| 308 | + FormVariableManager |
| 309 | + ] = FormVariableManager() |
202 | 310 |
|
203 | 311 | class Meta:
|
204 | 312 | verbose_name = _("Form variable")
|
|
0 commit comments