diff --git a/src/openforms/authentication/static_variables/static_variables.py b/src/openforms/authentication/static_variables/static_variables.py index 535f7bdead..77cab2b858 100644 --- a/src/openforms/authentication/static_variables/static_variables.py +++ b/src/openforms/authentication/static_variables/static_variables.py @@ -92,7 +92,7 @@ def as_json_schema(self): return { "title": "Authentication type", "type": "string", - "enum": AuthAttribute.values, + "enum": [*AuthAttribute.values, ""], } diff --git a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js index bdd11f7084..ba1002ae1f 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -771,6 +771,8 @@ export const ConfiguredBackends = { service: 1, path: 'example/endpoint', variables: [], + requiredMetadataVariables: [], + additionalMetadataVariables: [], }, }, ], @@ -1030,6 +1032,8 @@ export const JSONDump = { service: 1, path: 'example/endpoint', variables: [], + requiredMetadataVariables: ["form_name"], + additionalMetadataVariables: ["auth_bsn"], }, }, ], @@ -1089,7 +1093,7 @@ export const JSONDump = { formDefinition: null, name: 'BSN', key: 'auth_bsn', - source: 'static', + source: '', prefillPlugin: '', prefillAttribute: '', prefillIdentifierRole: '', @@ -1100,6 +1104,22 @@ export const JSONDump = { serviceFetchConfiguration: undefined, initialValue: '', }, + { + form: null, + formDefinition: null, + name: 'Form name', + key: 'form_name', + source: '', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: '', + prefillOptions: {}, + dataType: 'string', + dataFormat: '', + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: '', + } ], }, diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpOptionsForm.js index 987e8e09c0..27008db564 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpOptionsForm.js @@ -11,7 +11,7 @@ import { } from 'components/admin/forms/ValidationErrors'; import {getChoicesFromSchema} from 'utils/json-schema'; -import {Path, ServiceSelect, Variables} from './fields'; +import {MetadataVariables, Path, ServiceSelect, Variables} from './fields'; const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { const validationErrors = useContext(ValidationErrorContext); @@ -38,6 +38,8 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { service: null, path: '', variables: [], + requiredMetadataVariables: [], + additionalMetadataVariables: [], ...formData, }} onSubmit={values => onChange({formData: values})} @@ -48,6 +50,7 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { + @@ -69,6 +72,8 @@ JSONDumpOptionsForm.propTypes = { service: PropTypes.number, path: PropTypes.string, variables: PropTypes.arrayOf(PropTypes.string), + requiredMetadataVariables: PropTypes.arrayOf(PropTypes.string), + additionalMetadataVariables: PropTypes.arrayOf(PropTypes.string) }), onChange: PropTypes.func.isRequired, }; diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpSummaryHandler.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpSummaryHandler.js index 0c74f9e4d8..f39a31dd49 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpSummaryHandler.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpSummaryHandler.js @@ -2,19 +2,39 @@ import PropTypes from 'prop-types'; import React from 'react'; import {IconNo, IconYes} from 'components/admin/BooleanIcons'; +import {FormattedMessage} from 'react-intl'; const JSONDumpSummaryHandler = ({variable, backendOptions}) => { - const isIncluded = backendOptions.variables.includes(variable.key); + const isIncludedInVariables = backendOptions.variables.includes(variable.key) + const isIncludedInMetadata = backendOptions.requiredMetadataVariables.includes(variable.key) + || backendOptions.additionalMetadataVariables.includes(variable.key) - return isIncluded ? : ; + return ( + <> + + {isIncludedInVariables ? : } + {/*TODO-5012: maybe exclude this for non static variables?*/} +
+ + {isIncludedInMetadata ? : } + + ); }; JSONDumpSummaryHandler.propTypes = { variable: PropTypes.shape({ - key: PropTypes.string.isRequired, + source: PropTypes.string.isRequired, }).isRequired, backendOptions: PropTypes.shape({ variables: PropTypes.arrayOf(PropTypes.string).isRequired, + requiredMetadataVariables: PropTypes.arrayOf(PropTypes.string).isRequired, + additionalMetadataVariables: PropTypes.arrayOf(PropTypes.string).isRequired, }).isRequired, }; diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpVariableConfigurationEditor.js index 75f5b4077c..d34017a628 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpVariableConfigurationEditor.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpVariableConfigurationEditor.js @@ -9,39 +9,72 @@ import {Checkbox} from 'components/admin/forms/Inputs'; const JSONDumpVariableConfigurationEditor = ({variable}) => { const { - values: {variables = []}, + values: {variables = [], additionalMetadataVariables = [], requiredMetadataVariables = []}, setFieldValue, } = useFormikContext(); const isIncluded = variables.includes(variable.key); + const isRequiredInMetadata = requiredMetadataVariables.includes(variable.key) + const isInAdditionalMetadata = additionalMetadataVariables.includes(variable.key) return ( - - - - } - helpText={ - - } - checked={isIncluded} - onChange={event => { - const shouldBeIncluded = event.target.checked; - const newVariables = shouldBeIncluded - ? [...variables, variable.key] // add the variable to the array - : variables.filter(key => key !== variable.key); // remove the variable from the array - setFieldValue('variables', newVariables); - }} - /> - - + <> + + + + } + helpText={ + + } + checked={isIncluded} + onChange={event => { + const shouldBeIncluded = event.target.checked; + const newVariables = shouldBeIncluded + ? [...variables, variable.key] // add the variable to the array + : variables.filter(key => key !== variable.key); // remove the variable from the array + setFieldValue('variables', newVariables); + }} + /> + + + + + + + } + helpText={ + + } + checked={isInAdditionalMetadata || isRequiredInMetadata} + onChange={event => { + const shouldBeIncluded = event.target.checked; + const newVariables = shouldBeIncluded + ? [...additionalMetadataVariables, variable.key] // add the variable to the array + : additionalMetadataVariables.filter(key => key !== variable.key); // remove the variable from the array + setFieldValue('additionalMetadataVariables', newVariables); + }} + disabled={isRequiredInMetadata || variable.source !== ""} // disable if it is not required or the variable is not a static variable + /> + + + ); }; diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/MetadataVariables.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/MetadataVariables.js new file mode 100644 index 0000000000..8c0c6c3f11 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/MetadataVariables.js @@ -0,0 +1,48 @@ +import { useField } from 'formik'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + + + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import VariableSelection from 'components/admin/forms/VariableSelection'; + + +// TODO-5012: rename to additionalMetadataVariables? +const MetadataVariables = () => { + const [fieldProps] = useField('additionalMetadataVariables'); + const [, {value}] = useField('requiredMetadataVariables'); + + return ( + + + } + helpText={ + + } + noManageChildProps + > + variable.source === "" && !value.includes(variable.key)} // Only show static variables and variables which are not already required + /> + + + ); +}; + +export default MetadataVariables; diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/index.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/index.js index ab0ba696a9..e13d87de14 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/index.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/index.js @@ -1,5 +1,6 @@ +import MetadataVariables from './MetadataVariables'; import Path from './Path'; import ServiceSelect from './ServiceSelect'; import Variables from './Variables'; -export {Path, ServiceSelect, Variables}; +export {MetadataVariables, Path, ServiceSelect, Variables}; diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index ff3f45280a..22ba1f0b57 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -664,10 +664,35 @@ export const WithJSONDumpRegistrationBackend = { options: { service: 2, path: 'test', - variables: ['aSingleFile', 'now'], + variables: ['aSingleFile'], + requiredMetadataVariables: ['public_registration_reference'], + additionalMetadataVariables: ['now'] }, }, ], + registrationPluginsVariables: [ + { + pluginIdentifier: 'json_dump', + pluginVerboseName: 'JSON dump registration', + pluginVariables: [ + { + form: null, + formDefinition: null, + name: 'Public registration reference', + key: 'public_registration_reference', + source: '', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: 'main', + dataType: 'string', + dataFormat: '', + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: '', + }, + ], + }, + ], }, play: async ({canvasElement, step}) => { const canvas = within(canvasElement); diff --git a/src/openforms/js/components/admin/forms/VariableSelection.js b/src/openforms/js/components/admin/forms/VariableSelection.js index b4e2bf3959..f778581f42 100644 --- a/src/openforms/js/components/admin/forms/VariableSelection.js +++ b/src/openforms/js/components/admin/forms/VariableSelection.js @@ -37,6 +37,11 @@ const VariableOption = props => { const allowAny = () => true; +// TODO-5012: can we add the registrationPluginsVariables as well? Seems difficult, as there is no +// guarantee that the names of the variables are unique across different registration plugins. +// They would all be added under the static variables label, as they don't have a source (not +// user-defined nor component), which means they might get overwritten. I could change the grouping +// behaviour, but not sure if this is something we should be doing const VariableSelection = ({ id, name, @@ -46,7 +51,7 @@ const VariableSelection = ({ filter = allowAny, ...props }) => { - const {formSteps, formVariables, staticVariables} = useContext(FormContext); + const {formSteps, formVariables, staticVariables, registrationPluginsVariables, registrationBackends} = useContext(FormContext); const intl = useIntl(); let formDefinitionsNames = {}; diff --git a/src/openforms/registrations/contrib/json_dump/config.py b/src/openforms/registrations/contrib/json_dump/config.py index 724ec4e19b..de3d355089 100644 --- a/src/openforms/registrations/contrib/json_dump/config.py +++ b/src/openforms/registrations/contrib/json_dump/config.py @@ -47,6 +47,26 @@ class JSONDumpOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serialize required=True, min_length=1, ) + required_metadata_variables = serializers.ReadOnlyField( + default=[ + "public_reference", + "form_name", + "form_version", + "registration_timestamp", + "auth_type", + ], + label=_("Required metadata variable key list"), + help_text=_( + "A list of required variables to use in the metadata. These include " + "the registration variables of the JSON dump plugin" + ), + ) + additional_metadata_variables = serializers.ListField( + child=FormioVariableKeyField(), + label=_("Additional metadata variable key list"), + help_text=_("A list of additional variables to use in the metadata"), + required=True, + ) class JSONDumpOptions(TypedDict): @@ -60,3 +80,5 @@ class JSONDumpOptions(TypedDict): service: Service path: str variables: list[str] + required_metadata_variables: list[str] + additional_metadata_variables: list[str] diff --git a/src/openforms/registrations/contrib/json_dump/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py index b377d0f6a4..5465efea42 100644 --- a/src/openforms/registrations/contrib/json_dump/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -1,5 +1,6 @@ import base64 import json +from json import JSONDecodeError from typing import cast from django.core.exceptions import SuspiciousOperation @@ -8,6 +9,8 @@ from zgw_consumers.client import build_client +# TODO-5012: include in openforms.formio.service? +from openforms.formio.datastructures import FormioConfigurationWrapper from openforms.formio.service import rewrite_formio_components from openforms.formio.typing import ( FileComponent, @@ -16,15 +19,18 @@ SelectComponent, ) from openforms.forms.json_schema import generate_json_schema +from openforms.forms.models import FormVariable from openforms.submissions.models import Submission, SubmissionFileAttachment from openforms.submissions.service import DataContainer from openforms.typing import JSONObject from openforms.utils.json_schema import to_multiple from openforms.variables.constants import FormVariableSources +from openforms.variables.service import get_static_variables from ...base import BasePlugin # openforms.registrations.base from ...registry import register # openforms.registrations.registry from .config import JSONDumpOptions, JSONDumpOptionsSerializer +from .registration_variables import register as variables_registry @register("json_dump") @@ -45,20 +51,47 @@ def register_submission( **state.get_static_data(), **state.get_data(), # dynamic values from user input } + + # Update config wrapper. This is necessary to update the options for Select, + # SelectBoxes, and Radio components that get their options from another form + # variable. + data = DataContainer(state=state) + configuration_wrapper = rewrite_formio_components( + submission.total_configuration_wrapper, + submission=submission, + data=data.data, + ) + + # Values values = { key: value for key, value in all_values.items() if key in options["variables"] } + values_schema = generate_json_schema(submission.form, options["variables"]) + post_process(values, values_schema, submission, configuration_wrapper) - # Generate schema - schema = generate_json_schema(submission.form, options["variables"]) - - # Post-processing - post_process(values, schema, submission) + # Metadata + metadata = { + key: value + for key, value in all_values.items() + if key in options["metadata_variables"] + } + metadata_schema = generate_json_schema( + submission.form, options["metadata_variables"] + ) + post_process(metadata, metadata_schema, submission, configuration_wrapper) # Send to the service - data = json.dumps({"values": values, "schema": schema}, cls=DjangoJSONEncoder) + data = json.dumps( + { + "values": values, + "values_schema": values_schema, + "metadata": metadata, + "metadata_schema": metadata_schema, + }, + cls=DjangoJSONEncoder + ) service = options["service"] with build_client(service) as client: if ".." in (path := options["path"]): @@ -67,15 +100,26 @@ def register_submission( result = client.post(path, json=data) result.raise_for_status() - return {"api_response": result.json()} + try: + result_json = result.json() + except JSONDecodeError: + result_json = None + + return {"api_response": result_json} def check_config(self) -> None: # Config checks are not really relevant for this plugin right now pass + def get_variables(self) -> list[FormVariable]: + return get_static_variables(variables_registry=variables_registry) + def post_process( - values: JSONObject, schema: JSONObject, submission: Submission + values: JSONObject, + schema: JSONObject, + submission: Submission, + configuration_wrapper: FormioConfigurationWrapper, ) -> None: """Post-process the values and schema. @@ -87,19 +131,10 @@ def post_process( :param values: JSONObject :param schema: JSONObject :param submission: Submission + :param configuration_wrapper: FormioConfigurationWrapper """ state = submission.load_submission_value_variables_state() - # Update config wrapper. This is necessary to update the options for Select, - # SelectBoxes, and Radio components that get their options from another form - # variable. - data = DataContainer(state=state) - configuration_wrapper = rewrite_formio_components( - submission.total_configuration_wrapper, - submission=submission, - data=data.data, - ) - for key in values.keys(): variable = state.variables.get(key) if ( diff --git a/src/openforms/registrations/contrib/json_dump/registration_variables.py b/src/openforms/registrations/contrib/json_dump/registration_variables.py new file mode 100644 index 0000000000..6d1b2afc1b --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/registration_variables.py @@ -0,0 +1,52 @@ + +from django.utils.translation import gettext_lazy as _ + +from openforms.plugins.registry import BaseRegistry +from openforms.submissions.models import Submission +from openforms.variables.base import BaseStaticVariable +from openforms.variables.constants import FormVariableDataTypes +from openforms.variables.static_variables.static_variables import Now + + +class Registry(BaseRegistry[BaseStaticVariable]): + """ + A registry for the JSON Dump registration variables. + """ + + module = "json_dump" + + +register = Registry() +"""The JSON Dump registration variables registry.""" + + +@register("public_reference") +class PublicReference(BaseStaticVariable): + name = _("Public reference") + data_type = FormVariableDataTypes.string + + def get_initial_value(self, submission: Submission | None = None) -> str: + if submission is None: + return "" + return submission.public_registration_reference + + +@register("form_version") +class FormVersion(BaseStaticVariable): + name = _("Form version") + data_type = FormVariableDataTypes.string + + def get_initial_value(self, submission: Submission | None = None) -> str: + if submission is None: + return "" + + form_version = submission.form.formversion_set.order_by("created").last() + return form_version.description if form_version else "" + + def as_json_schema(self): + return {"title": "Form version", "type": "string"} + + +@register("registration_timestamp") +class RegistrationTimestamp(Now): + name = _("Registration timestamp") diff --git a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py index 03809cbfdc..7c8898bc82 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -8,6 +8,7 @@ from requests import RequestException from zgw_consumers.test.factories import ServiceFactory +from openforms.forms.tests.factories import FormVersionFactory from openforms.submissions.tests.factories import ( FormVariableFactory, SubmissionFactory, @@ -22,7 +23,7 @@ VCR_TEST_FILES = Path(__file__).parent / "files" -class JSONDumpBackendTests(OFVCRMixin, TestCase): +class JSONDumpBackendTests(TestCase): VCR_TEST_FILES = VCR_TEST_FILES @classmethod @@ -69,6 +70,7 @@ def test_submission_happy_flow(self): "service": self.service, "path": "json_plugin", "variables": ["firstName", "file", "auth_bsn"], + "metadata_variables": [], } json_plugin = JSONDumpRegistration("json_registration_plugin") @@ -724,3 +726,37 @@ def test_nested_component_key(self): result["api_response"]["data"]["schema"]["properties"]["foo.bar"]["type"], "string", ) + + def test_metadata(self): + submission = SubmissionFactory.from_components( + [ + {"key": "firstName", "type": "textfield"}, + {"key": "lastName", "type": "textfield"}, + ], + completed=True, + submitted_data={ + "firstName": "We Are", + "lastName": "Checking", + }, + bsn="123456789", + with_public_registration_reference=True, + ) + FormVersionFactory.create(form=submission.form, description="Version 1.0") + + json_plugin = JSONDumpRegistration("json_registration_plugin") + options: JSONDumpOptions = { + "service": self.service, + "path": "json_plugin", + "variables": [], + "metadata_variables": [ + "form_name", + "form_version", + "now", + "public_reference", + "auth_type", + ], + } + result = json_plugin.register_submission(submission, options) + + from pprint import pprint + pprint(result["api_response"]["data"]) diff --git a/src/openforms/variables/tests/test_static_variables.py b/src/openforms/variables/tests/test_static_variables.py index 05210280ca..b8f87533cd 100644 --- a/src/openforms/variables/tests/test_static_variables.py +++ b/src/openforms/variables/tests/test_static_variables.py @@ -6,6 +6,7 @@ from freezegun import freeze_time from jsonschema import Draft202012Validator +from openforms.forms.tests.factories import FormVersionFactory from openforms.submissions.tests.factories import SubmissionFactory from ..registry import register_static_variable as register @@ -93,6 +94,11 @@ def test_without_submission(self): self.assertEqual(variable.initial_value, "") + with self.subTest("form version"): + variable = _get_variable("form_version") + + self.assertEqual(variable.initial_value, "") + def test_with_submission(self): submission = SubmissionFactory.create( form__name="Public form name", @@ -112,6 +118,16 @@ def test_with_submission(self): variable.initial_value, "f5ea7397-65c3-4ce0-b955-9b8f408e0ae0" ) + with self.subTest("form version"): + FormVersionFactory.create(form=submission.form, description="Version 1") + FormVersionFactory.create(form=submission.form, description="Version 2") + + variable = _get_variable("form_version", submission=submission) + + self.assertEqual( + variable.initial_value, "Version 2" + ) + class TodayTests(TestCase): @freeze_time("2022-11-24T00:30:00+01:00") @@ -123,6 +139,19 @@ def test_date_has_the_right_day(self): self.assertEqual(variable.initial_value.day, 24) +class PublicReferenceTests(TestCase): + def test_without_submission(self): + variable = _get_variable("public_reference") + self.assertEqual(variable.initial_value, "") + + def test_with_submission(self): + submission = SubmissionFactory.create(public_registration_reference="OF-ABC123") + + variable = _get_variable("public_reference", submission=submission) + + self.assertEqual(variable.initial_value, "OF-ABC123") + + class StaticVariablesValidJsonSchemaTests(TestCase): validator = Draft202012Validator