Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import Counter
from datetime import date

from django.core.serializers.json import DjangoJSONEncoder
from django.utils.translation import gettext_lazy as _

from drf_polymorphic.serializers import PolymorphicSerializer
Expand Down Expand Up @@ -64,6 +65,7 @@ class LogicValueActionSerializer(serializers.Serializer):
"(other) Form.io components."
),
validators=[JsonLogicValidator()],
encoder=DjangoJSONEncoder,
)


Expand Down
42 changes: 21 additions & 21 deletions src/openforms/submissions/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from copy import deepcopy
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from datetime import timedelta
from typing import TypedDict

from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase
from django.core.serializers.json import DjangoJSONEncoder
from django.db import transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -35,10 +36,9 @@
FormDefinitionSerializer,
LogicComponentActionSerializer,
)
from openforms.forms.constants import SubmissionAllowedChoices
from openforms.forms.constants import LogicActionTypes, SubmissionAllowedChoices
from openforms.forms.models import FormLogic
from openforms.forms.validators import validate_not_deleted
from openforms.typing import JSONValue, VariableValue
from openforms.utils.json_logic import partially_evaluate_json_logic
from openforms.utils.urls import build_absolute_uri

Expand Down Expand Up @@ -230,21 +230,10 @@ def to_representation(self, instance):
return super().to_representation(instance)


def _to_json(value: VariableValue) -> JSONValue:
match value:
case dict():
return {k: _to_json(v) for k, v in value.items()}
case list():
return [_to_json(v) for v in value]
case date() | datetime() | time():
return value.isoformat()
case _:
return value


class FormLogicFrontendSerializer(OrderedModelSerializer):
"""Represent logic rules which can be executed in the frontend."""

# Note that both fields are processed in the `to_representation` method.
actions = LogicComponentActionSerializer(
read_only=True,
many=True,
Expand All @@ -253,12 +242,14 @@ class FormLogicFrontendSerializer(OrderedModelSerializer):
"Actions triggered when the trigger expression evaluates to 'truthy'."
),
)
json_logic_trigger = serializers.SerializerMethodField(
json_logic_trigger = serializers.JSONField(
read_only=True,
label=_("JSON Logic Trigger"),
help_text=_(
"The trigger expression to determine if the actions should execute. Will "
"be (partially) evaluated based on available data."
),
encoder=DjangoJSONEncoder,
)

class Meta:
Expand All @@ -268,8 +259,7 @@ class Meta:
"actions",
)

@extend_schema_field(serializers.JSONField)
def get_json_logic_trigger(self, instance) -> JSONValue:
def to_representation(self, instance):
step = self.context["submission_step"]
state = step.submission.load_submission_value_variables_state()
# We include all data, to make sure we have a (empty) value for every variable.
Expand All @@ -282,14 +272,24 @@ def get_json_logic_trigger(self, instance) -> JSONValue:
for key in state.get_variables_in_submission_step(step).keys():
data.pop(key)

# Add data type information before partially evaluating - otherwise we lose
# the variable context.
# Process action
for action in instance.actions:
if action["action"]["type"] != LogicActionTypes.variable:
continue

value = add_data_type_information(action["action"]["value"], state)
value, _ = partially_evaluate_json_logic(value, data)
action["action"]["value"] = value

# Process JSON logic trigger
json_logic_trigger = add_data_type_information(
instance.json_logic_trigger, state
)
json_logic_trigger, _ = partially_evaluate_json_logic(json_logic_trigger, data)
instance.json_logic_trigger = json_logic_trigger

return _to_json(json_logic_trigger)
# Serialize
return super().to_representation(instance)


class SubmissionStepSerializer(NestedHyperlinkedModelSerializer):
Expand Down
70 changes: 70 additions & 0 deletions src/openforms/submissions/tests/test_get_submission_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,3 +1330,73 @@ def test_with_date_trigger_that_could_be_partially_resolved(self):
]
}
self.assertEqual(expected, response.json()["logicRules"][0]["jsonLogicTrigger"])

def test_variable_action_with_json_logic_expression_as_value(self):
form = FormFactory.create(new_logic_evaluation_enabled=True)
step_1 = FormStepFactory.create(
form=form,
form_definition__configuration={
"components": [
{"type": "textfield", "key": "isoDuration", "label": "ISO duration"}
]
},
)
step_2 = FormStepFactory.create(
form=form,
form_definition__configuration={
"components": [
{"type": "date", "key": "dateOfBirth", "label": "Date of birth"},
{
"type": "date",
"key": "dateOfBirthMinusDuration",
"label": "Date of birth minus duration",
},
]
},
)
# This is an actual logic rule used by municipalities!
rule = FormLogicFactory.create(
form=form,
json_logic_trigger=True,
actions=[
{
"action": {
"type": "variable",
"value": {
"-": [
{"var": "dateOfBirth"},
{"duration": {"var": "isoDuration"}},
]
},
},
"variable": "dateOfBirthMinusDuration",
}
],
)
# Step 2 will be assigned here, because the variable action applies to
# "dateOfBirthMinusDuration".
rule.form_steps.set([step_2])

submission = SubmissionFactory.create(form=form)
self._add_submission_to_session(submission)

# Simulate submitting step 1
SubmissionStepFactory.create(
form_step=step_1, submission=submission, data={"isoDuration": "P18Y"}
)

# Get step 2
url = reverse(
"api:submission-steps-detail",
kwargs={"submission_uuid": submission.uuid, "step_uuid": step_2.uuid},
)
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)

# The "dateOfBrith" variable should have the date operator added to it, and the
# "isoDuration" variable should be prefilled because it was already submitted
# on the previous step.
expected = {"-": [{"date": [{"var": ["dateOfBirth"]}]}, {"duration": ["P18Y"]}]}
self.assertEqual(
expected, response.json()["logicRules"][0]["actions"][0]["action"]["value"]
)
Loading