Skip to content

Commit

Permalink
✨ [#4980] Revise processing of radio, select, and selectboxes components
Browse files Browse the repository at this point in the history
When the data source for these components is another form variable, the options are not available in the component.as_json_schema methods based on the form alone, because all user defined variables and calculated values are saved in the DB at the moment of submission. So just the form is not enough to get this data, and we need to do some post-processing in the plugin where we have the submission available
  • Loading branch information
viktorvanwijk committed Jan 22, 2025
1 parent ad5f2d3 commit 066f1ec
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 3 deletions.
70 changes: 67 additions & 3 deletions src/openforms/registrations/contrib/json_dump/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.translation import gettext_lazy as _

from glom import assign, glom
from zgw_consumers.client import build_client

from openforms.formio.dynamic_config import rewrite_formio_components
from openforms.formio.components.utils import to_multiple
from openforms.formio.typing import Component
from openforms.forms.utils import form_variables_to_json_schema
from openforms.submissions.logic.datastructures import DataContainer
from openforms.submissions.models import (
Submission,
SubmissionFileAttachment,
Expand Down Expand Up @@ -74,7 +77,9 @@ def post_processing(
"""Post-processing of values and schema.
File components need special treatment, as we send the content of the file
encoded with base64, instead of the output from the serializer.
encoded with base64, instead of the output from the serializer. Also, Radio,
Select, and SelectBoxes components need to be updated if their data source is
set to another form variable.
:param submission: Submission
:param values: JSONObject
Expand All @@ -92,7 +97,7 @@ def post_processing(
# not relevant here
continue

component = get_component(variable)
component = get_component(variable, submission)
match component["type"]:
case "file":
encoded_attachments = [
Expand Down Expand Up @@ -141,7 +146,53 @@ def post_processing(
)
values[key] = value
schema["properties"][key] = base_schema
case "radio":
data_src = component.get("openForms", {}).get("dataSrc")
if data_src != "variable":
# Only components where another variable is used as a data
# source need to be processed, so skip this one
continue

choices = [options["value"] for options in component["values"]]
choices.append("") # Take into account an unfilled field
assign(schema, f"properties.{variable.key}.enum", choices)
case "select":
data_src = component.get("openForms", {}).get("dataSrc")
if data_src != "variable":
# Only components where another variable is used as a data
# source need to be processed, so skip this one
continue

choices = [
options["value"] for options in component["data"]["values"]
]
choices.append("") # Take into account an unfilled field

base_schema_path = f"properties.{variable.key}"
sub_path = (
"enum"
if glom(schema, f"{base_schema_path}.type") == "string"
else "items.enum"
)
assign(schema, f"{base_schema_path}.{sub_path}", choices)
case "selectboxes":
data_src = component.get("openForms", {}).get("dataSrc")

# Only components where another variable is used as a data
# source need to be processed
if data_src == "variable":
properties = {
options["value"]: {"type": "boolean"}
for options in component["values"]
}
schema["properties"][variable.key].update(
{
"properties": properties,
"required": list(properties.keys()),
"additionalProperties": False,
}
)

# If the select boxes component is not filled, set required
# properties to empty list
if not values[key]:
Expand All @@ -161,13 +212,26 @@ def encode_attachment(attachment: SubmissionFileAttachment) -> str:
return base64.b64encode(f.read()).decode()


def get_component(variable: SubmissionValueVariable) -> Component | None:
def get_component(
variable: SubmissionValueVariable, submission : Submission
) -> Component | None:
"""Get the component from a submission value variable.
:param variable: SubmissionValueVariable
:param submission: Submission
:return component: None if the form variable has no form definition
"""
config_wrapper = variable.form_variable.form_definition.configuration_wrapper

# Update components. This is necessary to update the options for Select,
# SelectBoxes, and Radio components, which get their options from another form
# variable.
state = submission.load_submission_value_variables_state()
data = DataContainer(state=state)
config_wrapper = rewrite_formio_components(
config_wrapper, submission=submission, data=data.data
)

component = config_wrapper.component_map[variable.key]

return component
187 changes: 187 additions & 0 deletions src/openforms/registrations/contrib/json_dump/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
from zgw_consumers.test.factories import ServiceFactory

from openforms.submissions.tests.factories import (
FormVariableFactory,
SubmissionFactory,
SubmissionFileAttachmentFactory,
)
from openforms.utils.tests.vcr import OFVCRMixin
from openforms.variables.constants import FormVariableDataTypes

from ..config import JSONDumpOptions
from ..plugin import JSONDumpRegistration
Expand Down Expand Up @@ -385,3 +387,188 @@ def test_required_in_schema_is_empty_if_select_boxes_component_unfilled(self):
[]
)

def test_select_component_with_form_variable_as_data_source(self):

submission = SubmissionFactory.from_components(
[
{
"label": "Select",
"key": "select",
"type": "select",
"multiple": True,
"openForms": {
"dataSrc": "variable",
"itemsExpression": {"var": "valuesForSelect"},
},
"data": {
"values": [],
"json": "",
"url": "",
"resource": "",
"custom": "",
},
},
],
completed=True,
submitted_data={"select": ["A", "C"]},
)

FormVariableFactory.create(
form=submission.form,
name="Values for select",
key="valuesForSelect",
user_defined=True,
data_type=FormVariableDataTypes.array,
initial_value=["A", "B", "C"],
)

json_plugin = JSONDumpRegistration("json_registration_plugin")
set_submission_reference(submission)

json_form_options = dict(
service=(ServiceFactory(api_root="http://localhost:80/")),
relative_api_endpoint="json_plugin",
form_variables=["select"],
)

res = json_plugin.register_submission(submission, json_form_options)

self.assertEqual(
glom(res, "api_response.data.schema.properties.select.items.enum"),
["A", "B", "C", ""],
)

def test_select_boxes_component_with_form_variable_as_data_source(self):

submission = SubmissionFactory.from_components(
[
{
"label": "Select Boxes",
"key": "selectBoxes",
"type": "selectboxes",
"openForms": {
"dataSrc": "variable",
"translations": {},
"itemsExpression": {"var": "valuesForSelectBoxes"},
},
"values": [],
},
],
completed=True,
submitted_data={"selectBoxes": {"A": True, "B": False, "C": True}},
)

FormVariableFactory.create(
form=submission.form,
name="Values for select boxes",
key="valuesForSelectBoxes",
user_defined=True,
data_type=FormVariableDataTypes.array,
initial_value=["A", "B", "C"],
)

json_plugin = JSONDumpRegistration("json_registration_plugin")
set_submission_reference(submission)

json_form_options = dict(
service=(ServiceFactory(api_root="http://localhost:80/")),
relative_api_endpoint="json_plugin",
form_variables=["selectBoxes"],
)

res = json_plugin.register_submission(submission, json_form_options)

expected_schema = {
"title": "Select Boxes",
"type": "object",
"additionalProperties": False,
"properties": {
"A": {"type": "boolean"},
"B": {"type": "boolean"},
"C": {"type": "boolean"},
},
"required": ["A", "B", "C"],
}

self.assertEqual(
glom(res, "api_response.data.schema.properties.selectBoxes"),
expected_schema
)

def test_select_boxes_schema_required_is_empty_when_no_data_is_submitted(self):
submission = SubmissionFactory.from_components(
[
{
"label": "Select Boxes",
"key": "selectBoxes",
"type": "selectboxes",
"values": [
{"label": "A", "value": "a"},
{"label": "B", "value": "b"},
{"label": "C", "value": "c"},
],
},
],
completed=True,
submitted_data={"selectBoxes": {}},
)

json_plugin = JSONDumpRegistration("json_registration_plugin")
set_submission_reference(submission)

json_form_options = dict(
service=(ServiceFactory(api_root="http://localhost:80/")),
relative_api_endpoint="json_plugin",
form_variables=["selectBoxes"],
)

res = json_plugin.register_submission(submission, json_form_options)

self.assertEqual(
glom(res, "api_response.data.schema.properties.selectBoxes.required"),
[],
)

def test_radio_component_with_form_variable_as_data_source(self):
submission = SubmissionFactory.from_components(
[
{
"label": "Radio",
"key": "radio",
"type": "radio",
"openForms": {
"dataSrc": "variable",
"translations": {},
"itemsExpression": {"var": "valuesForRadio"},
},
"values": [],
},
],
completed=True,
submitted_data={"radio": "A"},
)

FormVariableFactory.create(
form=submission.form,
name="Values for radio",
key="valuesForRadio",
user_defined=True,
data_type=FormVariableDataTypes.array,
initial_value=["A", "B", "C"],
)

json_plugin = JSONDumpRegistration("json_registration_plugin")
set_submission_reference(submission)

json_form_options = dict(
service=(ServiceFactory(api_root="http://localhost:80/")),
relative_api_endpoint="json_plugin",
form_variables=["radio"],
)

res = json_plugin.register_submission(submission, json_form_options)

self.assertEqual(
glom(res, "api_response.data.schema.properties.radio.enum"),
["A", "B", "C", ""],
)

0 comments on commit 066f1ec

Please sign in to comment.