Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Viicos committed May 24, 2024
1 parent 512c6d7 commit 0aa0423
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 11 deletions.
6 changes: 3 additions & 3 deletions src/openforms/formio/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,9 @@ def __iter__(self) -> Iterator[Component]:
The components are iterated over depth-first, pre-order.
"""
# yield from self.path_map.values() # 10x faster
for node in self._configuration_tree:
yield node.data
yield from self.path_map.values() # 10x faster

Check warning on line 316 in src/openforms/formio/datastructures.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/formio/datastructures.py#L316

Added line #L316 was not covered by tests
# for node in self._configuration_tree:
# yield node.data

@cached_property
def _configuration_tree(self) -> Tree:
Expand Down
11 changes: 11 additions & 0 deletions src/openforms/forms/models/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import uuid as _uuid
from contextlib import suppress
from copy import deepcopy
from functools import cached_property
from typing import Literal

from django.conf import settings
Expand All @@ -11,6 +12,7 @@
from django.db.models import F, Window
from django.db.models.functions import RowNumber
from django.utils.translation import gettext_lazy as _, override
from openforms.formio.datastructures import FormioConfig

from autoslug import AutoSlugField
from privates.fields import PrivateMediaFileField
Expand Down Expand Up @@ -561,6 +563,15 @@ def iter_components(self, recursive=True):
for form_step in self.formstep_set.select_related("form_definition"):
yield from form_step.iter_components(recursive=recursive)

@cached_property
def formio_config(self) -> FormioConfig:

components: list[Component] = []

Check warning on line 569 in src/openforms/forms/models/form.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/forms/models/form.py#L569

Added line #L569 was not covered by tests
for form_step in self.formstep_set.select_related("form_definition"):
components.append(form_step.form_definition.configuration["components"])

Check warning on line 571 in src/openforms/forms/models/form.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/forms/models/form.py#L571

Added line #L571 was not covered by tests

return FormioConfig(components)

Check warning on line 573 in src/openforms/forms/models/form.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/forms/models/form.py#L573

Added line #L573 was not covered by tests

@transaction.atomic
def restore_old_version(
self, form_version_uuid: str, user: User | None = None
Expand Down
6 changes: 2 additions & 4 deletions src/openforms/submissions/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,19 +623,17 @@ def logic_check(self, request, *args, **kwargs):
data = form_data_serializer.validated_data["data"]
if data:
merged_data = FormioData({**submission.data, **data})
submission_step.data = DirtyData(data)

new_configuration = evaluate_form_logic(
evaluate_form_logic(
submission,
submission_step,
merged_data.data,
dirty=True,
request=request,
)
submission_step.form_step.form_definition.configuration = new_configuration

submission_state_logic_serializer = SubmissionStateLogicSerializer(
instance=SubmissionStateLogic(submission=submission, step=submission_step),
context={"request": request, "unsaved_data": data},
context={"request": request},
)
return Response(submission_state_logic_serializer.data)
2 changes: 1 addition & 1 deletion src/openforms/submissions/form_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def evaluate_form_logic(
data_container = DataContainer(state=submission_variables_state)

# 5. Evaluate the logic rules in order
mutation_operations = []
mutation_operations: list[ActionOperation] = []

# 5.1 - if the action type is to set a variable, update the variable state. This
# happens inside of iter_evaluate_rules. This is the ONLY operation that is allowed
Expand Down
268 changes: 265 additions & 3 deletions src/openforms/submissions/models/submission_value_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from openforms.forms.models.form_variable import FormVariable
from openforms.typing import DataMapping, JSONEncodable, JSONSerializable
from openforms.utils.date import format_date_value, parse_datetime, parse_time
from openforms.variables.constants import FormVariableDataTypes
from openforms.variables.constants import FormVariableDataTypes, FormVariableSources
from openforms.variables.service import get_static_variables

from ..constants import SubmissionValueVariableSources
Expand All @@ -31,6 +31,251 @@ def default(self, obj: JSONEncodable | JSONSerializable) -> JSONEncodable:
return to_json() if callable(to_json) else super().default(obj)


from typing import Iterator, Literal, cast, Collection

if TYPE_CHECKING:
from _typeshed import Incomplete
from functools import cached_property
from contextlib import contextmanager
from openforms.forms.models import FormStep, FormDefinition, FormVariable
from openforms.formio.datastructures import FormioConfig
from django.db.models import QuerySet
from ..logic.actions import ActionOperation

class SubmissionState:
def __init__(self, submission: Submission) -> None:
self.submission = submission
self._form_steps, self._submission_steps = self._load_state()

Check warning on line 48 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L47-L48

Added lines #L47 - L48 were not covered by tests

def _load_state(self) -> tuple[list[FormStep], list[SubmissionStep]]:
form_steps: list[FormStep] = list(

Check warning on line 51 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L51

Added line #L51 was not covered by tests
self.submission.form.formstep_set.select_related(
"form_definition"
).order_by("order")
)

# ⚡️ no select_related/prefetch ON PURPOSE - while processing the form steps,
# we're doing this in python as we have the objects already from the query
# above.
_submission_steps = cast(

Check warning on line 60 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L60

Added line #L60 was not covered by tests
QuerySet[SubmissionStep], self.submissionstep_set.all()
)
cached_submission_steps: dict[int, SubmissionStep] = {}

Check warning on line 63 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L63

Added line #L63 was not covered by tests
for submission_step in _submission_steps:
# non-empty value implies that the form_step FK was (cascade) deleted
if submission_step.form_step_history:
# deleted formstep FKs are loaded from history
if submission_step.form_step not in form_steps:
form_steps.append(submission_step.form_step)
cached_submission_steps[submission_step.form_step_id] = submission_step

Check warning on line 70 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L69-L70

Added lines #L69 - L70 were not covered by tests

# sort the steps again in case steps from history were inserted
form_steps = sorted(form_steps, key=lambda s: s.order)

# build the resulting list - some SubmissionStep instances will probably not exist
# in the database yet - this is on purpose!
submission_steps: list[SubmissionStep] = []

Check warning on line 77 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L77

Added line #L77 was not covered by tests
for form_step in form_steps:
if form_step.pk in cached_submission_steps:
submission_step = cached_submission_steps[form_step.pk]

Check warning on line 80 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L80

Added line #L80 was not covered by tests
# replace the python objects to avoid extra queries and/or joins in the
# submission step query.
submission_step.form_step = form_step
submission_step.submission = self.submission

Check warning on line 84 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L83-L84

Added lines #L83 - L84 were not covered by tests
else:
# there's no known DB record for this, so we create a fresh, unsaved
# instance and return this
submission_step = SubmissionStep(uuid=None, submission=self, form_step=form_step)
submission_steps.append(submission_step)

Check warning on line 89 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L88-L89

Added lines #L88 - L89 were not covered by tests

return form_steps, submission_steps

Check warning on line 91 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L91

Added line #L91 was not covered by tests

@property
def form_steps(self) -> list[FormStep]:
"""The list of form steps related to the submission.
TODO Add note about form steps being created from history.
"""
return self._form_steps

Check warning on line 99 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L99

Added line #L99 was not covered by tests

@property
def submission_steps(self) -> list[SubmissionStep]:
"""The list of submission steps related to the submission.
TODO Add note about submission steps not necessarily persisted in DB.
"""
return self._submission_steps

Check warning on line 107 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L107

Added line #L107 was not covered by tests


class VariableDoesNotExist(Exception): ...


class VariableReadOnly(Exception): ...


class VariableTypeError(Exception): ...


class VariablesState:
def __init__(self, submission_state: SubmissionState) -> None:
self.submission_state = submission_state
self.submission = submission_state.submission
self.formio_config = self.submission.form.formio_config
self._data_mutations: FormioData | None = None

Check warning on line 124 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L121-L124

Added lines #L121 - L124 were not covered by tests

self.variables = self._collect_variables()

Check warning on line 126 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L126

Added line #L126 was not covered by tests

@cached_property
def _static_data(self) -> dict[str, Any]:
return {
variable.key: variable.initial_value
for variable in get_static_variables(submission=self.submission)
}

#@cached_property?
@property
def data(self) -> FormioData:
formio_data = FormioData()

Check warning on line 138 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L138

Added line #L138 was not covered by tests

for variable_key, variable in self.variables.items():
if variable.source != SubmissionValueVariableSources.sensitive_data_cleaner:
formio_data[variable_key] = variable.value

Check warning on line 142 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L142

Added line #L142 was not covered by tests

# TODO check if `update` behaves as expected
formio_data.update(self._static_data)

Check warning on line 145 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L145

Added line #L145 was not covered by tests
# if self._data_mutations is not None:
# formio_data.update(self._data_mutations)
return formio_data

Check warning on line 148 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L148

Added line #L148 was not covered by tests

# TODO add filter to `persist`: only persist user defined vars, etc.
def persist(self, *, limit_to: Literal["component"]):
SubmissionValueVariable.objects.bulk_create_or_update_from_data_v2(
self.data,
{k: var for k, var in self.variables.items() if var.source == SubmissionValueVariableSources.user_input},
)

# TODO rename: `mutate`/`mutate_data`?
@contextmanager
def enter_evaluation_context(
self,
*,
mutation_operations: list[ActionOperation],
for_submission_step: SubmissionStep,
) -> Iterator[FormioData]:
self._data_mutations = FormioData()
try:
yield self._data_mutations

Check warning on line 167 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L165-L167

Added lines #L165 - L167 were not covered by tests
finally:
formio_config = for_submission_step.form_step.form_definition.formio_config

Check warning on line 169 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L169

Added line #L169 was not covered by tests

self._update_configuration(formio_config)

Check warning on line 171 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L171

Added line #L171 was not covered by tests

for mutation in mutation_operations:
mutation.apply(for_submission_step, formio_config)

Check warning on line 174 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L174

Added line #L174 was not covered by tests

self._apply_hidden_state()

Check warning on line 176 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L176

Added line #L176 was not covered by tests

self._data_mutations = None

Check warning on line 178 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L178

Added line #L178 was not covered by tests


def __getitem__(self, key: str, /) -> Incomplete:
try:
return self.data[key]
except Exception: # TODO good exception class
raise VariableDoesNotExist

Check warning on line 185 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L182-L185

Added lines #L182 - L185 were not covered by tests

def __setitem__(self, key: str, value: Incomplete, /) -> None:
# TODO: Should it be readonly by default, unless
# inside `enter_evaluation_context`?
# Or should we allow persist by default if not inside?
try:
submission_variable = self.variables[key]
except KeyError:

Check warning on line 193 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L191-L193

Added lines #L191 - L193 were not covered by tests
if key in self._static_data:
# TODO: better way to know if it is a static variable? With a source?
raise VariableReadOnly
raise VariableDoesNotExist

Check warning on line 197 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L196-L197

Added lines #L196 - L197 were not covered by tests

# TODO: check data type, raise `VariableTypeError`

if self._data_mutations is not None and self.data[key] != value:
# TODO only record mutation if `self.data[key] != value`?
# Maybe both?
self._data_mutations[key] = value
submission_variable.value = value

Check warning on line 205 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L204-L205

Added lines #L204 - L205 were not covered by tests
else:
submission_variable.value = value
self._update_configuration(self.formio_config)
self._apply_hidden_state()

Check warning on line 209 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L207-L209

Added lines #L207 - L209 were not covered by tests

def _collect_variables(self) -> dict[str, SubmissionValueVariable]:
# leverage the (already populated) submission state to get access to form
# steps and form definitions
form_definition_map: dict[int, FormDefinition] = {
form_step.form_definition.pk: form_step.form_definition
for form_step in self.submission_state.form_steps
}

# Build a collection of all form variables
all_form_variables: dict[str, FormVariable] = {
form_variable.key: form_variable
for form_variable in self.submission.form.formvariable_set.all()
}
# optimize the access from form_variable.form_definition using the already
# existing map, saving a `select_related` call on data we (probably) already
# have
for form_variable in all_form_variables.values():
if not (form_def_id := form_variable.form_definition_id):
# No related form definition for non-component form variables (user defined, ...)
continue
form_variable.form_definition = form_definition_map[form_def_id]

Check warning on line 231 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L230-L231

Added lines #L230 - L231 were not covered by tests

# now retrieve the persisted variables from the submission - avoiding select_related
# calls since we already have the relevant data
all_submission_variables: dict[str, SubmissionValueVariable] = {
submission_value_variables.key: submission_value_variables
for submission_value_variables in self.submission.submissionvaluevariable_set.all()
}
# do the join by `key`, which is unique across the form
for variable_key, submission_value_variable in all_submission_variables.items():
if variable_key not in all_form_variables:
continue
submission_value_variable.form_variable = all_form_variables[variable_key]

Check warning on line 243 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L242-L243

Added lines #L242 - L243 were not covered by tests

# finally, add in the unsaved variables from default values
for variable_key, form_variable in all_form_variables.items():
# if the key exists from the saved values in the DB, do nothing
if variable_key in all_submission_variables:
continue

Check warning on line 249 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L249

Added line #L249 was not covered by tests
# TODO Fill source field
unsaved_submission_var = SubmissionValueVariable(

Check warning on line 251 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L251

Added line #L251 was not covered by tests
submission=self.submission,
form_variable=form_variable,
key=variable_key,
value=form_variable.get_initial_value(),
is_initially_prefilled=(form_variable.prefill_plugin != ""),
)
all_submission_variables[variable_key] = unsaved_submission_var

Check warning on line 258 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L258

Added line #L258 was not covered by tests

return all_submission_variables

Check warning on line 260 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L260

Added line #L260 was not covered by tests

def _update_configuration(self, configuration: FormioConfig):
for component in configuration:

Check warning on line 263 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L263

Added line #L263 was not covered by tests
pass
# Call:
# rewrite_formio_component(configuration, self.submission, self.data)
# get_translated_custom_error_messages
# localize_components
# inject_prefill
# ... it should be possible to have these functions
# for each component, and avoid having them to iterate
# over all the components over and over.

def _apply_hidden_state(self):
...



@dataclass
class SubmissionValueVariablesState:
submission: "Submission"
Expand Down Expand Up @@ -205,7 +450,24 @@ def set_values(self, data: DataMapping) -> None:
variable.value = new_value


class SubmissionValueVariableManager(models.Manager):
class SubmissionValueVariableManager(models.Manager["SubmissionValueVariable"]):
def bulk_create_or_update_from_data_v2(
self,
data: FormioData,
# TODO might use list instead:
submission_variables: dict[str, SubmissionValueVariable],
) -> None:
variables_to_create = []
variables_to_update = []
variables_keys_to_delete = []

Check warning on line 462 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L460-L462

Added lines #L460 - L462 were not covered by tests

for key, variable in submission_variables.items():
try:
variable.value = data[key]
except KeyError: # TODO same, find proper exception

Check warning on line 467 in src/openforms/submissions/models/submission_value_variable.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/submissions/models/submission_value_variable.py#L465-L467

Added lines #L465 - L467 were not covered by tests
...


def bulk_create_or_update_from_data(
self,
data: DataMapping,
Expand Down Expand Up @@ -309,7 +571,7 @@ class SubmissionValueVariable(models.Model):
blank=True,
)

objects = SubmissionValueVariableManager()
objects: SubmissionValueVariableManager = SubmissionValueVariableManager()

class Meta:
verbose_name = _("Submission value variable")
Expand Down

0 comments on commit 0aa0423

Please sign in to comment.