From 8d34790aa08280b83e0705f0309aa30797067555 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Tue, 17 Dec 2024 12:09:15 +0100 Subject: [PATCH 01/22] :construction: [#4908] WIP --- src/openforms/conf/base.py | 1 + .../form_design/RegistrationFields.stories.js | 95 +++++++++++++++++++ .../admin/form_design/registrations/index.js | 2 + .../registrations/json/JSONOptionsForm.js | 91 ++++++++++++++++++ .../json/fields/FormVariablesSelect.js | 64 +++++++++++++ .../json/fields/RelativeAPIEndpoint.js | 35 +++++++ .../json/fields/ServiceSelect.js | 46 +++++++++ .../form_design/registrations/json/index.js | 3 + .../registrations/contrib/json/__init__.py | 0 .../registrations/contrib/json/apps.py | 12 +++ .../registrations/contrib/json/config.py | 30 ++++++ .../registrations/contrib/json/plugin.py | 18 ++++ 12 files changed, 397 insertions(+) create mode 100644 src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json/fields/FormVariablesSelect.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json/index.js create mode 100644 src/openforms/registrations/contrib/json/__init__.py create mode 100644 src/openforms/registrations/contrib/json/apps.py create mode 100644 src/openforms/registrations/contrib/json/config.py create mode 100644 src/openforms/registrations/contrib/json/plugin.py diff --git a/src/openforms/conf/base.py b/src/openforms/conf/base.py index 48feebee0f..f3a227b0ba 100644 --- a/src/openforms/conf/base.py +++ b/src/openforms/conf/base.py @@ -225,6 +225,7 @@ "openforms.registrations.contrib.objects_api", "openforms.registrations.contrib.microsoft_graph.apps.MicrosoftGraphApp", "openforms.registrations.contrib.camunda.apps.CamundaApp", + "openforms.registrations.contrib.json", "openforms.prefill", "openforms.prefill.contrib.demo.apps.DemoApp", "openforms.prefill.contrib.kvk.apps.KVKPrefillApp", 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 a6924eed48..af295481b6 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -396,6 +396,33 @@ export default { type: 'object', }, }, + { + id: 'json', + label: 'JSON registration', + schema: { + type: 'object', + properties: { + service: { + enum: [1, 2], + enumNames: ['Service 1', 'Service 2'], + }, + relativeApiEndpoint: { + minLength: 1, + title: 'Relative API endpoint', + type: 'string', + }, + formVariables: { + type: 'array', + title: 'List of form variables', + items: { + type: 'string', + title: 'form variable', + minLength: 1, + }, + }, + }, + }, + }, ], configuredBackends: [], onChange: fn(), @@ -736,6 +763,16 @@ export const ConfiguredBackends = { ], }, }, + { + key: 'backend11', + name: 'JSON', + backend: 'json', + options: { + service: 1, + relativeApiEndpoint: 'Example endpoint', + formVariables: [], + }, + }, ], validationErrors: [ ['form.registrationBackends.1.options.zgwApiGroup', 'You sure about this?'], @@ -981,3 +1018,61 @@ export const STUFZDS = { await userEvent.click(canvas.getByRole('button', {name: 'Opties instellen'})); }, }; + + +export const JSON = { + args: { + configuredBackends: [ + { + key: 'backend11', + name: 'JSON', + backend: 'json', + options: { + service: 1, + relativeApiEndpoint: 'We are checking.', + formVariables: [], + }, + }, + ], + availableFormVariables: [ + { + dataType: 'string', + form: null, + formDefinition: null, + key: 'firstName', + name: 'First name', + source: 'user_defined', + }, + { + dataType: 'string', + form: null, + formDefinition: null, + key: 'lastName', + name: 'Last name', + source: 'user_defined', + }, + { + dataType: 'file', + form: null, + formDefinition: null, + key: 'attachment', + name: 'Attachment', + source: 'user_defined', + }, + ], + availableStaticVariables: [ + { + form: null, + formDefinition: null, + name: 'BSN', + key: 'auth_bsn', + }, + ], + }, + + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + await userEvent.click(canvas.getByRole('button', {name: 'Opties instellen'})); + }, +}; diff --git a/src/openforms/js/components/admin/form_design/registrations/index.js b/src/openforms/js/components/admin/form_design/registrations/index.js index c5de3f8053..4c549674ee 100644 --- a/src/openforms/js/components/admin/form_design/registrations/index.js +++ b/src/openforms/js/components/admin/form_design/registrations/index.js @@ -1,6 +1,7 @@ import CamundaOptionsForm from './camunda'; import DemoOptionsForm from './demo'; import EmailOptionsForm from './email'; +import JSONOptionsForm from './json'; import MSGraphOptionsForm from './ms_graph'; import ObjectsApiOptionsForm from './objectsapi/ObjectsApiOptionsForm'; import ObjectsApiSummaryHandler from './objectsapi/ObjectsApiSummaryHandler'; @@ -46,6 +47,7 @@ export const BACKEND_OPTIONS_FORMS = { form: StufZDSOptionsForm, }, 'microsoft-graph': {form: MSGraphOptionsForm}, + 'json': {form: JSONOptionsForm}, // demo plugins demo: {form: DemoOptionsForm}, 'failing-demo': {form: DemoOptionsForm}, diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js new file mode 100644 index 0000000000..6001a1cacd --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import React, {useContext} from 'react'; +import {FormattedMessage} from 'react-intl'; + +import {FormContext} from 'components/admin/form_design/Context'; +import Fieldset from 'components/admin/forms/Fieldset'; +import ModalOptionsConfiguration from 'components/admin/forms/ModalOptionsConfiguration'; +import { + ValidationErrorContext, + ValidationErrorsProvider, + filterErrors, +} from 'components/admin/forms/ValidationErrors'; +import {getChoicesFromSchema} from 'utils/json-schema'; + +// TODO-4098: maybe create separate file (JSONOptionsFormFields) for all the fields? +// Though, no need to use multiple FieldSets, so adding the fields to the form is pretty +// straightforward. +import FormVariablesSelect from './fields/FormVariablesSelect'; +import RelativeAPIEndpoint from './fields/RelativeAPIEndpoint'; +import ServiceSelect from './fields/ServiceSelect'; + + +const JSONOptionsForm = ({name, label, schema, formData, onChange}) => { + const validationErrors = useContext(ValidationErrorContext); + const relevantErrors = filterErrors(name, validationErrors); + + // Get form variables and create form variable options + const formContext = useContext(FormContext) + const formVariables = formContext.formVariables ?? []; + const staticVariables = formContext.staticVariables ?? []; + const allFormVariables = staticVariables.concat(formVariables); + + const formVariableOptions = []; + for (const formVariable of allFormVariables) { + formVariableOptions.push({value: formVariable.key, label: formVariable.name}); + } + + // Create service options + const {service} = schema.properties; + const serviceOptions = getChoicesFromSchema( + service.enum, service.enumNames + ).map(([value, label]) => ({value, label})); + + return ( + + } + initialFormData={{...formData}} + onSubmit={values => onChange({formData: values})} + modalSize="medium" + > + +
+ + + +
+
+
+ ); +}; + +JSONOptionsForm.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.node.isRequired, + schema: PropTypes.shape({ + properties: PropTypes.shape({ + service: PropTypes.shape({ + enum: PropTypes.arrayOf(PropTypes.number).isRequired, + enumNames: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired, + }).isRequired, + }).isRequired, + formData: PropTypes.shape({ + service: PropTypes.number, + relativeApiEndpoint: PropTypes.string, + // TODO-4098: might need to rename this to selectedFormVariables to avoid confusion or even + // naming conflicts + formVariables: PropTypes.arrayOf(PropTypes.string), + }), + onChange: PropTypes.func.isRequired, +}; + +export default JSONOptionsForm; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/fields/FormVariablesSelect.js b/src/openforms/js/components/admin/form_design/registrations/json/fields/FormVariablesSelect.js new file mode 100644 index 0000000000..2d49b7bb0a --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json/fields/FormVariablesSelect.js @@ -0,0 +1,64 @@ +import {useField} from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import ReactSelect from 'components/admin/forms/ReactSelect'; + +const FormVariablesSelect = ({options}) => { + const [fieldProps, , {setValue}] = useField('formVariables'); + + const values = []; + if (fieldProps.value && fieldProps.value.length) { + fieldProps.value.forEach(item => { + const selectedOption = options.find(option => option.value === item); + if (selectedOption) { + values.push(selectedOption); + } + }); + } + + return ( + + + } + helpText={ + + } + > + { + setValue(newValue.map(item => item.value)); + }} + /> + + + ); +}; + +FormVariablesSelect.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.node.isRequired, + }) + ).isRequired, +}; + +export default FormVariablesSelect; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js b/src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js new file mode 100644 index 0000000000..a4febbc941 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js @@ -0,0 +1,35 @@ +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 {TextInput} from 'components/admin/forms/Inputs'; + + +const RelativeAPIEndpoint = () => { + const [fieldProps] = useField('relativeApiEndpoint'); + return ( + + + } + helpText={ + + } + > + + + + ); +}; + +export default RelativeAPIEndpoint; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js b/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js new file mode 100644 index 0000000000..2afc14d9ba --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import ReactSelect from 'components/admin/forms/ReactSelect'; + +const ServiceSelect = ({options}) => { + return ( + + + } + helpText={ + + } + > + + + + ); +}; + +ServiceSelect.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.node.isRequired, + }) + ).isRequired, +}; + +export default ServiceSelect; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/index.js b/src/openforms/js/components/admin/form_design/registrations/json/index.js new file mode 100644 index 0000000000..97a74031a6 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json/index.js @@ -0,0 +1,3 @@ +import JSONOptionsForm from './JSONOptionsForm'; + +export default JSONOptionsForm; diff --git a/src/openforms/registrations/contrib/json/__init__.py b/src/openforms/registrations/contrib/json/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/registrations/contrib/json/apps.py b/src/openforms/registrations/contrib/json/apps.py new file mode 100644 index 0000000000..0553f43210 --- /dev/null +++ b/src/openforms/registrations/contrib/json/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +# TODO-4908: maybe rename to FVaJ (Form Variables as JSON) +class JSONAppConfig(AppConfig): + name = "openforms.registrations.contrib.json" + label = "registrations_json" + verbose_name = _("JSON plugin") + + def ready(self): + from . import plugin # noqa diff --git a/src/openforms/registrations/contrib/json/config.py b/src/openforms/registrations/contrib/json/config.py new file mode 100644 index 0000000000..d71d7964da --- /dev/null +++ b/src/openforms/registrations/contrib/json/config.py @@ -0,0 +1,30 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers +from zgw_consumers.models import Service + +from openforms.api.fields import PrimaryKeyRelatedAsChoicesField +from openforms.formio.api.fields import FormioVariableKeyField +from openforms.utils.mixins import JsonSchemaSerializerMixin + + +class JSONOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): + service = PrimaryKeyRelatedAsChoicesField( + queryset=Service.objects.all(), + label=_("Service"), + help_text=_("Which service to use."), + ) + # TODO-4098: show the complete API endpoint as a (tooltip) hint after user entry? Might be a front-end thing... + relative_api_endpoint = serializers.CharField( + max_length=255, + label=_("Relative API endpoint"), + help_text=_("The API endpoint to send the data to (relative to the service API root)."), + allow_blank=True, + ) + form_variables = serializers.ListField( + child=FormioVariableKeyField(max_length=50), + label=_("Form variable key list"), + help_text=_( + "A comma-separated list of form variables (can also include static variables) to use." + ) + ) diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py new file mode 100644 index 0000000000..2474fe1a01 --- /dev/null +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -0,0 +1,18 @@ +from django.utils.translation import gettext_lazy as _ + +from openforms.submissions.models import Submission +from ...base import BasePlugin, OptionsT # openforms.registrations.base +from ...registry import register # openforms.registrations.registry +from .config import JSONOptionsSerializer + + +@register("json") +class JSONPlugin(BasePlugin): + verbose_name = _("JSON registration") + configuration_options = JSONOptionsSerializer + + def register_submission(self, submission: Submission, options: OptionsT) -> None: + print(options) + + def check_config(self): + pass From b44aca4cf3b069381952a02c3c59bbaee14934ee Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Wed, 18 Dec 2024 16:38:47 +0100 Subject: [PATCH 02/22] :sparkles: [#4908] Add processing of form variable options to JSON registration plugin Need to include all form variables listed in the options to a values dictionary --- .../registrations/contrib/json/plugin.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py index 2474fe1a01..14a6701095 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -1,18 +1,37 @@ from django.utils.translation import gettext_lazy as _ from openforms.submissions.models import Submission +from openforms.variables.service import get_static_variables + from ...base import BasePlugin, OptionsT # openforms.registrations.base from ...registry import register # openforms.registrations.registry from .config import JSONOptionsSerializer @register("json") -class JSONPlugin(BasePlugin): +class JSONRegistration(BasePlugin): verbose_name = _("JSON registration") configuration_options = JSONOptionsSerializer def register_submission(self, submission: Submission, options: OptionsT) -> None: - print(options) + static_variables = get_static_variables(submission=submission) + static_variables_dict = { + variable.key: variable.initial_value for variable in static_variables + } + + # Update values dict with relevant form data + all_variables = {**submission.data, **static_variables_dict} + values.update( + { + form_variable: all_variables[form_variable] + for form_variable in options["form_variables"] + } + ) + + print(values) + + # TODO-4908: send `values` to some service + data_to_be_sent = {"values": values} def check_config(self): pass From 91d63a9165ef5b9f78e0667cf6ee33894a7f2d55 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Wed, 18 Dec 2024 16:50:39 +0100 Subject: [PATCH 03/22] :white_check_mark: [#4908] Add tests --- .../contrib/json/tests/__init__.py | 0 .../contrib/json/tests/test_backend.py | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/openforms/registrations/contrib/json/tests/__init__.py create mode 100644 src/openforms/registrations/contrib/json/tests/test_backend.py diff --git a/src/openforms/registrations/contrib/json/tests/__init__.py b/src/openforms/registrations/contrib/json/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/registrations/contrib/json/tests/test_backend.py b/src/openforms/registrations/contrib/json/tests/test_backend.py new file mode 100644 index 0000000000..b21eb25b9f --- /dev/null +++ b/src/openforms/registrations/contrib/json/tests/test_backend.py @@ -0,0 +1,68 @@ +from django.test import TestCase + +from openforms.appointments.contrib.qmatic.tests.factories import ServiceFactory +from openforms.submissions.public_references import set_submission_reference +from openforms.submissions.tests.factories import ( + SubmissionFactory, + SubmissionFileAttachmentFactory, +) + +from ..plugin import JSONRegistration + + +class JSONBackendTests(TestCase): + # VCR_TEST_FILES = VCR_TEST_FILES + + def test_submission_with_json_backend(self): + submission = SubmissionFactory.from_components( + [ + {"key": "firstName", "type": "textField"}, + {"key": "lastName", "type": "textfield"}, + {"key": "file", "type": "file"}, + ], + completed=True, + submitted_data={ + "firstName": "We Are", + "lastName": "Checking", + "file": [ + { + "url": "some://url", + "name": "my-foo.bin", + "type": "application/foo", + "originalName": "my-foo.bin", + } + ], + }, + bsn="123456789", + ) + + submission_file_attachment = SubmissionFileAttachmentFactory.create( + form_key="file", + submission_step=submission.submissionstep_set.get(), + file_name="test_file.txt", + content_type="application/text", + content__data=b"This is example content.", + _component_configuration_path="components.2", + _component_data_path="file", + ) + + json_form_options = dict( + service=ServiceFactory(api_root="http://example.com/api/v2"), + relative_api_endpoint="", + form_variables=["firstName", "lastName", "file", "auth_bsn"], + ) + email_submission = JSONRegistration("json_plugin") + + set_submission_reference(submission) + + data_to_be_sent = email_submission.register_submission(submission, json_form_options) + + expected_data_to_be_sent = { + "values": { + "firstName": "We Are", + "lastName": "Checking", + "file": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", + "auth_bsn": "123456789", + } + } + self.assertEqual(data_to_be_sent, expected_data_to_be_sent) From 8b00df7b0edeed9652e4ce0eab91c230b7daab06 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 19 Dec 2024 11:10:11 +0100 Subject: [PATCH 04/22] :sparkles: [#4908] Add processing of attachments in plugin The content of the attachment is now added to the result --- .../registrations/contrib/json/plugin.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py index 14a6701095..a66c59b489 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -1,10 +1,15 @@ +import base64 + from django.utils.translation import gettext_lazy as _ +from zgw_consumers.client import build_client + from openforms.submissions.models import Submission from openforms.variables.service import get_static_variables from ...base import BasePlugin, OptionsT # openforms.registrations.base from ...registry import register # openforms.registrations.registry +from ...utils import execute_unless_result_exists from .config import JSONOptionsSerializer @@ -14,6 +19,25 @@ class JSONRegistration(BasePlugin): configuration_options = JSONOptionsSerializer def register_submission(self, submission: Submission, options: OptionsT) -> None: + # TODO-4908: the email plugin works with a EmailConfig singleton model. Is that useful here? + # TODO-4908: add typing for options dict + + # TODO-4908: any other form field types that need 'special attention'? + + values = {} + # Encode (base64) and add attachments to values dict if their form keys were specified in the + # form variables list + for attachment in submission.attachments: + if not attachment.form_key in options["form_variables"]: + continue + options["form_variables"].remove(attachment.form_key) + with attachment.content.open("rb") as f: + f.seek(0) + values[attachment.form_key] = base64.b64encode(f.read()).decode() + + # TODO-4908: what should the behaviour be when a form + # variable is not in the data or static variables? + # Create static variables dict static_variables = get_static_variables(submission=submission) static_variables_dict = { variable.key: variable.initial_value for variable in static_variables @@ -30,8 +54,10 @@ def register_submission(self, submission: Submission, options: OptionsT) -> None print(values) - # TODO-4908: send `values` to some service - data_to_be_sent = {"values": values} + # TODO-4908: send `values` to the service + # TODO-4908: added return for testing purposes + return {"values": values} + # TODO-4098: what to do in here? def check_config(self): pass From 5370c8efd3444b659100a120b043e2dabc1274b5 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 19 Dec 2024 15:02:35 +0100 Subject: [PATCH 05/22] :sparkles: [#4908] Implement sending result to specified endpoint --- .../registrations/contrib/json/plugin.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py index a66c59b489..2bcdfb816a 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -54,10 +54,20 @@ def register_submission(self, submission: Submission, options: OptionsT) -> None print(values) - # TODO-4908: send `values` to the service - # TODO-4908: added return for testing purposes - return {"values": values} + # Send to the service + json = {"values": values} + service = options["service"] + submission.registration_result = result = {} + with build_client(service) as client: + result["api_response"] = res = client.post( + options.get("relative_api_endpoint", ""), + json=json, + headers={"Content-Type": "application/json"}, + ) + res.raise_for_status() - # TODO-4098: what to do in here? - def check_config(self): + return result + + def check_config(self) -> None: + # Config checks are not really relevant for this plugin right now pass From 4029b698a6e42ff3d1dd3d624df837d12fa7eac3 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Fri, 20 Dec 2024 10:38:04 +0100 Subject: [PATCH 06/22] :sparkles: [#4908] Add docker compose flask app Required for simulating a receiving service --- docker/docker-compose.json-registration.yml | 15 +++++++++++++++ docker/json-registration/Dockerfile | 17 +++++++++++++++++ docker/json-registration/README.md | 18 ++++++++++++++++++ docker/json-registration/app.py | 20 ++++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 docker/docker-compose.json-registration.yml create mode 100644 docker/json-registration/Dockerfile create mode 100644 docker/json-registration/README.md create mode 100644 docker/json-registration/app.py diff --git a/docker/docker-compose.json-registration.yml b/docker/docker-compose.json-registration.yml new file mode 100644 index 0000000000..9cd4030faf --- /dev/null +++ b/docker/docker-compose.json-registration.yml @@ -0,0 +1,15 @@ +version: '3.8' + +name: json-registration + +services: + flask_app: + build: ./json-registration + ports: + - "80:80" + volumes: + - ./json-registration/:/app/ + +networks: + open-forms-dev: + name: open-forms-dev diff --git a/docker/json-registration/Dockerfile b/docker/json-registration/Dockerfile new file mode 100644 index 0000000000..56d713f353 --- /dev/null +++ b/docker/json-registration/Dockerfile @@ -0,0 +1,17 @@ +# Use the official Python image from the Docker Hub +FROM python:3.12-slim + +# Set the working directory +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY . /app + +# Install the dependencies +RUN pip install Flask + +# Make port 80 available to the world outside this container +EXPOSE 80 + +# Run app.py when the container launches +CMD ["python", "app.py"] diff --git a/docker/json-registration/README.md b/docker/json-registration/README.md new file mode 100644 index 0000000000..675b25f92e --- /dev/null +++ b/docker/json-registration/README.md @@ -0,0 +1,18 @@ +# JSON registration plugin + +The `docker-compose.json-registration.yml` compose file is available to run a mock service intended +to simulate a receiving server to test the JSON registration backend plugin. It contains an endpoint for +sending json data (`json_plugin`) and testing the connection (`test_connection`). + +The `json_plugin` endpoint returns a confirmation message that the data was received, together with the +received data. The `test_connection` endpoint just returns an 'OK' message. + +## docker compose + +Start an instance in your local environment from the parent directory: + +```bash +docker compose -f docker-compose.json-registration.yml up -d +``` + +This starts a flask application at http://localhost:80/ with the endpoints `json_plugin` and `test_connection`. diff --git a/docker/json-registration/app.py b/docker/json-registration/app.py new file mode 100644 index 0000000000..e2e8b08bcb --- /dev/null +++ b/docker/json-registration/app.py @@ -0,0 +1,20 @@ +from flask import Flask, jsonify, request + + +app = Flask(__name__) + +@app.route("/json_plugin", methods=["POST"]) +def json_plugin_post(): + data = request.get_json() + + message = "No data" if data is None else "Data received" + return jsonify({"message": message, "data": data}), 201 + + +@app.route("/test_connection", methods=["GET"]) +def test_connection(): + return jsonify({"message": "OK"}), 200 + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=80, debug=True) From 75bcc5dbcd04be702db85b23dc165bb35dcf9c11 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Fri, 20 Dec 2024 11:13:46 +0100 Subject: [PATCH 07/22] :sparkles: [#4908] Add typing hints for json registration options --- src/openforms/registrations/contrib/json/config.py | 14 ++++++++++++++ src/openforms/registrations/contrib/json/plugin.py | 7 +++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/openforms/registrations/contrib/json/config.py b/src/openforms/registrations/contrib/json/config.py index d71d7964da..091cc529e0 100644 --- a/src/openforms/registrations/contrib/json/config.py +++ b/src/openforms/registrations/contrib/json/config.py @@ -1,3 +1,5 @@ +from typing import Required, TypedDict + from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -28,3 +30,15 @@ class JSONOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): "A comma-separated list of form variables (can also include static variables) to use." ) ) + + +class JSONOptions(TypedDict): + """ + JSON registration plugin options + + This describes the shape of :attr:`JSONOptionsSerializer.validated_data`, after + the input data has been cleaned/validated. + """ + service: Required[Service] + relative_api_endpoint: str + form_variables: Required[list[str]] diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py index 2bcdfb816a..51a1f53c43 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -7,10 +7,10 @@ from openforms.submissions.models import Submission from openforms.variables.service import get_static_variables -from ...base import BasePlugin, OptionsT # openforms.registrations.base +from ...base import BasePlugin # openforms.registrations.base from ...registry import register # openforms.registrations.registry from ...utils import execute_unless_result_exists -from .config import JSONOptionsSerializer +from .config import JSONOptions, JSONOptionsSerializer @register("json") @@ -18,9 +18,8 @@ class JSONRegistration(BasePlugin): verbose_name = _("JSON registration") configuration_options = JSONOptionsSerializer - def register_submission(self, submission: Submission, options: OptionsT) -> None: + def register_submission(self, submission: Submission, options: JSONOptions) -> dict: # TODO-4908: the email plugin works with a EmailConfig singleton model. Is that useful here? - # TODO-4908: add typing for options dict # TODO-4908: any other form field types that need 'special attention'? From cfa43a00963b211f2c3a8f3540ec7123343a893d Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Fri, 20 Dec 2024 12:23:03 +0100 Subject: [PATCH 08/22] :sparkles: [#4908] Update JSON options serializer * Add required properties * Filter service queryset on ORC, as this is the only relevant type here * Add minimum list length for the form variables: the configuration options could be saved without specifying any form variables to include. This does not make much sense. Added a minimum length of 1 for the form variables list in the serializer --- .../registrations/json/JSONOptionsForm.js | 4 ++-- src/openforms/registrations/contrib/json/config.py | 14 +++++++++----- src/openforms/registrations/contrib/json/plugin.py | 5 ----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js index 6001a1cacd..9bb72a1974 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js @@ -12,7 +12,7 @@ import { } from 'components/admin/forms/ValidationErrors'; import {getChoicesFromSchema} from 'utils/json-schema'; -// TODO-4098: maybe create separate file (JSONOptionsFormFields) for all the fields? +// TODO-4908: maybe create separate file (JSONOptionsFormFields) for all the fields? // Though, no need to use multiple FieldSets, so adding the fields to the form is pretty // straightforward. import FormVariablesSelect from './fields/FormVariablesSelect'; @@ -81,7 +81,7 @@ JSONOptionsForm.propTypes = { formData: PropTypes.shape({ service: PropTypes.number, relativeApiEndpoint: PropTypes.string, - // TODO-4098: might need to rename this to selectedFormVariables to avoid confusion or even + // TODO-4908: might need to rename this to selectedFormVariables to avoid confusion or even // naming conflicts formVariables: PropTypes.arrayOf(PropTypes.string), }), diff --git a/src/openforms/registrations/contrib/json/config.py b/src/openforms/registrations/contrib/json/config.py index 091cc529e0..f7f2a4d8fb 100644 --- a/src/openforms/registrations/contrib/json/config.py +++ b/src/openforms/registrations/contrib/json/config.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from zgw_consumers.constants import APITypes from zgw_consumers.models import Service from openforms.api.fields import PrimaryKeyRelatedAsChoicesField @@ -12,23 +13,26 @@ class JSONOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): service = PrimaryKeyRelatedAsChoicesField( - queryset=Service.objects.all(), + queryset=Service.objects.filter(api_type=APITypes.orc), label=_("Service"), help_text=_("Which service to use."), + required=True, ) - # TODO-4098: show the complete API endpoint as a (tooltip) hint after user entry? Might be a front-end thing... + # TODO-4908: show the complete API endpoint as a (tooltip) hint after user entry? + # Might be a front-end thing... relative_api_endpoint = serializers.CharField( max_length=255, label=_("Relative API endpoint"), help_text=_("The API endpoint to send the data to (relative to the service API root)."), allow_blank=True, + required=False, ) form_variables = serializers.ListField( child=FormioVariableKeyField(max_length=50), label=_("Form variable key list"), - help_text=_( - "A comma-separated list of form variables (can also include static variables) to use." - ) + help_text=_("A list of form variables (can also include static variables) to use."), + required=True, + min_length=1, ) diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py index 51a1f53c43..ee6f26276c 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -9,7 +9,6 @@ from ...base import BasePlugin # openforms.registrations.base from ...registry import register # openforms.registrations.registry -from ...utils import execute_unless_result_exists from .config import JSONOptions, JSONOptionsSerializer @@ -19,10 +18,6 @@ class JSONRegistration(BasePlugin): configuration_options = JSONOptionsSerializer def register_submission(self, submission: Submission, options: JSONOptions) -> dict: - # TODO-4908: the email plugin works with a EmailConfig singleton model. Is that useful here? - - # TODO-4908: any other form field types that need 'special attention'? - values = {} # Encode (base64) and add attachments to values dict if their form keys were specified in the # form variables list From d455de40dcce639971738b75af3724b8e5d9a25e Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Mon, 23 Dec 2024 17:02:57 +0100 Subject: [PATCH 09/22] :sparkles: [#4908] Add checkbox to include variables from the variable table --- .../admin/form_design/registrations/index.js | 11 ++- .../registrations/json/JSONSummaryHandler.js | 32 +++++++++ .../json/JSONVariableConfigurationEditor.js | 68 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js diff --git a/src/openforms/js/components/admin/form_design/registrations/index.js b/src/openforms/js/components/admin/form_design/registrations/index.js index 4c549674ee..9a973dc8f7 100644 --- a/src/openforms/js/components/admin/form_design/registrations/index.js +++ b/src/openforms/js/components/admin/form_design/registrations/index.js @@ -1,7 +1,9 @@ import CamundaOptionsForm from './camunda'; import DemoOptionsForm from './demo'; import EmailOptionsForm from './email'; -import JSONOptionsForm from './json'; +import JSONOptionsForm from './json/JSONOptionsForm'; +import JSONSummaryHandler from './json/JSONSummaryHandler'; +import JSONVariableConfigurationEditor from './json/JSONVariableConfigurationEditor'; import MSGraphOptionsForm from './ms_graph'; import ObjectsApiOptionsForm from './objectsapi/ObjectsApiOptionsForm'; import ObjectsApiSummaryHandler from './objectsapi/ObjectsApiSummaryHandler'; @@ -47,7 +49,12 @@ export const BACKEND_OPTIONS_FORMS = { form: StufZDSOptionsForm, }, 'microsoft-graph': {form: MSGraphOptionsForm}, - 'json': {form: JSONOptionsForm}, + 'json': { + form: JSONOptionsForm, + configurableFromVariables: true, + summaryHandler: JSONSummaryHandler, + variableConfigurationEditor: JSONVariableConfigurationEditor, + }, // demo plugins demo: {form: DemoOptionsForm}, 'failing-demo': {form: DemoOptionsForm}, diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js new file mode 100644 index 0000000000..28878d65ac --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js @@ -0,0 +1,32 @@ +import {FormattedMessage} from 'react-intl'; +import React from 'react'; + +const JSONSummaryHandler = ({variable, backendOptions}) => { + + const isIncluded = backendOptions.formVariables.includes(variable.key); + + if (isIncluded) { + return ( + + ); + } + else { + return ( + + ); + } +}; + + +// TODO-4098: ?? +JSONSummaryHandler.propTypes = { + +}; + +export default JSONSummaryHandler; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js new file mode 100644 index 0000000000..fe423577f9 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js @@ -0,0 +1,68 @@ +// TODO-4908: fix imports +import {Checkbox} from 'components/admin/forms/Inputs'; +import Field from '../../../forms/Field'; +import {FormattedMessage} from 'react-intl'; +import FormRow from '../../../forms/FormRow'; +import React from 'react'; +import {useField, useFormikContext} from 'formik'; +import PropTypes from 'prop-types'; + + +const JSONVariableConfigurationEditor = ({variable}) => { + const [fieldProps, , {setValue}] = useField('formVariables'); + + const formVariables = fieldProps.value + const isIncluded = formVariables.includes(variable.key); + + return ( + + + + } + helpText={ + + } + checked={isIncluded} + onChange={event => { + const formVariablesNew = formVariables.slice(); + const index = formVariablesNew.indexOf(variable.key); + if (event.target.checked) { + // TODO-4908: remove this when testing is implemented + if (index !== -1) {throw new Error( + "This form variable is already on the list of " + + "form variables to include. This shouldn't happen" + );} + formVariablesNew.push(variable.key); + } else { + if (index === -1) {throw new Error( + "This form variable is not yet on the list of " + + "form variables to include. This shouldn't happen." + );} + formVariablesNew.splice(index, 1); + } + setValue(formVariablesNew); + }} + /> + + + ) +} + +// TODO-4098: ??? +JSONVariableConfigurationEditor.propTypes = { + variable: PropTypes.shape({ + key: PropTypes.string.isRequired, + }).isRequired, +}; + + +export default JSONVariableConfigurationEditor From 44783d2070edf082798c964a8e96a9a3315ef10e Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Mon, 23 Dec 2024 17:04:29 +0100 Subject: [PATCH 10/22] :white_check_mark: [#4908] Add JSON registration plugin story to variables editor --- .../variables/VariablesEditor.stories.js | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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 94498595cb..6b63c5f441 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 @@ -654,6 +654,63 @@ export const WithObjectsAPIAndTestRegistrationBackends = { }, }; +export const WithJSONRegistrationBackend = { + args: { + registrationBackends: [ + { + backend: 'json', + key: 'test_json_backend', + name: 'JSON registration', + options: { + service: 2, + relativeApiEndpoint: 'test', + formVariables: ['aSingleFile', 'now'], + }, + }, + ], + }, + play: async ({canvasElement, step}) => { + // TODO-4098: can I get the formVariables backendOptions here? + const canvas = within(canvasElement); + + const editIcons = canvas.getAllByTitle('Registratie-instellingen bewerken'); + await expect(editIcons).toHaveLength(3); + + await step('formioComponent checkbox unchecked', async () => { + await userEvent.click(editIcons[0]); + + const checkbox = await canvas.findByRole('checkbox'); + await expect(checkbox).not.toBeChecked(); + + const saveButton = canvas.getByRole('button', {name: 'Opslaan'}); + await userEvent.click(saveButton); + }) + + await step('aSingleFile checkbox checked', async () => { + await userEvent.click(editIcons[1]); + + const checkbox = await canvas.findByRole('checkbox'); + await expect(checkbox).toBeChecked(); + + const saveButton = canvas.getByRole('button', {name: 'Opslaan'}); + await userEvent.click(saveButton); + }) + + await step('now checkbox checked', async () => { + const staticVariables = canvas.getByRole('tab', {name: 'Vaste variabelen'}); + await userEvent.click(staticVariables); + + const editIcon = canvas.getByTitle('Registratie-instellingen bewerken') + await userEvent.click(editIcon) + + const checkbox = await canvas.findByRole('checkbox'); + await expect(checkbox).toBeChecked(); + }) + + + }, +}; + export const ConfigurePrefill = { play: async ({canvasElement}) => { const canvas = within(canvasElement); From 2c2abf93a153ddfb403e9bd01d63d1aa7a5d532d Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 2 Jan 2025 10:40:42 +0100 Subject: [PATCH 11/22] :construction: [#4908] Add hard-coded schema to result dict in json registration plugin Just as an example of what will be implemented in #4980 --- .../registrations/contrib/json/plugin.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py index ee6f26276c..3ac854c85b 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -29,8 +29,6 @@ def register_submission(self, submission: Submission, options: JSONOptions) -> d f.seek(0) values[attachment.form_key] = base64.b64encode(f.read()).decode() - # TODO-4908: what should the behaviour be when a form - # variable is not in the data or static variables? # Create static variables dict static_variables = get_static_variables(submission=submission) static_variables_dict = { @@ -46,10 +44,33 @@ def register_submission(self, submission: Submission, options: JSONOptions) -> d } ) - print(values) + # Generate schema + # TODO: will be added in #4980. Hardcoded example for now. + schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "static_var_1": { + "type": "string", + "pattern": "^cool_pattern$" + }, + "form_var_1": { + "type": "string" + }, + "form_var_2": { + "type": "string" + }, + "attachment": { + "type": "string", + "contentEncoding": "base64" + }, + }, + "required": ["static_var_1", "form_var_1", "form_var_2"], + "additionalProperties": False, + } # Send to the service - json = {"values": values} + json = {"values": values, "schema": schema} service = options["service"] submission.registration_result = result = {} with build_client(service) as client: From ac487c4940be7a431cff82bb0bdb845ec691442f Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 2 Jan 2025 11:36:47 +0100 Subject: [PATCH 12/22] :art: [#4908] Fix imports and black code --- .../form_design/RegistrationFields.stories.js | 3 +- .../admin/form_design/registrations/index.js | 2 +- .../registrations/json/JSONOptionsForm.js | 13 +++--- .../registrations/json/JSONSummaryHandler.js | 17 ++++---- .../json/JSONVariableConfigurationEditor.js | 36 ++++++++-------- .../json/fields/RelativeAPIEndpoint.js | 1 - .../json/fields/ServiceSelect.js | 6 +-- .../variables/VariablesEditor.stories.js | 13 +++--- .../registrations/contrib/json/config.py | 9 +++- .../registrations/contrib/json/plugin.py | 20 +++------ .../contrib/json/tests/test_backend.py | 41 +++++++++++++------ 11 files changed, 83 insertions(+), 78 deletions(-) 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 af295481b6..16db4e5ab8 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -404,7 +404,7 @@ export default { properties: { service: { enum: [1, 2], - enumNames: ['Service 1', 'Service 2'], + enumNames: ['Service 1', 'Service 2'], }, relativeApiEndpoint: { minLength: 1, @@ -1019,7 +1019,6 @@ export const STUFZDS = { }, }; - export const JSON = { args: { configuredBackends: [ diff --git a/src/openforms/js/components/admin/form_design/registrations/index.js b/src/openforms/js/components/admin/form_design/registrations/index.js index 9a973dc8f7..76cac9cef3 100644 --- a/src/openforms/js/components/admin/form_design/registrations/index.js +++ b/src/openforms/js/components/admin/form_design/registrations/index.js @@ -49,7 +49,7 @@ export const BACKEND_OPTIONS_FORMS = { form: StufZDSOptionsForm, }, 'microsoft-graph': {form: MSGraphOptionsForm}, - 'json': { + json: { form: JSONOptionsForm, configurableFromVariables: true, summaryHandler: JSONSummaryHandler, diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js index 9bb72a1974..45bde93ab9 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js @@ -19,13 +19,12 @@ import FormVariablesSelect from './fields/FormVariablesSelect'; import RelativeAPIEndpoint from './fields/RelativeAPIEndpoint'; import ServiceSelect from './fields/ServiceSelect'; - const JSONOptionsForm = ({name, label, schema, formData, onChange}) => { const validationErrors = useContext(ValidationErrorContext); const relevantErrors = filterErrors(name, validationErrors); // Get form variables and create form variable options - const formContext = useContext(FormContext) + const formContext = useContext(FormContext); const formVariables = formContext.formVariables ?? []; const staticVariables = formContext.staticVariables ?? []; const allFormVariables = staticVariables.concat(formVariables); @@ -37,9 +36,9 @@ const JSONOptionsForm = ({name, label, schema, formData, onChange}) => { // Create service options const {service} = schema.properties; - const serviceOptions = getChoicesFromSchema( - service.enum, service.enumNames - ).map(([value, label]) => ({value, label})); + const serviceOptions = getChoicesFromSchema(service.enum, service.enumNames).map( + ([value, label]) => ({value, label}) + ); return ( { >
- + - +
diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js index 28878d65ac..50d3313412 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js +++ b/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js @@ -2,18 +2,13 @@ import {FormattedMessage} from 'react-intl'; import React from 'react'; const JSONSummaryHandler = ({variable, backendOptions}) => { - const isIncluded = backendOptions.formVariables.includes(variable.key); if (isIncluded) { return ( - + ); - } - else { + } else { return ( { }; -// TODO-4098: ?? JSONSummaryHandler.propTypes = { - + variable: PropTypes.shape({ + key: PropTypes.string.isRequired, + }).isRequired, + backendOptions: PropTypes.shape({ + formVariables: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired, }; export default JSONSummaryHandler; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js index fe423577f9..2d6d97f582 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js +++ b/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js @@ -4,14 +4,16 @@ import Field from '../../../forms/Field'; import {FormattedMessage} from 'react-intl'; import FormRow from '../../../forms/FormRow'; import React from 'react'; -import {useField, useFormikContext} from 'formik'; -import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {Checkbox} from 'components/admin/forms/Inputs'; const JSONVariableConfigurationEditor = ({variable}) => { const [fieldProps, , {setValue}] = useField('formVariables'); - const formVariables = fieldProps.value + const formVariables = fieldProps.value; const isIncluded = formVariables.includes(variable.key); return ( @@ -36,17 +38,20 @@ const JSONVariableConfigurationEditor = ({variable}) => { const formVariablesNew = formVariables.slice(); const index = formVariablesNew.indexOf(variable.key); if (event.target.checked) { - // TODO-4908: remove this when testing is implemented - if (index !== -1) {throw new Error( - "This form variable is already on the list of " + - "form variables to include. This shouldn't happen" - );} + if (index !== -1) { + throw new Error( + 'This form variable is already on the list of ' + + "form variables to include. This shouldn't happen." + ); + } formVariablesNew.push(variable.key); } else { - if (index === -1) {throw new Error( - "This form variable is not yet on the list of " + - "form variables to include. This shouldn't happen." - );} + if (index === -1) { + throw new Error( + 'This form variable is not yet on the list of ' + + "form variables to include. This shouldn't happen." + ); + } formVariablesNew.splice(index, 1); } setValue(formVariablesNew); @@ -54,8 +59,8 @@ const JSONVariableConfigurationEditor = ({variable}) => { /> - ) -} + ); +}; // TODO-4098: ??? JSONVariableConfigurationEditor.propTypes = { @@ -64,5 +69,4 @@ JSONVariableConfigurationEditor.propTypes = { }).isRequired, }; - -export default JSONVariableConfigurationEditor +export default JSONVariableConfigurationEditor; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js b/src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js index a4febbc941..a59c14e2b5 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js +++ b/src/openforms/js/components/admin/form_design/registrations/json/fields/RelativeAPIEndpoint.js @@ -6,7 +6,6 @@ import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; import {TextInput} from 'components/admin/forms/Inputs'; - const RelativeAPIEndpoint = () => { const [fieldProps] = useField('relativeApiEndpoint'); return ( diff --git a/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js b/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js index 2afc14d9ba..e834cae168 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js +++ b/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js @@ -24,11 +24,7 @@ const ServiceSelect = ({options}) => { /> } > - + ); 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 6b63c5f441..c06ad80d03 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 @@ -670,7 +670,6 @@ export const WithJSONRegistrationBackend = { ], }, play: async ({canvasElement, step}) => { - // TODO-4098: can I get the formVariables backendOptions here? const canvas = within(canvasElement); const editIcons = canvas.getAllByTitle('Registratie-instellingen bewerken'); @@ -684,7 +683,7 @@ export const WithJSONRegistrationBackend = { const saveButton = canvas.getByRole('button', {name: 'Opslaan'}); await userEvent.click(saveButton); - }) + }); await step('aSingleFile checkbox checked', async () => { await userEvent.click(editIcons[1]); @@ -694,20 +693,18 @@ export const WithJSONRegistrationBackend = { const saveButton = canvas.getByRole('button', {name: 'Opslaan'}); await userEvent.click(saveButton); - }) + }); await step('now checkbox checked', async () => { const staticVariables = canvas.getByRole('tab', {name: 'Vaste variabelen'}); await userEvent.click(staticVariables); - const editIcon = canvas.getByTitle('Registratie-instellingen bewerken') - await userEvent.click(editIcon) + const editIcon = canvas.getByTitle('Registratie-instellingen bewerken'); + await userEvent.click(editIcon); const checkbox = await canvas.findByRole('checkbox'); await expect(checkbox).toBeChecked(); - }) - - + }); }, }; diff --git a/src/openforms/registrations/contrib/json/config.py b/src/openforms/registrations/contrib/json/config.py index f7f2a4d8fb..c4af8a1dd7 100644 --- a/src/openforms/registrations/contrib/json/config.py +++ b/src/openforms/registrations/contrib/json/config.py @@ -23,14 +23,18 @@ class JSONOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): relative_api_endpoint = serializers.CharField( max_length=255, label=_("Relative API endpoint"), - help_text=_("The API endpoint to send the data to (relative to the service API root)."), + help_text=_( + "The API endpoint to send the data to (relative to the service API root)." + ), allow_blank=True, required=False, ) form_variables = serializers.ListField( child=FormioVariableKeyField(max_length=50), label=_("Form variable key list"), - help_text=_("A list of form variables (can also include static variables) to use."), + help_text=_( + "A list of form variables (can also include static variables) to use." + ), required=True, min_length=1, ) @@ -43,6 +47,7 @@ class JSONOptions(TypedDict): This describes the shape of :attr:`JSONOptionsSerializer.validated_data`, after the input data has been cleaned/validated. """ + service: Required[Service] relative_api_endpoint: str form_variables: Required[list[str]] diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json/plugin.py index 3ac854c85b..bdb5993773 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json/plugin.py @@ -22,7 +22,7 @@ def register_submission(self, submission: Submission, options: JSONOptions) -> d # Encode (base64) and add attachments to values dict if their form keys were specified in the # form variables list for attachment in submission.attachments: - if not attachment.form_key in options["form_variables"]: + if attachment.form_key not in options["form_variables"]: continue options["form_variables"].remove(attachment.form_key) with attachment.content.open("rb") as f: @@ -50,20 +50,10 @@ def register_submission(self, submission: Submission, options: JSONOptions) -> d "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { - "static_var_1": { - "type": "string", - "pattern": "^cool_pattern$" - }, - "form_var_1": { - "type": "string" - }, - "form_var_2": { - "type": "string" - }, - "attachment": { - "type": "string", - "contentEncoding": "base64" - }, + "static_var_1": {"type": "string", "pattern": "^cool_pattern$"}, + "form_var_1": {"type": "string"}, + "form_var_2": {"type": "string"}, + "attachment": {"type": "string", "contentEncoding": "base64"}, }, "required": ["static_var_1", "form_var_1", "form_var_2"], "additionalProperties": False, diff --git a/src/openforms/registrations/contrib/json/tests/test_backend.py b/src/openforms/registrations/contrib/json/tests/test_backend.py index b21eb25b9f..cfcf10d4fa 100644 --- a/src/openforms/registrations/contrib/json/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json/tests/test_backend.py @@ -1,6 +1,8 @@ from django.test import TestCase -from openforms.appointments.contrib.qmatic.tests.factories import ServiceFactory +from requests import RequestException +from zgw_consumers.test.factories import ServiceFactory + from openforms.submissions.public_references import set_submission_reference from openforms.submissions.tests.factories import ( SubmissionFactory, @@ -9,9 +11,11 @@ from ..plugin import JSONRegistration +VCR_TEST_FILES = Path(__file__).parent / "files" + -class JSONBackendTests(TestCase): - # VCR_TEST_FILES = VCR_TEST_FILES +class JSONBackendTests(OFVCRMixin, TestCase): + VCR_TEST_FILES = VCR_TEST_FILES def test_submission_with_json_backend(self): submission = SubmissionFactory.from_components( @@ -55,14 +59,27 @@ def test_submission_with_json_backend(self): set_submission_reference(submission) - data_to_be_sent = email_submission.register_submission(submission, json_form_options) - - expected_data_to_be_sent = { - "values": { - "firstName": "We Are", - "lastName": "Checking", - "file": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", - "auth_bsn": "123456789", - } + expected_response = { + # Note that `lastName` is not included here as it wasn't specified in the form_variables + "data": { + "values": { + "auth_bsn": "123456789", + "file": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", # Content of the attachment encoded using base64 + "firstName": "We Are", + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "static_var_1": {"type": "string", "pattern": "^cool_pattern$"}, + "form_var_1": {"type": "string"}, + "form_var_2": {"type": "string"}, + "attachment": {"type": "string", "contentEncoding": "base64"}, + }, + "required": ["static_var_1", "form_var_1", "form_var_2"], + "additionalProperties": False, + }, + }, + "message": "Data received", } self.assertEqual(data_to_be_sent, expected_data_to_be_sent) From 12f5d7824ea72d1f7633a069f9923a4be63cfad7 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 2 Jan 2025 15:48:42 +0100 Subject: [PATCH 13/22] :ok_hand: [#4908] Process PR feedback * Rename plugin from JSON to JSONDump * Add required and noManageChildProps to ServiceSelect and FormVariableSelect because they are required and need to prevent some Field props being passed down to the children * Remove max length of FormioVariableKeyField: it is based on a text field which has no length constraints * Remove id field from RelativeAPIEndpoint TextInput: is managed automatically * Remove Required from JSONDumpOptions: required is the default for TypedDict * Add default value for relative_api_endpoint: guarantees that the key is always present, which gives a strict type definition * Remove content type headers: is set automatically when using the json kwargs * Replace 'Included/Not Included' text with icons: makes it easier for the user to see whether it is included. * Revise JSONDumpVariableConfigurationEditor: use useFormikContext instead of useField, and clean up the on-change event * Add initial form data: formData will be an empty object if we add a new object, which means the default values for the configuration fields are missing * Add missing form and static variable properties in JSONDump story * Refactor building the values object of the to be submitted data: tt is neater to build up what needs to be processed first, before doing the processing --- ...ation.yml => docker-compose.json-dump.yml} | 6 +- .../Dockerfile | 0 .../README.md | 10 +-- .../{json-registration => json-dump}/app.py | 0 src/openforms/conf/base.py | 2 +- .../form_design/RegistrationFields.stories.js | 64 +++++++++++++---- .../admin/form_design/registrations/index.js | 16 +++-- .../registrations/json/JSONSummaryHandler.js | 31 -------- .../json/JSONVariableConfigurationEditor.js | 72 ------------------- .../form_design/registrations/json/index.js | 3 - .../JSONDumpOptionsForm.js} | 20 +++--- .../json_dump/JSONDumpSummaryHandler.js | 21 ++++++ .../JSONDumpVariableConfigurationEditor.js | 54 ++++++++++++++ .../fields/FormVariablesSelect.js | 2 + .../fields/RelativeAPIEndpoint.js | 2 +- .../fields/ServiceSelect.js | 4 +- .../registrations/json_dump/fields/index.js | 5 ++ .../registrations/json_dump/index.js | 5 ++ .../variables/VariablesEditor.stories.js | 8 +-- .../registrations/contrib/json/apps.py | 12 ---- .../contrib/{json => json_dump}/__init__.py | 0 .../registrations/contrib/json_dump/apps.py | 11 +++ .../contrib/{json => json_dump}/config.py | 15 ++-- .../migrations}/__init__.py | 0 .../contrib/{json => json_dump}/plugin.py | 44 ++++++------ .../contrib/json_dump/tests/__init__.py | 0 ...ervice_returns_unexpected_status_code.yaml | 54 ++++++++++++++ ...est_submission_with_json_dump_backend.yaml | 52 ++++++++++++++ .../{json => json_dump}/tests/test_backend.py | 53 +++++++++++--- 29 files changed, 361 insertions(+), 205 deletions(-) rename docker/{docker-compose.json-registration.yml => docker-compose.json-dump.yml} (59%) rename docker/{json-registration => json-dump}/Dockerfile (100%) rename docker/{json-registration => json-dump}/README.md (51%) rename docker/{json-registration => json-dump}/app.py (100%) delete mode 100644 src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js delete mode 100644 src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js delete mode 100644 src/openforms/js/components/admin/form_design/registrations/json/index.js rename src/openforms/js/components/admin/form_design/registrations/{json/JSONOptionsForm.js => json_dump/JSONDumpOptionsForm.js} (83%) create mode 100644 src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpSummaryHandler.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpVariableConfigurationEditor.js rename src/openforms/js/components/admin/form_design/registrations/{json => json_dump}/fields/FormVariablesSelect.js (97%) rename src/openforms/js/components/admin/form_design/registrations/{json => json_dump}/fields/RelativeAPIEndpoint.js (93%) rename src/openforms/js/components/admin/form_design/registrations/{json => json_dump}/fields/ServiceSelect.js (92%) create mode 100644 src/openforms/js/components/admin/form_design/registrations/json_dump/fields/index.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json_dump/index.js delete mode 100644 src/openforms/registrations/contrib/json/apps.py rename src/openforms/registrations/contrib/{json => json_dump}/__init__.py (100%) create mode 100644 src/openforms/registrations/contrib/json_dump/apps.py rename src/openforms/registrations/contrib/{json => json_dump}/config.py (79%) rename src/openforms/registrations/contrib/{json/tests => json_dump/migrations}/__init__.py (100%) rename src/openforms/registrations/contrib/{json => json_dump}/plugin.py (69%) create mode 100644 src/openforms/registrations/contrib/json_dump/tests/__init__.py create mode 100644 src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml create mode 100644 src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml rename src/openforms/registrations/contrib/{json => json_dump}/tests/test_backend.py (59%) diff --git a/docker/docker-compose.json-registration.yml b/docker/docker-compose.json-dump.yml similarity index 59% rename from docker/docker-compose.json-registration.yml rename to docker/docker-compose.json-dump.yml index 9cd4030faf..bbf9908a84 100644 --- a/docker/docker-compose.json-registration.yml +++ b/docker/docker-compose.json-dump.yml @@ -1,14 +1,14 @@ version: '3.8' -name: json-registration +name: json-dump services: flask_app: - build: ./json-registration + build: ./json-dump ports: - "80:80" volumes: - - ./json-registration/:/app/ + - ./json-dump/:/app/ networks: open-forms-dev: diff --git a/docker/json-registration/Dockerfile b/docker/json-dump/Dockerfile similarity index 100% rename from docker/json-registration/Dockerfile rename to docker/json-dump/Dockerfile diff --git a/docker/json-registration/README.md b/docker/json-dump/README.md similarity index 51% rename from docker/json-registration/README.md rename to docker/json-dump/README.md index 675b25f92e..bbdcec1c7d 100644 --- a/docker/json-registration/README.md +++ b/docker/json-dump/README.md @@ -1,8 +1,8 @@ -# JSON registration plugin +# JSON dump registration plugin -The `docker-compose.json-registration.yml` compose file is available to run a mock service intended -to simulate a receiving server to test the JSON registration backend plugin. It contains an endpoint for -sending json data (`json_plugin`) and testing the connection (`test_connection`). +The `docker-compose.json-dump.yml` compose file is available to run a mock service intended +to simulate a receiving server to test the JSON dump registration backend plugin. It contains an +endpoint for sending json data (`json_plugin`) and testing the connection (`test_connection`). The `json_plugin` endpoint returns a confirmation message that the data was received, together with the received data. The `test_connection` endpoint just returns an 'OK' message. @@ -12,7 +12,7 @@ received data. The `test_connection` endpoint just returns an 'OK' message. Start an instance in your local environment from the parent directory: ```bash -docker compose -f docker-compose.json-registration.yml up -d +docker compose -f docker-compose.json-dump.yml up -d ``` This starts a flask application at http://localhost:80/ with the endpoints `json_plugin` and `test_connection`. diff --git a/docker/json-registration/app.py b/docker/json-dump/app.py similarity index 100% rename from docker/json-registration/app.py rename to docker/json-dump/app.py diff --git a/src/openforms/conf/base.py b/src/openforms/conf/base.py index f3a227b0ba..6706c1d0dd 100644 --- a/src/openforms/conf/base.py +++ b/src/openforms/conf/base.py @@ -225,7 +225,7 @@ "openforms.registrations.contrib.objects_api", "openforms.registrations.contrib.microsoft_graph.apps.MicrosoftGraphApp", "openforms.registrations.contrib.camunda.apps.CamundaApp", - "openforms.registrations.contrib.json", + "openforms.registrations.contrib.json_dump", "openforms.prefill", "openforms.prefill.contrib.demo.apps.DemoApp", "openforms.prefill.contrib.kvk.apps.KVKPrefillApp", 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 16db4e5ab8..6f993e4d57 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -397,8 +397,8 @@ export default { }, }, { - id: 'json', - label: 'JSON registration', + id: 'json_dump', + label: 'JSON Dump registration', schema: { type: 'object', properties: { @@ -765,11 +765,11 @@ export const ConfiguredBackends = { }, { key: 'backend11', - name: 'JSON', - backend: 'json', + name: 'JSON Dump', + backend: 'json_dump', options: { service: 1, - relativeApiEndpoint: 'Example endpoint', + relativeApiEndpoint: 'example/endpoint', formVariables: [], }, }, @@ -1019,44 +1019,68 @@ export const STUFZDS = { }, }; -export const JSON = { +export const JSONDump = { args: { configuredBackends: [ { key: 'backend11', - name: 'JSON', - backend: 'json', + name: 'JSON Dump', + backend: 'json_dump', options: { service: 1, - relativeApiEndpoint: 'We are checking.', + relativeApiEndpoint: 'example/endpoint', formVariables: [], }, }, ], availableFormVariables: [ { - dataType: 'string', form: null, formDefinition: null, - key: 'firstName', name: 'First name', + key: 'firstName', source: 'user_defined', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: '', + prefillOptions: {}, + dataType: 'string', + dataFormat: '', + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: '', }, { - dataType: 'string', form: null, formDefinition: null, - key: 'lastName', name: 'Last name', + key: 'lastName', source: 'user_defined', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: '', + prefillOptions: {}, + dataType: 'string', + dataFormat: '', + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: '', }, { - dataType: 'file', form: null, formDefinition: null, - key: 'attachment', name: 'Attachment', + key: 'attachment', source: 'user_defined', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: '', + prefillOptions: {}, + dataType: 'file', + dataFormat: '', + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: '', }, ], availableStaticVariables: [ @@ -1065,6 +1089,16 @@ export const JSON = { formDefinition: null, name: 'BSN', key: 'auth_bsn', + source: 'static', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: '', + prefillOptions: {}, + dataType: 'string', + dataFormat: '', + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: '', }, ], }, diff --git a/src/openforms/js/components/admin/form_design/registrations/index.js b/src/openforms/js/components/admin/form_design/registrations/index.js index 76cac9cef3..0b87a2d813 100644 --- a/src/openforms/js/components/admin/form_design/registrations/index.js +++ b/src/openforms/js/components/admin/form_design/registrations/index.js @@ -1,9 +1,11 @@ import CamundaOptionsForm from './camunda'; import DemoOptionsForm from './demo'; import EmailOptionsForm from './email'; -import JSONOptionsForm from './json/JSONOptionsForm'; -import JSONSummaryHandler from './json/JSONSummaryHandler'; -import JSONVariableConfigurationEditor from './json/JSONVariableConfigurationEditor'; +import { + JSONDumpOptionsForm, + JSONDumpSummaryHandler, + JSONDumpVariableConfigurationEditor, +} from './json_dump'; import MSGraphOptionsForm from './ms_graph'; import ObjectsApiOptionsForm from './objectsapi/ObjectsApiOptionsForm'; import ObjectsApiSummaryHandler from './objectsapi/ObjectsApiSummaryHandler'; @@ -49,11 +51,11 @@ export const BACKEND_OPTIONS_FORMS = { form: StufZDSOptionsForm, }, 'microsoft-graph': {form: MSGraphOptionsForm}, - json: { - form: JSONOptionsForm, + json_dump: { + form: JSONDumpOptionsForm, configurableFromVariables: true, - summaryHandler: JSONSummaryHandler, - variableConfigurationEditor: JSONVariableConfigurationEditor, + summaryHandler: JSONDumpSummaryHandler, + variableConfigurationEditor: JSONDumpVariableConfigurationEditor, }, // demo plugins demo: {form: DemoOptionsForm}, diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js deleted file mode 100644 index 50d3313412..0000000000 --- a/src/openforms/js/components/admin/form_design/registrations/json/JSONSummaryHandler.js +++ /dev/null @@ -1,31 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import React from 'react'; - -const JSONSummaryHandler = ({variable, backendOptions}) => { - const isIncluded = backendOptions.formVariables.includes(variable.key); - - if (isIncluded) { - return ( - - ); - } else { - return ( - - ); - } -}; - - -JSONSummaryHandler.propTypes = { - variable: PropTypes.shape({ - key: PropTypes.string.isRequired, - }).isRequired, - backendOptions: PropTypes.shape({ - formVariables: PropTypes.arrayOf(PropTypes.string).isRequired, - }).isRequired, -}; - -export default JSONSummaryHandler; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js deleted file mode 100644 index 2d6d97f582..0000000000 --- a/src/openforms/js/components/admin/form_design/registrations/json/JSONVariableConfigurationEditor.js +++ /dev/null @@ -1,72 +0,0 @@ -// TODO-4908: fix imports -import {Checkbox} from 'components/admin/forms/Inputs'; -import Field from '../../../forms/Field'; -import {FormattedMessage} from 'react-intl'; -import FormRow from '../../../forms/FormRow'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import Field from 'components/admin/forms/Field'; -import FormRow from 'components/admin/forms/FormRow'; -import {Checkbox} from 'components/admin/forms/Inputs'; - -const JSONVariableConfigurationEditor = ({variable}) => { - const [fieldProps, , {setValue}] = useField('formVariables'); - - const formVariables = fieldProps.value; - const isIncluded = formVariables.includes(variable.key); - - return ( - - - - } - helpText={ - - } - checked={isIncluded} - onChange={event => { - const formVariablesNew = formVariables.slice(); - const index = formVariablesNew.indexOf(variable.key); - if (event.target.checked) { - if (index !== -1) { - throw new Error( - 'This form variable is already on the list of ' + - "form variables to include. This shouldn't happen." - ); - } - formVariablesNew.push(variable.key); - } else { - if (index === -1) { - throw new Error( - 'This form variable is not yet on the list of ' + - "form variables to include. This shouldn't happen." - ); - } - formVariablesNew.splice(index, 1); - } - setValue(formVariablesNew); - }} - /> - - - ); -}; - -// TODO-4098: ??? -JSONVariableConfigurationEditor.propTypes = { - variable: PropTypes.shape({ - key: PropTypes.string.isRequired, - }).isRequired, -}; - -export default JSONVariableConfigurationEditor; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/index.js b/src/openforms/js/components/admin/form_design/registrations/json/index.js deleted file mode 100644 index 97a74031a6..0000000000 --- a/src/openforms/js/components/admin/form_design/registrations/json/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import JSONOptionsForm from './JSONOptionsForm'; - -export default JSONOptionsForm; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpOptionsForm.js similarity index 83% rename from src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js rename to src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpOptionsForm.js index 45bde93ab9..ddb0018777 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/JSONOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpOptionsForm.js @@ -12,14 +12,9 @@ import { } from 'components/admin/forms/ValidationErrors'; import {getChoicesFromSchema} from 'utils/json-schema'; -// TODO-4908: maybe create separate file (JSONOptionsFormFields) for all the fields? -// Though, no need to use multiple FieldSets, so adding the fields to the form is pretty -// straightforward. -import FormVariablesSelect from './fields/FormVariablesSelect'; -import RelativeAPIEndpoint from './fields/RelativeAPIEndpoint'; -import ServiceSelect from './fields/ServiceSelect'; +import {FormVariablesSelect, RelativeAPIEndpoint, ServiceSelect} from './fields'; -const JSONOptionsForm = ({name, label, schema, formData, onChange}) => { +const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { const validationErrors = useContext(ValidationErrorContext); const relevantErrors = filterErrors(name, validationErrors); @@ -51,7 +46,12 @@ const JSONOptionsForm = ({name, label, schema, formData, onChange}) => { defaultMessage="Plugin configuration: JSON" /> } - initialFormData={{...formData}} + initialFormData={{ + service: null, + relativeApiEndpoint: '', + formVariables: [], + ...formData, + }} onSubmit={values => onChange({formData: values})} modalSize="medium" > @@ -66,7 +66,7 @@ const JSONOptionsForm = ({name, label, schema, formData, onChange}) => { ); }; -JSONOptionsForm.propTypes = { +JSONDumpOptionsForm.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.node.isRequired, schema: PropTypes.shape({ @@ -87,4 +87,4 @@ JSONOptionsForm.propTypes = { onChange: PropTypes.func.isRequired, }; -export default JSONOptionsForm; +export default JSONDumpOptionsForm; 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 new file mode 100644 index 0000000000..4c971e5ce2 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpSummaryHandler.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import {IconNo, IconYes} from 'components/admin/BooleanIcons'; + +const JSONDumpSummaryHandler = ({variable, backendOptions}) => { + const isIncluded = backendOptions.formVariables.includes(variable.key); + + return isIncluded ? : ; +}; + +JSONDumpSummaryHandler.propTypes = { + variable: PropTypes.shape({ + key: PropTypes.string.isRequired, + }).isRequired, + backendOptions: PropTypes.shape({ + formVariables: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired, +}; + +export default JSONDumpSummaryHandler; 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 new file mode 100644 index 0000000000..802031ebe3 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/JSONDumpVariableConfigurationEditor.js @@ -0,0 +1,54 @@ +import {useFormikContext} from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {Checkbox} from 'components/admin/forms/Inputs'; + +const JSONDumpVariableConfigurationEditor = ({variable}) => { + const { + values: {formVariables = []}, + setFieldValue, + } = useFormikContext(); + const isIncluded = formVariables.includes(variable.key); + + return ( + + + + } + helpText={ + + } + checked={isIncluded} + onChange={event => { + const shouldBeIncluded = event.target.checked; + const newFormVariables = shouldBeIncluded + ? [...formVariables, variable.key] // add the variable to the array + : formVariables.filter(key => key !== variable.key); // remove the variable from the array + setFieldValue('formVariables', newFormVariables); + }} + /> + + + ); +}; + +JSONDumpVariableConfigurationEditor.propTypes = { + variable: PropTypes.shape({ + key: PropTypes.string.isRequired, + }).isRequired, +}; + +export default JSONDumpVariableConfigurationEditor; diff --git a/src/openforms/js/components/admin/form_design/registrations/json/fields/FormVariablesSelect.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js similarity index 97% rename from src/openforms/js/components/admin/form_design/registrations/json/fields/FormVariablesSelect.js rename to src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js index 2d49b7bb0a..ea3102f5b1 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/fields/FormVariablesSelect.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js @@ -36,6 +36,8 @@ const FormVariablesSelect = ({options}) => { defaultMessage="Which form variables to include in the data to be sent" /> } + required + noManageChildProps > { /> } > - + ); diff --git a/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/ServiceSelect.js similarity index 92% rename from src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js rename to src/openforms/js/components/admin/form_design/registrations/json_dump/fields/ServiceSelect.js index e834cae168..6240cffdc4 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json/fields/ServiceSelect.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/ServiceSelect.js @@ -23,6 +23,8 @@ const ServiceSelect = ({options}) => { defaultMessage="Which service to send the data to" /> } + required + noManageChildProps > @@ -33,7 +35,7 @@ const ServiceSelect = ({options}) => { ServiceSelect.propTypes = { options: PropTypes.arrayOf( PropTypes.shape({ - value: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, label: PropTypes.node.isRequired, }) ).isRequired, 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 new file mode 100644 index 0000000000..f0d78829b4 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/index.js @@ -0,0 +1,5 @@ +import FormVariablesSelect from './FormVariablesSelect'; +import RelativeAPIEndpoint from './RelativeAPIEndpoint'; +import ServiceSelect from './ServiceSelect'; + +export {FormVariablesSelect, RelativeAPIEndpoint, ServiceSelect}; diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/index.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/index.js new file mode 100644 index 0000000000..3a7de1f457 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/index.js @@ -0,0 +1,5 @@ +import JSONDumpOptionsForm from './JSONDumpOptionsForm'; +import JSONDumpSummaryHandler from './JSONDumpSummaryHandler'; +import JSONDumpVariableConfigurationEditor from './JSONDumpVariableConfigurationEditor'; + +export {JSONDumpOptionsForm, JSONDumpSummaryHandler, JSONDumpVariableConfigurationEditor}; 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 c06ad80d03..7833fa768b 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 @@ -654,13 +654,13 @@ export const WithObjectsAPIAndTestRegistrationBackends = { }, }; -export const WithJSONRegistrationBackend = { +export const WithJSONDumpRegistrationBackend = { args: { registrationBackends: [ { - backend: 'json', - key: 'test_json_backend', - name: 'JSON registration', + backend: 'json_dump', + key: 'test_json_dump_backend', + name: 'JSON dump registration', options: { service: 2, relativeApiEndpoint: 'test', diff --git a/src/openforms/registrations/contrib/json/apps.py b/src/openforms/registrations/contrib/json/apps.py deleted file mode 100644 index 0553f43210..0000000000 --- a/src/openforms/registrations/contrib/json/apps.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -# TODO-4908: maybe rename to FVaJ (Form Variables as JSON) -class JSONAppConfig(AppConfig): - name = "openforms.registrations.contrib.json" - label = "registrations_json" - verbose_name = _("JSON plugin") - - def ready(self): - from . import plugin # noqa diff --git a/src/openforms/registrations/contrib/json/__init__.py b/src/openforms/registrations/contrib/json_dump/__init__.py similarity index 100% rename from src/openforms/registrations/contrib/json/__init__.py rename to src/openforms/registrations/contrib/json_dump/__init__.py diff --git a/src/openforms/registrations/contrib/json_dump/apps.py b/src/openforms/registrations/contrib/json_dump/apps.py new file mode 100644 index 0000000000..646d6e254a --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class JSONDumpAppConfig(AppConfig): + name = "openforms.registrations.contrib.json_dump" + label = "registrations_json_dump" + verbose_name = _("JSON Dump plugin") + + def ready(self): + from . import plugin # noqa diff --git a/src/openforms/registrations/contrib/json/config.py b/src/openforms/registrations/contrib/json_dump/config.py similarity index 79% rename from src/openforms/registrations/contrib/json/config.py rename to src/openforms/registrations/contrib/json_dump/config.py index c4af8a1dd7..3446c9eba7 100644 --- a/src/openforms/registrations/contrib/json/config.py +++ b/src/openforms/registrations/contrib/json_dump/config.py @@ -11,7 +11,7 @@ from openforms.utils.mixins import JsonSchemaSerializerMixin -class JSONOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): +class JSONDumpOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): service = PrimaryKeyRelatedAsChoicesField( queryset=Service.objects.filter(api_type=APITypes.orc), label=_("Service"), @@ -28,9 +28,10 @@ class JSONOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): ), allow_blank=True, required=False, + default="", ) form_variables = serializers.ListField( - child=FormioVariableKeyField(max_length=50), + child=FormioVariableKeyField(), label=_("Form variable key list"), help_text=_( "A list of form variables (can also include static variables) to use." @@ -40,14 +41,14 @@ class JSONOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): ) -class JSONOptions(TypedDict): +class JSONDumpOptions(TypedDict): """ - JSON registration plugin options + JSON dump registration plugin options - This describes the shape of :attr:`JSONOptionsSerializer.validated_data`, after + This describes the shape of :attr:`JSONDumpOptionsSerializer.validated_data`, after the input data has been cleaned/validated. """ - service: Required[Service] + service: Service relative_api_endpoint: str - form_variables: Required[list[str]] + form_variables: list[str] diff --git a/src/openforms/registrations/contrib/json/tests/__init__.py b/src/openforms/registrations/contrib/json_dump/migrations/__init__.py similarity index 100% rename from src/openforms/registrations/contrib/json/tests/__init__.py rename to src/openforms/registrations/contrib/json_dump/migrations/__init__.py diff --git a/src/openforms/registrations/contrib/json/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py similarity index 69% rename from src/openforms/registrations/contrib/json/plugin.py rename to src/openforms/registrations/contrib/json_dump/plugin.py index bdb5993773..1159e94965 100644 --- a/src/openforms/registrations/contrib/json/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -5,20 +5,34 @@ from zgw_consumers.client import build_client from openforms.submissions.models import Submission +from openforms.typing import JSONObject 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 JSONOptions, JSONOptionsSerializer +from .config import JSONDumpOptions, JSONDumpOptionsSerializer -@register("json") -class JSONRegistration(BasePlugin): - verbose_name = _("JSON registration") - configuration_options = JSONOptionsSerializer +@register("json_dump") +class JSONDumpRegistration(BasePlugin): + verbose_name = _("JSON dump registration") + configuration_options = JSONDumpOptionsSerializer + + def register_submission( + self, submission: Submission, options: JSONDumpOptions + ) -> dict: + state = submission.load_submission_value_variables_state() + + all_values: JSONObject = { + **state.get_static_data(), + **state.get_data(), # dynamic values from user input + } + values = { + key: value + for key, value in all_values.items() + if key in options["form_variables"] + } - def register_submission(self, submission: Submission, options: JSONOptions) -> dict: - values = {} # Encode (base64) and add attachments to values dict if their form keys were specified in the # form variables list for attachment in submission.attachments: @@ -29,21 +43,6 @@ def register_submission(self, submission: Submission, options: JSONOptions) -> d f.seek(0) values[attachment.form_key] = base64.b64encode(f.read()).decode() - # Create static variables dict - static_variables = get_static_variables(submission=submission) - static_variables_dict = { - variable.key: variable.initial_value for variable in static_variables - } - - # Update values dict with relevant form data - all_variables = {**submission.data, **static_variables_dict} - values.update( - { - form_variable: all_variables[form_variable] - for form_variable in options["form_variables"] - } - ) - # Generate schema # TODO: will be added in #4980. Hardcoded example for now. schema = { @@ -67,7 +66,6 @@ def register_submission(self, submission: Submission, options: JSONOptions) -> d result["api_response"] = res = client.post( options.get("relative_api_endpoint", ""), json=json, - headers={"Content-Type": "application/json"}, ) res.raise_for_status() diff --git a/src/openforms/registrations/contrib/json_dump/tests/__init__.py b/src/openforms/registrations/contrib/json_dump/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml new file mode 100644 index 0000000000..781d459570 --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml @@ -0,0 +1,54 @@ +interactions: +- request: + body: '{"values": {"firstName": "We Are", "auth_bsn": "123456789"}, "schema": + {"$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", + "properties": {"static_var_1": {"type": "string", "pattern": "^cool_pattern$"}, + "form_var_1": {"type": "string"}, "form_var_2": {"type": "string"}, "attachment": + {"type": "string", "contentEncoding": "base64"}}, "required": ["static_var_1", + "form_var_1", "form_var_2"], "additionalProperties": false}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3MzU4MjY2MzksImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.8PuPAIY6PI3_g4edfqzFFbHNldYxxRIBjPuAh-p00xk + Connection: + - keep-alive + Content-Length: + - '450' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost/fake_endpoint + response: + body: + string: ' + + + + 404 Not Found + +

Not Found

+ +

The requested URL was not found on the server. If you entered the URL manually + please check your spelling and try again.

+ + ' + headers: + Connection: + - close + Content-Length: + - '207' + Content-Type: + - text/html; charset=utf-8 + Date: + - Thu, 02 Jan 2025 14:03:59 GMT + Server: + - Werkzeug/3.1.3 Python/3.12.8 + status: + code: 404 + message: NOT FOUND +version: 1 diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml new file mode 100644 index 0000000000..d2294549fc --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml @@ -0,0 +1,52 @@ +interactions: +- request: + body: '{"values": {"file": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", "firstName": "We + Are", "auth_bsn": "123456789"}, "schema": {"$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", "properties": {"static_var_1": {"type": "string", "pattern": + "^cool_pattern$"}, "form_var_1": {"type": "string"}, "form_var_2": {"type": + "string"}, "attachment": {"type": "string", "contentEncoding": "base64"}}, "required": + ["static_var_1", "form_var_1", "form_var_2"], "additionalProperties": false}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3MzU4MjY2MzksImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.8PuPAIY6PI3_g4edfqzFFbHNldYxxRIBjPuAh-p00xk + Connection: + - keep-alive + Content-Length: + - '494' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost/json_plugin + response: + body: + string: "{\n \"data\": {\n \"schema\": {\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n + \ \"additionalProperties\": false,\n \"properties\": {\n \"attachment\": + {\n \"contentEncoding\": \"base64\",\n \"type\": \"string\"\n + \ },\n \"form_var_1\": {\n \"type\": \"string\"\n },\n + \ \"form_var_2\": {\n \"type\": \"string\"\n },\n \"static_var_1\": + {\n \"pattern\": \"^cool_pattern$\",\n \"type\": \"string\"\n + \ }\n },\n \"required\": [\n \"static_var_1\",\n \"form_var_1\",\n + \ \"form_var_2\"\n ],\n \"type\": \"object\"\n },\n \"values\": + {\n \"auth_bsn\": \"123456789\",\n \"file\": \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\",\n + \ \"firstName\": \"We Are\"\n }\n },\n \"message\": \"Data received\"\n}\n" + headers: + Connection: + - close + Content-Length: + - '783' + Content-Type: + - application/json + Date: + - Thu, 02 Jan 2025 14:03:59 GMT + Server: + - Werkzeug/3.1.3 Python/3.12.8 + status: + code: 201 + message: CREATED +version: 1 diff --git a/src/openforms/registrations/contrib/json/tests/test_backend.py b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py similarity index 59% rename from src/openforms/registrations/contrib/json/tests/test_backend.py rename to src/openforms/registrations/contrib/json_dump/tests/test_backend.py index cfcf10d4fa..82b4fcd94b 100644 --- a/src/openforms/registrations/contrib/json/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -1,3 +1,6 @@ +from base64 import b64decode +from pathlib import Path + from django.test import TestCase from requests import RequestException @@ -8,16 +11,17 @@ SubmissionFactory, SubmissionFileAttachmentFactory, ) +from openforms.utils.tests.vcr import OFVCRMixin -from ..plugin import JSONRegistration +from ..plugin import JSONDumpRegistration VCR_TEST_FILES = Path(__file__).parent / "files" -class JSONBackendTests(OFVCRMixin, TestCase): +class JSONDumpBackendTests(OFVCRMixin, TestCase): VCR_TEST_FILES = VCR_TEST_FILES - def test_submission_with_json_backend(self): + def test_submission_with_json_dump_backend(self): submission = SubmissionFactory.from_components( [ {"key": "firstName", "type": "textField"}, @@ -40,7 +44,7 @@ def test_submission_with_json_backend(self): bsn="123456789", ) - submission_file_attachment = SubmissionFileAttachmentFactory.create( + SubmissionFileAttachmentFactory.create( form_key="file", submission_step=submission.submissionstep_set.get(), file_name="test_file.txt", @@ -51,12 +55,11 @@ def test_submission_with_json_backend(self): ) json_form_options = dict( - service=ServiceFactory(api_root="http://example.com/api/v2"), - relative_api_endpoint="", - form_variables=["firstName", "lastName", "file", "auth_bsn"], + service=(ServiceFactory(api_root="http://localhost:80/")), + relative_api_endpoint="json_plugin", + form_variables=["firstName", "file", "auth_bsn"], ) - email_submission = JSONRegistration("json_plugin") - + json_plugin = JSONDumpRegistration("json_registration_plugin") set_submission_reference(submission) expected_response = { @@ -82,4 +85,34 @@ def test_submission_with_json_backend(self): }, "message": "Data received", } - self.assertEqual(data_to_be_sent, expected_data_to_be_sent) + + res = json_plugin.register_submission(submission, json_form_options) + res_json = res["api_response"].json() + + self.assertEqual(res_json, expected_response) + + with self.subTest("attachment content encoded"): + decoded_content = b64decode(res_json["data"]["values"]["file"]) + self.assertEqual(decoded_content, b"This is example content.") + + def test_exception_raised_when_service_returns_unexpected_status_code(self): + submission = SubmissionFactory.from_components( + [ + {"key": "firstName", "type": "textField"}, + {"key": "lastName", "type": "textfield"}, + ], + completed=True, + submitted_data={"firstName": "We Are", "lastName": "Checking"}, + bsn="123456789", + ) + + json_form_options = dict( + service=(ServiceFactory(api_root="http://localhost:80/")), + relative_api_endpoint="fake_endpoint", + form_variables=["firstName", "auth_bsn"], + ) + json_plugin = JSONDumpRegistration("json_registration_plugin") + set_submission_reference(submission) + + with self.assertRaises(RequestException): + json_plugin.register_submission(submission, json_form_options) From 55a4d255ac07ef70ac8f58a7d7e3d8ffd5fe791a Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Tue, 7 Jan 2025 16:06:36 +0100 Subject: [PATCH 14/22] :bug: [#4908] Fix bugs with JSON serializing * The requests.Response object is not JSON serializable, so need to add just the json response (assuming it is json for now) of it . * Convert the data to be sent to a json format. Needed because date/datetimes are not JSON serializable --- .../registrations/contrib/json_dump/plugin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/openforms/registrations/contrib/json_dump/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py index 1159e94965..929d6575e2 100644 --- a/src/openforms/registrations/contrib/json_dump/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -1,12 +1,13 @@ import base64 +import json +from django.core.serializers.json import DjangoJSONEncoder from django.utils.translation import gettext_lazy as _ from zgw_consumers.client import build_client from openforms.submissions.models import Submission from openforms.typing import JSONObject -from openforms.variables.service import get_static_variables from ...base import BasePlugin # openforms.registrations.base from ...registry import register # openforms.registrations.registry @@ -59,16 +60,18 @@ def register_submission( } # Send to the service - json = {"values": values, "schema": schema} + data = json.dumps({"values": values, "schema": schema}, cls=DjangoJSONEncoder) service = options["service"] submission.registration_result = result = {} with build_client(service) as client: - result["api_response"] = res = client.post( + res = client.post( options.get("relative_api_endpoint", ""), - json=json, + json=data, ) res.raise_for_status() + result["api_response"] = res.json() + return result def check_config(self) -> None: From 926964f1dd8648edbc21abbd3f569bdb46040f0a Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 9 Jan 2025 16:45:06 +0100 Subject: [PATCH 15/22] :bug: [#4908] Revise how attachments are processed There was a bug with the previous attachment processing, where attachments would get overwritten if multiple were uploaded. Now, an object will be returned with the 'file_name' and 'content' properties in case of a single file component (or 'None' if no file was uploaded). And in case of a multiple files component, it will be a list of these file objects (or an empty list if no file was uploaded). This keeps the data type consistent, and we can give guarantees that an object will always have the file_name and content keys. --- .../registrations/contrib/json_dump/plugin.py | 84 ++++++++++++++++--- .../contrib/json_dump/tests/test_backend.py | 62 ++++++++++++++ 2 files changed, 136 insertions(+), 10 deletions(-) diff --git a/src/openforms/registrations/contrib/json_dump/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py index 929d6575e2..8726da6ce1 100644 --- a/src/openforms/registrations/contrib/json_dump/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -6,7 +6,12 @@ from zgw_consumers.client import build_client -from openforms.submissions.models import Submission +from openforms.formio.typing import Component +from openforms.submissions.models import ( + Submission, + SubmissionFileAttachment, + SubmissionValueVariable, +) from openforms.typing import JSONObject from ...base import BasePlugin # openforms.registrations.base @@ -34,15 +39,8 @@ def register_submission( if key in options["form_variables"] } - # Encode (base64) and add attachments to values dict if their form keys were specified in the - # form variables list - for attachment in submission.attachments: - if attachment.form_key not in options["form_variables"]: - continue - options["form_variables"].remove(attachment.form_key) - with attachment.content.open("rb") as f: - f.seek(0) - values[attachment.form_key] = base64.b64encode(f.read()).decode() + # Process attachments + self.process_variables(submission, values) # Generate schema # TODO: will be added in #4980. Hardcoded example for now. @@ -77,3 +75,69 @@ def register_submission( def check_config(self) -> None: # Config checks are not really relevant for this plugin right now pass + + @staticmethod + def process_variables(submission: Submission, values: JSONObject): + """Process variables. + + File components need special treatment, as we send the content of the file + encoded with base64, instead of the output from the serializer. + """ + state = submission.load_submission_value_variables_state() + + for key in values.keys(): + variable = state.variables.get(key) + if variable is None: + # None for static variables + continue + + component = get_component(variable) + if component is None or component["type"] != "file": + continue + + encoded_attachments = [ + { + "file_name": attachment.original_name, + "content": encode_attachment(attachment), + } + for attachment in submission.attachments + if attachment.form_key == key + ] + + match ( + multiple := component.get("multiple", False), + n_attachments := len(encoded_attachments) + ): + case False, 0: + values[key] = None + case False, 1: + values[key] = encoded_attachments[0] + case True, _: + values[key] = encoded_attachments + case _: + raise ValueError( + f"Combination of multiple ({multiple}) and number of " + f"attachments ({n_attachments}) is not allowed." + ) + +def encode_attachment(attachment: SubmissionFileAttachment) -> str: + """Encode an attachment using base64 + + :param attachment: Attachment to encode + :returns: Encoded base64 data as a string + """ + with attachment.content.open("rb") as f: + f.seek(0) + return base64.b64encode(f.read()).decode() + + +def get_component(variable: SubmissionValueVariable) -> Component: + """Get the component from a submission value variable. + + :param variable: SubmissionValueVariable + :return component: Component + """ + config_wrapper = variable.form_variable.form_definition.configuration_wrapper + component = config_wrapper.component_map[variable.key] + + return component 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 82b4fcd94b..514e7dceb2 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -116,3 +116,65 @@ def test_exception_raised_when_service_returns_unexpected_status_code(self): with self.assertRaises(RequestException): json_plugin.register_submission(submission, json_form_options) + + def test_multiple_file_uploads(self): + submission = SubmissionFactory.from_components( + [{"key": "file", "type": "file", "multiple": True}], + completed=True, + submitted_data={ + "file": [ + { + "url": "some://url", + "name": "file1.txt", + "type": "application/text", + "originalName": "file1.txt", + }, + { + "url": "some://url", + "name": "file2.txt", + "type": "application/text", + "originalName": "file2.txt", + } + ], + }, + ) + + SubmissionFileAttachmentFactory.create( + form_key="file", + submission_step=submission.submissionstep_set.get(), + file_name="file1.txt", + content_type="application/text", + content__data=b"This is example content.", + _component_configuration_path="components.2", + _component_data_path="file", + ) + + SubmissionFileAttachmentFactory.create( + form_key="file", + submission_step=submission.submissionstep_set.get(), + file_name="file2.txt", + content_type="application/text", + content__data=b"Content example is this.", + _component_configuration_path="components.2", + _component_data_path="file", + ) + + json_form_options = dict( + service=(ServiceFactory(api_root="http://localhost:80/")), + relative_api_endpoint="json_plugin", + form_variables=["file"], + ) + json_plugin = JSONDumpRegistration("json_registration_plugin") + set_submission_reference(submission) + + expected_values = { + "file": { + "file1.txt": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", # This is example content. + "file2.txt": "Q29udGVudCBleGFtcGxlIGlzIHRoaXMu", # Content example is this. + }, + } + + res = json_plugin.register_submission(submission, json_form_options) + res_json = res["api_response"] + + self.assertEqual(res_json["data"]["values"], expected_values) From 4233f5c8c90a1b97332e0f63a1e69aad353a21ae Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Wed, 15 Jan 2025 16:04:59 +0100 Subject: [PATCH 16/22] :alien: [#4908] Add validation for path We check if the path contains '..' to prevent possible path traversal attacks. For example: '..', 'foo/..', '../foo', and 'foo/../bar' are all not allowed --- .../registrations/contrib/json_dump/config.py | 14 +++++++++ .../registrations/contrib/json_dump/plugin.py | 10 ++++--- .../contrib/json_dump/tests/test_backend.py | 29 +++++++++++++++++++ .../contrib/json_dump/tests/test_config.py | 26 +++++++++++++++++ 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/openforms/registrations/contrib/json_dump/tests/test_config.py diff --git a/src/openforms/registrations/contrib/json_dump/config.py b/src/openforms/registrations/contrib/json_dump/config.py index 3446c9eba7..407d15e098 100644 --- a/src/openforms/registrations/contrib/json_dump/config.py +++ b/src/openforms/registrations/contrib/json_dump/config.py @@ -1,5 +1,6 @@ from typing import Required, TypedDict +from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -11,6 +12,18 @@ from openforms.utils.mixins import JsonSchemaSerializerMixin +def validate_path(v: str) -> None: + """Validate path by checking if it contains '..', which can lead to path traversal + attacks. + + :param v: path to validate + """ + if ".." in v: + raise ValidationError( + "Path cannot contain '..', as it can lead to path traversal attacks." + ) + + class JSONDumpOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): service = PrimaryKeyRelatedAsChoicesField( queryset=Service.objects.filter(api_type=APITypes.orc), @@ -29,6 +42,7 @@ class JSONDumpOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serialize allow_blank=True, required=False, default="", + validators=[validate_path], ) form_variables = serializers.ListField( child=FormioVariableKeyField(), diff --git a/src/openforms/registrations/contrib/json_dump/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py index 8726da6ce1..0e864c010c 100644 --- a/src/openforms/registrations/contrib/json_dump/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -1,6 +1,7 @@ import base64 import json +from django.core.exceptions import SuspiciousOperation from django.core.serializers.json import DjangoJSONEncoder from django.utils.translation import gettext_lazy as _ @@ -62,10 +63,10 @@ def register_submission( service = options["service"] submission.registration_result = result = {} with build_client(service) as client: - res = client.post( - options.get("relative_api_endpoint", ""), - json=data, - ) + if ".." in (path := options["relative_api_endpoint"]): + raise SuspiciousOperation("Possible path traversal detected") + + res = client.post(path, json=data) res.raise_for_status() result["api_response"] = res.json() @@ -120,6 +121,7 @@ def process_variables(submission: Submission, values: JSONObject): f"attachments ({n_attachments}) is not allowed." ) + def encode_attachment(attachment: SubmissionFileAttachment) -> str: """Encode an attachment using base64 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 514e7dceb2..dc637e86f5 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -1,6 +1,7 @@ from base64 import b64decode from pathlib import Path +from django.core.exceptions import SuspiciousOperation from django.test import TestCase from requests import RequestException @@ -178,3 +179,31 @@ def test_multiple_file_uploads(self): res_json = res["api_response"] self.assertEqual(res_json["data"]["values"], expected_values) + + def test_path_traversal_attack(self): + submission = SubmissionFactory.from_components( + [ + {"key": "firstName", "type": "textField"}, + {"key": "lastName", "type": "textfield"}, + ], + completed=True, + submitted_data={ + "firstName": "We Are", + "lastName": "Checking", + }, + bsn="123456789", + ) + + json_form_options = dict( + service=(ServiceFactory(api_root="http://localhost:80/")), + path="..", + form_variables=["firstName", "file", "auth_bsn"], + ) + json_plugin = JSONDumpRegistration("json_registration_plugin") + set_submission_reference(submission) + + for path in ("..", "../foo", "foo/..", "foo/../bar"): + with self.subTest(path): + json_form_options["path"] = path + with self.assertRaises(SuspiciousOperation): + json_plugin.register_submission(submission, json_form_options) diff --git a/src/openforms/registrations/contrib/json_dump/tests/test_config.py b/src/openforms/registrations/contrib/json_dump/tests/test_config.py new file mode 100644 index 0000000000..d499e4125d --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/test_config.py @@ -0,0 +1,26 @@ +from django.test import TestCase + +from openforms.appointments.contrib.qmatic.tests.factories import ServiceFactory + +from ..config import JSONDumpOptions, JSONDumpOptionsSerializer + + +class JSONDumpConfigTests(TestCase): + def test_serializer_raises_validation_error_on_path_traversal(self): + service = ServiceFactory.create(api_root="https://example.com/api/v2") + + data: JSONDumpOptions = { + "service": service.pk, + "path": "", + "variables": ["now"], + } + + # Ensuring that the options are valid in the first place + serializer = JSONDumpOptionsSerializer(data=data) + self.assertTrue(serializer.is_valid()) + + for path in ("..", "../foo", "foo/..", "foo/../bar"): + with self.subTest(path): + data["path"] = path + serializer = JSONDumpOptionsSerializer(data=data) + self.assertFalse(serializer.is_valid()) From 3ee85038b9a241f8d9c8d885c4ea4b5f86cb1a84 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Wed, 15 Jan 2025 16:05:32 +0100 Subject: [PATCH 17/22] :truck: [#4908] Rename relative_api_endpoint to path --- .../form_design/RegistrationFields.stories.js | 6 +++--- .../json_dump/JSONDumpOptionsForm.js | 8 ++++---- .../fields/{RelativeAPIEndpoint.js => Path.js} | 16 ++++++++-------- .../registrations/json_dump/fields/index.js | 4 ++-- .../variables/VariablesEditor.stories.js | 2 +- .../registrations/contrib/json_dump/config.py | 14 +++++--------- .../registrations/contrib/json_dump/plugin.py | 11 ++++++++--- .../contrib/json_dump/tests/test_backend.py | 8 ++++---- 8 files changed, 35 insertions(+), 34 deletions(-) rename src/openforms/js/components/admin/form_design/registrations/json_dump/fields/{RelativeAPIEndpoint.js => Path.js} (51%) 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 6f993e4d57..2e092faddb 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -406,7 +406,7 @@ export default { enum: [1, 2], enumNames: ['Service 1', 'Service 2'], }, - relativeApiEndpoint: { + path: { minLength: 1, title: 'Relative API endpoint', type: 'string', @@ -769,7 +769,7 @@ export const ConfiguredBackends = { backend: 'json_dump', options: { service: 1, - relativeApiEndpoint: 'example/endpoint', + path: 'example/endpoint', formVariables: [], }, }, @@ -1028,7 +1028,7 @@ export const JSONDump = { backend: 'json_dump', options: { service: 1, - relativeApiEndpoint: 'example/endpoint', + path: 'example/endpoint', formVariables: [], }, }, 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 ddb0018777..a23024e2ef 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 @@ -12,7 +12,7 @@ import { } from 'components/admin/forms/ValidationErrors'; import {getChoicesFromSchema} from 'utils/json-schema'; -import {FormVariablesSelect, RelativeAPIEndpoint, ServiceSelect} from './fields'; +import {FormVariablesSelect, Path, ServiceSelect} from './fields'; const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { const validationErrors = useContext(ValidationErrorContext); @@ -48,7 +48,7 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { } initialFormData={{ service: null, - relativeApiEndpoint: '', + path: '', formVariables: [], ...formData, }} @@ -58,7 +58,7 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => {
- +
@@ -79,7 +79,7 @@ JSONDumpOptionsForm.propTypes = { }).isRequired, formData: PropTypes.shape({ service: PropTypes.number, - relativeApiEndpoint: PropTypes.string, + path: PropTypes.string, // TODO-4908: might need to rename this to selectedFormVariables to avoid confusion or even // naming conflicts formVariables: PropTypes.arrayOf(PropTypes.string), diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/RelativeAPIEndpoint.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Path.js similarity index 51% rename from src/openforms/js/components/admin/form_design/registrations/json_dump/fields/RelativeAPIEndpoint.js rename to src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Path.js index 36a0700324..0f7ca403ff 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/RelativeAPIEndpoint.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Path.js @@ -6,22 +6,22 @@ import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; import {TextInput} from 'components/admin/forms/Inputs'; -const RelativeAPIEndpoint = () => { - const [fieldProps] = useField('relativeApiEndpoint'); +const Path = () => { + const [fieldProps] = useField('path'); return ( } helpText={ } > @@ -31,4 +31,4 @@ const RelativeAPIEndpoint = () => { ); }; -export default RelativeAPIEndpoint; +export default Path; 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 f0d78829b4..2b2cab17e5 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,5 @@ import FormVariablesSelect from './FormVariablesSelect'; -import RelativeAPIEndpoint from './RelativeAPIEndpoint'; +import Path from './Path'; import ServiceSelect from './ServiceSelect'; -export {FormVariablesSelect, RelativeAPIEndpoint, ServiceSelect}; +export {FormVariablesSelect, Path, ServiceSelect}; 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 7833fa768b..26c4ff6b04 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 @@ -663,7 +663,7 @@ export const WithJSONDumpRegistrationBackend = { name: 'JSON dump registration', options: { service: 2, - relativeApiEndpoint: 'test', + path: 'test', formVariables: ['aSingleFile', 'now'], }, }, diff --git a/src/openforms/registrations/contrib/json_dump/config.py b/src/openforms/registrations/contrib/json_dump/config.py index 407d15e098..0b30938e8c 100644 --- a/src/openforms/registrations/contrib/json_dump/config.py +++ b/src/openforms/registrations/contrib/json_dump/config.py @@ -1,4 +1,4 @@ -from typing import Required, TypedDict +from typing import TypedDict from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -31,14 +31,10 @@ class JSONDumpOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serialize help_text=_("Which service to use."), required=True, ) - # TODO-4908: show the complete API endpoint as a (tooltip) hint after user entry? - # Might be a front-end thing... - relative_api_endpoint = serializers.CharField( + path = serializers.CharField( max_length=255, - label=_("Relative API endpoint"), - help_text=_( - "The API endpoint to send the data to (relative to the service API root)." - ), + label=_("Path"), + help_text=_("Path relative to the Service API root."), allow_blank=True, required=False, default="", @@ -64,5 +60,5 @@ class JSONDumpOptions(TypedDict): """ service: Service - relative_api_endpoint: str + path: str form_variables: list[str] diff --git a/src/openforms/registrations/contrib/json_dump/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py index 0e864c010c..18020edf08 100644 --- a/src/openforms/registrations/contrib/json_dump/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -14,6 +14,7 @@ SubmissionValueVariable, ) from openforms.typing import JSONObject +from openforms.variables.constants import FormVariableSources from ...base import BasePlugin # openforms.registrations.base from ...registry import register # openforms.registrations.registry @@ -63,7 +64,7 @@ def register_submission( service = options["service"] submission.registration_result = result = {} with build_client(service) as client: - if ".." in (path := options["relative_api_endpoint"]): + if ".." in (path := options["path"]): raise SuspiciousOperation("Possible path traversal detected") res = client.post(path, json=data) @@ -88,8 +89,12 @@ def process_variables(submission: Submission, values: JSONObject): for key in values.keys(): variable = state.variables.get(key) - if variable is None: - # None for static variables + if ( + variable is None + or variable.form_variable.source == FormVariableSources.user_defined + ): + # None for static variables, and processing user defined variables is + # not relevant here continue component = get_component(variable) 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 dc637e86f5..45777a314d 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -57,7 +57,7 @@ def test_submission_with_json_dump_backend(self): json_form_options = dict( service=(ServiceFactory(api_root="http://localhost:80/")), - relative_api_endpoint="json_plugin", + path="json_plugin", form_variables=["firstName", "file", "auth_bsn"], ) json_plugin = JSONDumpRegistration("json_registration_plugin") @@ -109,7 +109,7 @@ def test_exception_raised_when_service_returns_unexpected_status_code(self): json_form_options = dict( service=(ServiceFactory(api_root="http://localhost:80/")), - relative_api_endpoint="fake_endpoint", + path="fake_endpoint", form_variables=["firstName", "auth_bsn"], ) json_plugin = JSONDumpRegistration("json_registration_plugin") @@ -135,7 +135,7 @@ def test_multiple_file_uploads(self): "name": "file2.txt", "type": "application/text", "originalName": "file2.txt", - } + }, ], }, ) @@ -162,7 +162,7 @@ def test_multiple_file_uploads(self): json_form_options = dict( service=(ServiceFactory(api_root="http://localhost:80/")), - relative_api_endpoint="json_plugin", + path="json_plugin", form_variables=["file"], ) json_plugin = JSONDumpRegistration("json_registration_plugin") From a48c9228b7d911e1086170331c12adfd94d16859 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Wed, 15 Jan 2025 16:36:26 +0100 Subject: [PATCH 18/22] :truck: [#4908] Rename form_variables to variables --- .../form_design/RegistrationFields.stories.js | 6 +++--- .../json_dump/JSONDumpOptionsForm.js | 16 +++++++--------- .../json_dump/JSONDumpSummaryHandler.js | 4 ++-- .../JSONDumpVariableConfigurationEditor.js | 12 ++++++------ .../json_dump/fields/FormVariablesSelect.js | 14 +++++++------- .../variables/VariablesEditor.stories.js | 2 +- .../registrations/contrib/json_dump/config.py | 10 ++++------ .../registrations/contrib/json_dump/plugin.py | 2 +- .../contrib/json_dump/tests/test_backend.py | 10 +++++----- 9 files changed, 36 insertions(+), 40 deletions(-) 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 2e092faddb..bdd11f7084 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -411,7 +411,7 @@ export default { title: 'Relative API endpoint', type: 'string', }, - formVariables: { + variables: { type: 'array', title: 'List of form variables', items: { @@ -770,7 +770,7 @@ export const ConfiguredBackends = { options: { service: 1, path: 'example/endpoint', - formVariables: [], + variables: [], }, }, ], @@ -1029,7 +1029,7 @@ export const JSONDump = { options: { service: 1, path: 'example/endpoint', - formVariables: [], + variables: [], }, }, ], 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 a23024e2ef..e27ca5fa5b 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 @@ -22,11 +22,11 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { const formContext = useContext(FormContext); const formVariables = formContext.formVariables ?? []; const staticVariables = formContext.staticVariables ?? []; - const allFormVariables = staticVariables.concat(formVariables); + const allVariables = staticVariables.concat(formVariables); - const formVariableOptions = []; - for (const formVariable of allFormVariables) { - formVariableOptions.push({value: formVariable.key, label: formVariable.name}); + const variableOptions = []; + for (const variable of allVariables) { + variableOptions.push({value: variable.key, label: variable.name}); } // Create service options @@ -49,7 +49,7 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { initialFormData={{ service: null, path: '', - formVariables: [], + variables: [], ...formData, }} onSubmit={values => onChange({formData: values})} @@ -59,7 +59,7 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => {
- +
@@ -80,9 +80,7 @@ JSONDumpOptionsForm.propTypes = { formData: PropTypes.shape({ service: PropTypes.number, path: PropTypes.string, - // TODO-4908: might need to rename this to selectedFormVariables to avoid confusion or even - // naming conflicts - formVariables: PropTypes.arrayOf(PropTypes.string), + variables: 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 4c971e5ce2..0c74f9e4d8 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 @@ -4,7 +4,7 @@ import React from 'react'; import {IconNo, IconYes} from 'components/admin/BooleanIcons'; const JSONDumpSummaryHandler = ({variable, backendOptions}) => { - const isIncluded = backendOptions.formVariables.includes(variable.key); + const isIncluded = backendOptions.variables.includes(variable.key); return isIncluded ? : ; }; @@ -14,7 +14,7 @@ JSONDumpSummaryHandler.propTypes = { key: PropTypes.string.isRequired, }).isRequired, backendOptions: PropTypes.shape({ - formVariables: PropTypes.arrayOf(PropTypes.string).isRequired, + variables: 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 802031ebe3..31bb9b8fea 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,10 +9,10 @@ import {Checkbox} from 'components/admin/forms/Inputs'; const JSONDumpVariableConfigurationEditor = ({variable}) => { const { - values: {formVariables = []}, + values: {variables = []}, setFieldValue, } = useFormikContext(); - const isIncluded = formVariables.includes(variable.key); + const isIncluded = variables.includes(variable.key); return ( @@ -34,10 +34,10 @@ const JSONDumpVariableConfigurationEditor = ({variable}) => { checked={isIncluded} onChange={event => { const shouldBeIncluded = event.target.checked; - const newFormVariables = shouldBeIncluded - ? [...formVariables, variable.key] // add the variable to the array - : formVariables.filter(key => key !== variable.key); // remove the variable from the array - setFieldValue('formVariables', newFormVariables); + 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', variables); }} />
diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js index ea3102f5b1..7ac5993b2b 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js @@ -8,7 +8,7 @@ import FormRow from 'components/admin/forms/FormRow'; import ReactSelect from 'components/admin/forms/ReactSelect'; const FormVariablesSelect = ({options}) => { - const [fieldProps, , {setValue}] = useField('formVariables'); + const [fieldProps, , {setValue}] = useField('variables'); const values = []; if (fieldProps.value && fieldProps.value.length) { @@ -23,24 +23,24 @@ const FormVariablesSelect = ({options}) => { return ( } helpText={ } required noManageChildProps > Date: Thu, 16 Jan 2025 09:48:41 +0100 Subject: [PATCH 19/22] :recycle: [#4908] Use VariableSelection component Now supports multi selection, so we can use it here --- .../json_dump/JSONDumpOptionsForm.js | 18 +---- .../JSONDumpVariableConfigurationEditor.js | 2 +- .../json_dump/fields/FormVariablesSelect.js | 66 ------------------- .../json_dump/fields/Variables.js | 43 ++++++++++++ .../registrations/json_dump/fields/index.js | 4 +- 5 files changed, 49 insertions(+), 84 deletions(-) delete mode 100644 src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js 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 e27ca5fa5b..987e8e09c0 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 @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, {useContext} from 'react'; import {FormattedMessage} from 'react-intl'; -import {FormContext} from 'components/admin/form_design/Context'; import Fieldset from 'components/admin/forms/Fieldset'; import ModalOptionsConfiguration from 'components/admin/forms/ModalOptionsConfiguration'; import { @@ -12,23 +11,12 @@ import { } from 'components/admin/forms/ValidationErrors'; import {getChoicesFromSchema} from 'utils/json-schema'; -import {FormVariablesSelect, Path, ServiceSelect} from './fields'; +import {Path, ServiceSelect, Variables} from './fields'; const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { const validationErrors = useContext(ValidationErrorContext); const relevantErrors = filterErrors(name, validationErrors); - // Get form variables and create form variable options - const formContext = useContext(FormContext); - const formVariables = formContext.formVariables ?? []; - const staticVariables = formContext.staticVariables ?? []; - const allVariables = staticVariables.concat(formVariables); - - const variableOptions = []; - for (const variable of allVariables) { - variableOptions.push({value: variable.key, label: variable.name}); - } - // Create service options const {service} = schema.properties; const serviceOptions = getChoicesFromSchema(service.enum, service.enumNames).map( @@ -53,13 +41,13 @@ const JSONDumpOptionsForm = ({name, label, schema, formData, onChange}) => { ...formData, }} onSubmit={values => onChange({formData: values})} - modalSize="medium" + modalSize="large" >
- +
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 31bb9b8fea..75f5b4077c 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 @@ -37,7 +37,7 @@ const JSONDumpVariableConfigurationEditor = ({variable}) => { 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', variables); + setFieldValue('variables', newVariables); }} />
diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js deleted file mode 100644 index 7ac5993b2b..0000000000 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/FormVariablesSelect.js +++ /dev/null @@ -1,66 +0,0 @@ -import {useField} from 'formik'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import Field from 'components/admin/forms/Field'; -import FormRow from 'components/admin/forms/FormRow'; -import ReactSelect from 'components/admin/forms/ReactSelect'; - -const FormVariablesSelect = ({options}) => { - const [fieldProps, , {setValue}] = useField('variables'); - - const values = []; - if (fieldProps.value && fieldProps.value.length) { - fieldProps.value.forEach(item => { - const selectedOption = options.find(option => option.value === item); - if (selectedOption) { - values.push(selectedOption); - } - }); - } - - return ( - - - } - helpText={ - - } - required - noManageChildProps - > - { - setValue(newValue.map(item => item.value)); - }} - /> - - - ); -}; - -FormVariablesSelect.propTypes = { - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - label: PropTypes.node.isRequired, - }) - ).isRequired, -}; - -export default FormVariablesSelect; diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js new file mode 100644 index 0000000000..2d934a3ced --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js @@ -0,0 +1,43 @@ +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'; + +const Variables = () => { + const [fieldProps] = useField('variables'); + + return ( + + + } + helpText={ + + } + required + noManageChildProps + > + + + + ); +}; + +export default Variables; 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 2b2cab17e5..ab0ba696a9 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,5 @@ -import FormVariablesSelect from './FormVariablesSelect'; import Path from './Path'; import ServiceSelect from './ServiceSelect'; +import Variables from './Variables'; -export {FormVariablesSelect, Path, ServiceSelect}; +export {Path, ServiceSelect, Variables}; From 13928d09368790e234b96353646d49fa3a832880 Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 16 Jan 2025 11:16:50 +0100 Subject: [PATCH 20/22] :white_check_mark: [#4908] Add and update tests In the JSON dump docker app, explicitly load the received data before sending again. This prevents the data from being interpreted as a string instead of a JSON object. --- docker/json-dump/app.py | 7 +- ...ervice_returns_unexpected_status_code.yaml | 19 +- ...ckendTests.test_multiple_file_uploads.yaml | 56 +++++ ..._upload_for_multiple_files_component.yaml} | 23 +-- ...file_upload_for_single_file_component.yaml | 51 +++++ ...e_upload_for_multiple_files_component.yaml | 54 +++++ ...ckendTests.test_submission_happy_flow.yaml | 56 +++++ .../contrib/json_dump/tests/test_backend.py | 191 ++++++++++++++---- 8 files changed, 391 insertions(+), 66 deletions(-) create mode 100644 src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_multiple_file_uploads.yaml rename src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/{JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml => JSONDumpBackendTests.test_no_file_upload_for_multiple_files_component.yaml} (60%) create mode 100644 src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_no_file_upload_for_single_file_component.yaml create mode 100644 src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_one_file_upload_for_multiple_files_component.yaml create mode 100644 src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_happy_flow.yaml diff --git a/docker/json-dump/app.py b/docker/json-dump/app.py index e2e8b08bcb..963c91e03f 100644 --- a/docker/json-dump/app.py +++ b/docker/json-dump/app.py @@ -1,5 +1,6 @@ -from flask import Flask, jsonify, request +import json +from flask import Flask, jsonify, request app = Flask(__name__) @@ -7,8 +8,10 @@ def json_plugin_post(): data = request.get_json() + app.logger.info(f"Data received: {data}") + message = "No data" if data is None else "Data received" - return jsonify({"message": message, "data": data}), 201 + return jsonify({"message": message, "data": json.loads(data)}), 201 @app.route("/test_connection", methods=["GET"]) diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml index 781d459570..874b327225 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_exception_raised_when_service_returns_unexpected_status_code.yaml @@ -1,22 +1,23 @@ interactions: - request: - body: '{"values": {"firstName": "We Are", "auth_bsn": "123456789"}, "schema": - {"$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", - "properties": {"static_var_1": {"type": "string", "pattern": "^cool_pattern$"}, - "form_var_1": {"type": "string"}, "form_var_2": {"type": "string"}, "attachment": - {"type": "string", "contentEncoding": "base64"}}, "required": ["static_var_1", - "form_var_1", "form_var_2"], "additionalProperties": false}}' + body: '"{\"values\": {\"auth_bsn\": \"123456789\", \"firstName\": \"We Are\"}, + \"schema\": {\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", + \"type\": \"object\", \"properties\": {\"static_var_1\": {\"type\": \"string\", + \"pattern\": \"^cool_pattern$\"}, \"form_var_1\": {\"type\": \"string\"}, \"form_var_2\": + {\"type\": \"string\"}, \"attachment\": {\"type\": \"string\", \"contentEncoding\": + \"base64\"}}, \"required\": [\"static_var_1\", \"form_var_1\", \"form_var_2\"], + \"additionalProperties\": false}}"' headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate, br Authorization: - - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3MzU4MjY2MzksImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.8PuPAIY6PI3_g4edfqzFFbHNldYxxRIBjPuAh-p00xk + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3Mzc0Njk5MDAsImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.VaaQtoX7c3ac2_Zf8GPvB8fAMYfmJft53c7qXNBY_lk Connection: - keep-alive Content-Length: - - '450' + - '516' Content-Type: - application/json User-Agent: @@ -45,7 +46,7 @@ interactions: Content-Type: - text/html; charset=utf-8 Date: - - Thu, 02 Jan 2025 14:03:59 GMT + - Tue, 21 Jan 2025 14:31:40 GMT Server: - Werkzeug/3.1.3 Python/3.12.8 status: diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_multiple_file_uploads.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_multiple_file_uploads.yaml new file mode 100644 index 0000000000..da0de50808 --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_multiple_file_uploads.yaml @@ -0,0 +1,56 @@ +interactions: +- request: + body: '"{\"values\": {\"file\": [{\"file_name\": \"file1.txt\", \"content\": \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\"}, + {\"file_name\": \"file2.txt\", \"content\": \"Q29udGVudCBleGFtcGxlIGlzIHRoaXMu\"}]}, + \"schema\": {\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", + \"type\": \"object\", \"properties\": {\"static_var_1\": {\"type\": \"string\", + \"pattern\": \"^cool_pattern$\"}, \"form_var_1\": {\"type\": \"string\"}, \"form_var_2\": + {\"type\": \"string\"}, \"attachment\": {\"type\": \"string\", \"contentEncoding\": + \"base64\"}}, \"required\": [\"static_var_1\", \"form_var_1\", \"form_var_2\"], + \"additionalProperties\": false}}"' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3Mzc0Njk5MDAsImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.VaaQtoX7c3ac2_Zf8GPvB8fAMYfmJft53c7qXNBY_lk + Connection: + - keep-alive + Content-Length: + - '638' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost/json_plugin + response: + body: + string: "{\n \"data\": {\n \"schema\": {\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n + \ \"additionalProperties\": false,\n \"properties\": {\n \"attachment\": + {\n \"contentEncoding\": \"base64\",\n \"type\": \"string\"\n + \ },\n \"form_var_1\": {\n \"type\": \"string\"\n },\n + \ \"form_var_2\": {\n \"type\": \"string\"\n },\n \"static_var_1\": + {\n \"pattern\": \"^cool_pattern$\",\n \"type\": \"string\"\n + \ }\n },\n \"required\": [\n \"static_var_1\",\n \"form_var_1\",\n + \ \"form_var_2\"\n ],\n \"type\": \"object\"\n },\n \"values\": + {\n \"file\": [\n {\n \"content\": \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\",\n + \ \"file_name\": \"file1.txt\"\n },\n {\n \"content\": + \"Q29udGVudCBleGFtcGxlIGlzIHRoaXMu\",\n \"file_name\": \"file2.txt\"\n + \ }\n ]\n }\n },\n \"message\": \"Data received\"\n}\n" + headers: + Connection: + - close + Content-Length: + - '923' + Content-Type: + - application/json + Date: + - Tue, 21 Jan 2025 14:31:40 GMT + Server: + - Werkzeug/3.1.3 Python/3.12.8 + status: + code: 201 + message: CREATED +version: 1 diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_no_file_upload_for_multiple_files_component.yaml similarity index 60% rename from src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml rename to src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_no_file_upload_for_multiple_files_component.yaml index d2294549fc..af4ae5be94 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_with_json_dump_backend.yaml +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_no_file_upload_for_multiple_files_component.yaml @@ -1,22 +1,22 @@ interactions: - request: - body: '{"values": {"file": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", "firstName": "We - Are", "auth_bsn": "123456789"}, "schema": {"$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", "properties": {"static_var_1": {"type": "string", "pattern": - "^cool_pattern$"}, "form_var_1": {"type": "string"}, "form_var_2": {"type": - "string"}, "attachment": {"type": "string", "contentEncoding": "base64"}}, "required": - ["static_var_1", "form_var_1", "form_var_2"], "additionalProperties": false}}' + body: '"{\"values\": {\"file\": []}, \"schema\": {\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", + \"type\": \"object\", \"properties\": {\"static_var_1\": {\"type\": \"string\", + \"pattern\": \"^cool_pattern$\"}, \"form_var_1\": {\"type\": \"string\"}, \"form_var_2\": + {\"type\": \"string\"}, \"attachment\": {\"type\": \"string\", \"contentEncoding\": + \"base64\"}}, \"required\": [\"static_var_1\", \"form_var_1\", \"form_var_2\"], + \"additionalProperties\": false}}"' headers: Accept: - '*/*' Accept-Encoding: - gzip, deflate, br Authorization: - - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3MzU4MjY2MzksImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.8PuPAIY6PI3_g4edfqzFFbHNldYxxRIBjPuAh-p00xk + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3Mzc0Njk5MDAsImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.VaaQtoX7c3ac2_Zf8GPvB8fAMYfmJft53c7qXNBY_lk Connection: - keep-alive Content-Length: - - '494' + - '474' Content-Type: - application/json User-Agent: @@ -33,17 +33,16 @@ interactions: {\n \"pattern\": \"^cool_pattern$\",\n \"type\": \"string\"\n \ }\n },\n \"required\": [\n \"static_var_1\",\n \"form_var_1\",\n \ \"form_var_2\"\n ],\n \"type\": \"object\"\n },\n \"values\": - {\n \"auth_bsn\": \"123456789\",\n \"file\": \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\",\n - \ \"firstName\": \"We Are\"\n }\n },\n \"message\": \"Data received\"\n}\n" + {\n \"file\": []\n }\n },\n \"message\": \"Data received\"\n}\n" headers: Connection: - close Content-Length: - - '783' + - '691' Content-Type: - application/json Date: - - Thu, 02 Jan 2025 14:03:59 GMT + - Tue, 21 Jan 2025 14:31:40 GMT Server: - Werkzeug/3.1.3 Python/3.12.8 status: diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_no_file_upload_for_single_file_component.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_no_file_upload_for_single_file_component.yaml new file mode 100644 index 0000000000..55f07cc3d8 --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_no_file_upload_for_single_file_component.yaml @@ -0,0 +1,51 @@ +interactions: +- request: + body: '"{\"values\": {\"file\": null}, \"schema\": {\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", + \"type\": \"object\", \"properties\": {\"static_var_1\": {\"type\": \"string\", + \"pattern\": \"^cool_pattern$\"}, \"form_var_1\": {\"type\": \"string\"}, \"form_var_2\": + {\"type\": \"string\"}, \"attachment\": {\"type\": \"string\", \"contentEncoding\": + \"base64\"}}, \"required\": [\"static_var_1\", \"form_var_1\", \"form_var_2\"], + \"additionalProperties\": false}}"' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3Mzc0Njk5MDAsImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.VaaQtoX7c3ac2_Zf8GPvB8fAMYfmJft53c7qXNBY_lk + Connection: + - keep-alive + Content-Length: + - '476' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost/json_plugin + response: + body: + string: "{\n \"data\": {\n \"schema\": {\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n + \ \"additionalProperties\": false,\n \"properties\": {\n \"attachment\": + {\n \"contentEncoding\": \"base64\",\n \"type\": \"string\"\n + \ },\n \"form_var_1\": {\n \"type\": \"string\"\n },\n + \ \"form_var_2\": {\n \"type\": \"string\"\n },\n \"static_var_1\": + {\n \"pattern\": \"^cool_pattern$\",\n \"type\": \"string\"\n + \ }\n },\n \"required\": [\n \"static_var_1\",\n \"form_var_1\",\n + \ \"form_var_2\"\n ],\n \"type\": \"object\"\n },\n \"values\": + {\n \"file\": null\n }\n },\n \"message\": \"Data received\"\n}\n" + headers: + Connection: + - close + Content-Length: + - '693' + Content-Type: + - application/json + Date: + - Tue, 21 Jan 2025 14:31:40 GMT + Server: + - Werkzeug/3.1.3 Python/3.12.8 + status: + code: 201 + message: CREATED +version: 1 diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_one_file_upload_for_multiple_files_component.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_one_file_upload_for_multiple_files_component.yaml new file mode 100644 index 0000000000..b129859841 --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_one_file_upload_for_multiple_files_component.yaml @@ -0,0 +1,54 @@ +interactions: +- request: + body: '"{\"values\": {\"file\": [{\"file_name\": \"file1.txt\", \"content\": \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\"}]}, + \"schema\": {\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", + \"type\": \"object\", \"properties\": {\"static_var_1\": {\"type\": \"string\", + \"pattern\": \"^cool_pattern$\"}, \"form_var_1\": {\"type\": \"string\"}, \"form_var_2\": + {\"type\": \"string\"}, \"attachment\": {\"type\": \"string\", \"contentEncoding\": + \"base64\"}}, \"required\": [\"static_var_1\", \"form_var_1\", \"form_var_2\"], + \"additionalProperties\": false}}"' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3Mzc0Njk5MDAsImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.VaaQtoX7c3ac2_Zf8GPvB8fAMYfmJft53c7qXNBY_lk + Connection: + - keep-alive + Content-Length: + - '555' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost/json_plugin + response: + body: + string: "{\n \"data\": {\n \"schema\": {\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n + \ \"additionalProperties\": false,\n \"properties\": {\n \"attachment\": + {\n \"contentEncoding\": \"base64\",\n \"type\": \"string\"\n + \ },\n \"form_var_1\": {\n \"type\": \"string\"\n },\n + \ \"form_var_2\": {\n \"type\": \"string\"\n },\n \"static_var_1\": + {\n \"pattern\": \"^cool_pattern$\",\n \"type\": \"string\"\n + \ }\n },\n \"required\": [\n \"static_var_1\",\n \"form_var_1\",\n + \ \"form_var_2\"\n ],\n \"type\": \"object\"\n },\n \"values\": + {\n \"file\": [\n {\n \"content\": \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\",\n + \ \"file_name\": \"file1.txt\"\n }\n ]\n }\n },\n + \ \"message\": \"Data received\"\n}\n" + headers: + Connection: + - close + Content-Length: + - '810' + Content-Type: + - application/json + Date: + - Tue, 21 Jan 2025 14:31:40 GMT + Server: + - Werkzeug/3.1.3 Python/3.12.8 + status: + code: 201 + message: CREATED +version: 1 diff --git a/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_happy_flow.yaml b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_happy_flow.yaml new file mode 100644 index 0000000000..7eb76f66a9 --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/files/vcr_cassettes/JSONDumpBackendTests/JSONDumpBackendTests.test_submission_happy_flow.yaml @@ -0,0 +1,56 @@ +interactions: +- request: + body: '"{\"values\": {\"auth_bsn\": \"123456789\", \"firstName\": \"We Are\", + \"file\": {\"file_name\": \"test_file.txt\", \"content\": \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\"}}, + \"schema\": {\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", + \"type\": \"object\", \"properties\": {\"static_var_1\": {\"type\": \"string\", + \"pattern\": \"^cool_pattern$\"}, \"form_var_1\": {\"type\": \"string\"}, \"form_var_2\": + {\"type\": \"string\"}, \"attachment\": {\"type\": \"string\", \"contentEncoding\": + \"base64\"}}, \"required\": [\"static_var_1\", \"form_var_1\", \"form_var_2\"], + \"additionalProperties\": false}}"' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIiLCJpYXQiOjE3Mzc0Njk5MDAsImNsaWVudF9pZCI6IiIsInVzZXJfaWQiOiIiLCJ1c2VyX3JlcHJlc2VudGF0aW9uIjoiIn0.VaaQtoX7c3ac2_Zf8GPvB8fAMYfmJft53c7qXNBY_lk + Connection: + - keep-alive + Content-Length: + - '613' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost/json_plugin + response: + body: + string: "{\n \"data\": {\n \"schema\": {\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n + \ \"additionalProperties\": false,\n \"properties\": {\n \"attachment\": + {\n \"contentEncoding\": \"base64\",\n \"type\": \"string\"\n + \ },\n \"form_var_1\": {\n \"type\": \"string\"\n },\n + \ \"form_var_2\": {\n \"type\": \"string\"\n },\n \"static_var_1\": + {\n \"pattern\": \"^cool_pattern$\",\n \"type\": \"string\"\n + \ }\n },\n \"required\": [\n \"static_var_1\",\n \"form_var_1\",\n + \ \"form_var_2\"\n ],\n \"type\": \"object\"\n },\n \"values\": + {\n \"auth_bsn\": \"123456789\",\n \"file\": {\n \"content\": + \"VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu\",\n \"file_name\": \"test_file.txt\"\n + \ },\n \"firstName\": \"We Are\"\n }\n },\n \"message\": \"Data + received\"\n}\n" + headers: + Connection: + - close + Content-Length: + - '850' + Content-Type: + - application/json + Date: + - Tue, 21 Jan 2025 14:31:40 GMT + Server: + - Werkzeug/3.1.3 Python/3.12.8 + status: + code: 201 + message: CREATED +version: 1 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 f2dbd51a3e..c9157efc74 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -7,13 +7,13 @@ from requests import RequestException from zgw_consumers.test.factories import ServiceFactory -from openforms.submissions.public_references import set_submission_reference from openforms.submissions.tests.factories import ( SubmissionFactory, SubmissionFileAttachmentFactory, ) from openforms.utils.tests.vcr import OFVCRMixin +from ..config import JSONDumpOptions from ..plugin import JSONDumpRegistration VCR_TEST_FILES = Path(__file__).parent / "files" @@ -22,10 +22,14 @@ class JSONDumpBackendTests(OFVCRMixin, TestCase): VCR_TEST_FILES = VCR_TEST_FILES - def test_submission_with_json_dump_backend(self): + @classmethod + def setUpTestData(cls): + cls.service = ServiceFactory.create(api_root="http://localhost:80/") + + def test_submission_happy_flow(self): submission = SubmissionFactory.from_components( [ - {"key": "firstName", "type": "textField"}, + {"key": "firstName", "type": "textfield"}, {"key": "lastName", "type": "textfield"}, {"key": "file", "type": "file"}, ], @@ -43,32 +47,36 @@ def test_submission_with_json_dump_backend(self): ], }, bsn="123456789", + with_public_registration_reference=True, ) SubmissionFileAttachmentFactory.create( form_key="file", submission_step=submission.submissionstep_set.get(), file_name="test_file.txt", + original_name="test_file.txt", content_type="application/text", content__data=b"This is example content.", _component_configuration_path="components.2", _component_data_path="file", ) - json_form_options = dict( - service=(ServiceFactory(api_root="http://localhost:80/")), - path="json_plugin", - variables=["firstName", "file", "auth_bsn"], - ) + options: JSONDumpOptions = { + "service": self.service, + "path": "json_plugin", + "variables": ["firstName", "file", "auth_bsn"], + } json_plugin = JSONDumpRegistration("json_registration_plugin") - set_submission_reference(submission) expected_response = { # Note that `lastName` is not included here as it wasn't specified in the variables "data": { "values": { "auth_bsn": "123456789", - "file": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", # Content of the attachment encoded using base64 + "file": { + "file_name": "test_file.txt", + "content": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", + }, "firstName": "We Are", }, "schema": { @@ -87,36 +95,37 @@ def test_submission_with_json_dump_backend(self): "message": "Data received", } - res = json_plugin.register_submission(submission, json_form_options) - res_json = res["api_response"].json() + result = json_plugin.register_submission(submission, options) - self.assertEqual(res_json, expected_response) + self.assertEqual(result["api_response"], expected_response) with self.subTest("attachment content encoded"): - decoded_content = b64decode(res_json["data"]["values"]["file"]) + decoded_content = b64decode( + result["api_response"]["data"]["values"]["file"]["content"] + ) self.assertEqual(decoded_content, b"This is example content.") def test_exception_raised_when_service_returns_unexpected_status_code(self): submission = SubmissionFactory.from_components( [ - {"key": "firstName", "type": "textField"}, + {"key": "firstName", "type": "textfield"}, {"key": "lastName", "type": "textfield"}, ], completed=True, submitted_data={"firstName": "We Are", "lastName": "Checking"}, bsn="123456789", + with_public_registration_reference=True, ) - json_form_options = dict( - service=(ServiceFactory(api_root="http://localhost:80/")), - path="fake_endpoint", - variables=["firstName", "auth_bsn"], - ) + options: JSONDumpOptions = { + "service": self.service, + "path": "fake_endpoint", + "variables": ["firstName", "auth_bsn"], + } json_plugin = JSONDumpRegistration("json_registration_plugin") - set_submission_reference(submission) with self.assertRaises(RequestException): - json_plugin.register_submission(submission, json_form_options) + json_plugin.register_submission(submission, options) def test_multiple_file_uploads(self): submission = SubmissionFactory.from_components( @@ -138,12 +147,14 @@ def test_multiple_file_uploads(self): }, ], }, + with_public_registration_reference=True, ) SubmissionFileAttachmentFactory.create( form_key="file", submission_step=submission.submissionstep_set.get(), file_name="file1.txt", + original_name="file1.txt", content_type="application/text", content__data=b"This is example content.", _component_configuration_path="components.2", @@ -154,36 +165,130 @@ def test_multiple_file_uploads(self): form_key="file", submission_step=submission.submissionstep_set.get(), file_name="file2.txt", + original_name="file2.txt", content_type="application/text", content__data=b"Content example is this.", _component_configuration_path="components.2", _component_data_path="file", ) - json_form_options = dict( - service=(ServiceFactory(api_root="http://localhost:80/")), - path="json_plugin", - variables=["file"], + options: JSONDumpOptions = { + "service": self.service, + "path": "json_plugin", + "variables": ["file"], + } + json_plugin = JSONDumpRegistration("json_registration_plugin") + + expected_files = [ + { + "file_name": "file1.txt", + "content": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", # This is example content. + }, + { + "file_name": "file2.txt", + "content": "Q29udGVudCBleGFtcGxlIGlzIHRoaXMu", # Content example is this. + }, + ] + + result = json_plugin.register_submission(submission, options) + + self.assertEqual( + result["api_response"]["data"]["values"]["file"], expected_files ) + + def test_one_file_upload_for_multiple_files_component(self): + submission = SubmissionFactory.from_components( + [{"key": "file", "type": "file", "multiple": True}], + completed=True, + submitted_data={ + "file": [ + { + "url": "some://url", + "name": "file1.txt", + "type": "application/text", + "originalName": "file1.txt", + } + ], + }, + with_public_registration_reference=True, + ) + + SubmissionFileAttachmentFactory.create( + form_key="file", + submission_step=submission.submissionstep_set.get(), + file_name="file1.txt", + original_name="file1.txt", + content_type="application/text", + content__data=b"This is example content.", + _component_configuration_path="components.2", + _component_data_path="file", + ) + + options: JSONDumpOptions = { + "service": self.service, + "path": "json_plugin", + "variables": ["file"], + } + json_plugin = JSONDumpRegistration("json_registration_plugin") + + result = json_plugin.register_submission(submission, options) + + self.assertEqual( + result["api_response"]["data"]["values"]["file"], + [ + { + "file_name": "file1.txt", + "content": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", # This is example content. + } + ], + ) + + def test_no_file_upload_for_single_file_component(self): + submission = SubmissionFactory.from_components( + [{"key": "file", "type": "file"}], + completed=True, + submitted_data={ + "file": [], + }, + with_public_registration_reference=True, + ) + + options: JSONDumpOptions = { + "service": self.service, + "path": "json_plugin", + "variables": ["file"], + } json_plugin = JSONDumpRegistration("json_registration_plugin") - set_submission_reference(submission) - expected_values = { - "file": { - "file1.txt": "VGhpcyBpcyBleGFtcGxlIGNvbnRlbnQu", # This is example content. - "file2.txt": "Q29udGVudCBleGFtcGxlIGlzIHRoaXMu", # Content example is this. + result = json_plugin.register_submission(submission, options) + + self.assertEqual(result["api_response"]["data"]["values"]["file"], None) + + def test_no_file_upload_for_multiple_files_component(self): + submission = SubmissionFactory.from_components( + [{"key": "file", "type": "file", "multiple": True}], + completed=True, + submitted_data={ + "file": [], }, + with_public_registration_reference=True, + ) + + options: JSONDumpOptions = { + "service": self.service, + "path": "json_plugin", + "variables": ["file"], } + json_plugin = JSONDumpRegistration("json_registration_plugin") - res = json_plugin.register_submission(submission, json_form_options) - res_json = res["api_response"] + result = json_plugin.register_submission(submission, options) - self.assertEqual(res_json["data"]["values"], expected_values) + self.assertEqual(result["api_response"]["data"]["values"]["file"], []) def test_path_traversal_attack(self): submission = SubmissionFactory.from_components( [ - {"key": "firstName", "type": "textField"}, + {"key": "firstName", "type": "textfield"}, {"key": "lastName", "type": "textfield"}, ], completed=True, @@ -192,18 +297,18 @@ def test_path_traversal_attack(self): "lastName": "Checking", }, bsn="123456789", + with_public_registration_reference=True, ) - json_form_options = dict( - service=(ServiceFactory(api_root="http://localhost:80/")), - path="..", - variables=["firstName", "file", "auth_bsn"], - ) + options: JSONDumpOptions = { + "service": self.service, + "path": "..", + "variables": ["firstName", "file", "auth_bsn"], + } json_plugin = JSONDumpRegistration("json_registration_plugin") - set_submission_reference(submission) for path in ("..", "../foo", "foo/..", "foo/../bar"): with self.subTest(path): - json_form_options["path"] = path + options["path"] = path with self.assertRaises(SuspiciousOperation): - json_plugin.register_submission(submission, json_form_options) + json_plugin.register_submission(submission, options) From 2500948672e395fe2026cd9c9c7b4744e0f7312b Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Thu, 16 Jan 2025 16:23:50 +0100 Subject: [PATCH 21/22] :globe_with_meridians: [#4908] Update translations --- src/openforms/js/compiled-lang/en.json | 54 ++++++++++++++++++++++++++ src/openforms/js/compiled-lang/nl.json | 54 ++++++++++++++++++++++++++ src/openforms/js/lang/en.json | 45 +++++++++++++++++++++ src/openforms/js/lang/nl.json | 45 +++++++++++++++++++++ 4 files changed, 198 insertions(+) diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index 65ff58eb40..a45c1ff7c9 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -663,6 +663,12 @@ "value": "User defined" } ], + "5rj0a+": [ + { + "type": 0, + "value": "Service" + } + ], "5uaKBM": [ { "type": 0, @@ -1231,6 +1237,12 @@ "value": "Something went wrong while retrieving the available role types defined in the selected case. Please check that the services in the selected API group are configured correctly." } ], + "AkG8Zu": [ + { + "type": 0, + "value": "Plugin configuration: JSON" + } + ], "AtBVAV": [ { "type": 0, @@ -1523,6 +1535,12 @@ "value": "length" } ], + "CZ774U": [ + { + "type": 0, + "value": "Which variables to include in the data to be sent" + } + ], "Cf5zSF": [ { "type": 0, @@ -2245,6 +2263,12 @@ "value": " is unknown. We can only display the JSON definition." } ], + "Ibejpf": [ + { + "type": 0, + "value": "Path relative to the Service API root" + } + ], "Igt0Rc": [ { "type": 0, @@ -2487,6 +2511,12 @@ "value": "Manually defined" } ], + "Kl9yvd": [ + { + "type": 0, + "value": "Path" + } + ], "KrJ+rN": [ { "type": 0, @@ -2883,6 +2913,12 @@ "value": "Advanced" } ], + "OpkUgV": [ + { + "type": 0, + "value": "Whether to include this variable in the data to be sent." + } + ], "Orf0vr": [ { "type": 0, @@ -3803,6 +3839,12 @@ "value": "Remove" } ], + "XyDeaD": [ + { + "type": 0, + "value": "Variables" + } + ], "Y4oNhH": [ { "type": 0, @@ -5673,6 +5715,12 @@ "value": "Something went wrong while retrieving the available object type versions." } ], + "nZZkHx": [ + { + "type": 0, + "value": "Include variable" + } + ], "neCqv9": [ { "type": 0, @@ -5985,6 +6033,12 @@ "value": "This template is evaluated with the submission data when the payment is received. The resulting JSON is sent to the objects API to update (the payment fields of) the earlier created object." } ], + "qPiUic": [ + { + "type": 0, + "value": "Which service to send the data to" + } + ], "qUYLVg": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index f524fc6fd6..0b1a0de606 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -663,6 +663,12 @@ "value": "Gebruikersvariabelen" } ], + "5rj0a+": [ + { + "type": 0, + "value": "Service" + } + ], "5uaKBM": [ { "type": 0, @@ -1235,6 +1241,12 @@ "value": "Er ging iets fout bij het ophalen van de beschikbare roltypen in het geselecteerde zaaktype. Controleer of de services in de geselecteerde API-groep goed ingesteld zijn." } ], + "AkG8Zu": [ + { + "type": 0, + "value": "Plugin configuration: JSON" + } + ], "AtBVAV": [ { "type": 0, @@ -1527,6 +1539,12 @@ "value": "length" } ], + "CZ774U": [ + { + "type": 0, + "value": "Which variables to include in the data to be sent" + } + ], "Cf5zSF": [ { "type": 0, @@ -2266,6 +2284,12 @@ "value": " is niet bekend. We kunnen enkel de JSON-definitie weergeven." } ], + "Ibejpf": [ + { + "type": 0, + "value": "Path relative to the Service API root" + } + ], "Igt0Rc": [ { "type": 0, @@ -2504,6 +2528,12 @@ "value": "Handmatig ingesteld" } ], + "Kl9yvd": [ + { + "type": 0, + "value": "Path" + } + ], "KrJ+rN": [ { "type": 0, @@ -2900,6 +2930,12 @@ "value": "Geavanceerd" } ], + "OpkUgV": [ + { + "type": 0, + "value": "Whether to include this variable in the data to be sent." + } + ], "Orf0vr": [ { "type": 0, @@ -3816,6 +3852,12 @@ "value": "Verwijderen" } ], + "XyDeaD": [ + { + "type": 0, + "value": "Variables" + } + ], "Y4oNhH": [ { "type": 0, @@ -5691,6 +5733,12 @@ "value": "Er ging iets fout bij het ophalen van de objecttypeversies." } ], + "nZZkHx": [ + { + "type": 0, + "value": "Include variable" + } + ], "neCqv9": [ { "type": 0, @@ -6003,6 +6051,12 @@ "value": "Dit sjabloon wordt geƫvalueerd met de inzendingsgegevens wanneer de betaling ontvangen is. De resulterende JSON wordt naar de Objecten-API gestuurd om (de betaalattributen van) het eerder aangemaakte object bij te werken." } ], + "qPiUic": [ + { + "type": 0, + "value": "Which service to send the data to" + } + ], "qUYLVg": [ { "type": 0, diff --git a/src/openforms/js/lang/en.json b/src/openforms/js/lang/en.json index 90c5f34416..20a972fe60 100644 --- a/src/openforms/js/lang/en.json +++ b/src/openforms/js/lang/en.json @@ -274,6 +274,11 @@ "description": "Variable source group label for user defined variables", "originalDefault": "User defined" }, + "5rj0a+": { + "defaultMessage": "Service", + "description": "JSON registration options 'serviceSelect' label", + "originalDefault": "Service" + }, "5uaKBM": { "defaultMessage": "minus", "description": "\"-\" operator description", @@ -544,6 +549,11 @@ "description": "ZGW APIs registrations options: role type error", "originalDefault": "Something went wrong while retrieving the available role types defined in the selected case. Please check that the services in the selected API group are configured correctly." }, + "AkG8Zu": { + "defaultMessage": "Plugin configuration: JSON", + "description": "JSON registration options modal title", + "originalDefault": "Plugin configuration: JSON" + }, "AtBVAV": { "defaultMessage": "Confirm", "description": "Form definition select confirm button", @@ -649,6 +659,11 @@ "description": "\"literal\" operand type", "originalDefault": "value" }, + "CZ774U": { + "defaultMessage": "Which variables to include in the data to be sent", + "description": "JSON registration options 'variables' helpText", + "originalDefault": "Which variables to include in the data to be sent" + }, "CiaAYL": { "defaultMessage": "Select the allowed authentication plugins to log in at the start of the form.", "description": "Auth plugin field help text", @@ -1044,6 +1059,11 @@ "description": "Objects API prefill mappings fieldset title", "originalDefault": "Mappings" }, + "Ibejpf": { + "defaultMessage": "Path relative to the Service API root", + "description": "JSON registration options 'path' helpText", + "originalDefault": "Path relative to the Service API root" + }, "Igt0Rc": { "defaultMessage": "Skip ownership check", "description": "Objects API registration: skipOwnershipCheck label", @@ -1164,6 +1184,11 @@ "description": "JSON editor: \"manual\" variable source label", "originalDefault": "Manually defined" }, + "Kl9yvd": { + "defaultMessage": "Path", + "description": "JSON registration options 'path' label", + "originalDefault": "Path" + }, "KtVIGz": { "defaultMessage": "Document types (legacy)", "description": "Objects registration: document types (legacy)", @@ -1394,6 +1419,11 @@ "description": "Advanced logic type", "originalDefault": "Advanced" }, + "OpkUgV": { + "defaultMessage": "Whether to include this variable in the data to be sent.", + "description": "'Include variable' checkbox help text", + "originalDefault": "Whether to include this variable in the data to be sent." + }, "Orf0vr": { "defaultMessage": "Update", "description": "Update service fetch configuration button label", @@ -1814,6 +1844,11 @@ "description": "Objects registration variable mapping, addressNL component: 'options.houseNumber schema target' label", "originalDefault": "House number Schema target" }, + "XyDeaD": { + "defaultMessage": "Variables", + "description": "JSON registration options 'variables' label", + "originalDefault": "Variables" + }, "YDjQH8": { "defaultMessage": "Something went wrong while retrieving the available catalogues and/or document types.", "description": "Objects API registrations options: document types selection error", @@ -2634,6 +2669,11 @@ "description": "Objects API registrations options: object type version select error", "originalDefault": "Something went wrong while retrieving the available object type versions." }, + "nZZkHx": { + "defaultMessage": "Include variable", + "description": "'Include variable' checkbox label", + "originalDefault": "Include variable" + }, "neCqv9": { "defaultMessage": "The registration result will be an object from the selected type.", "description": "Objects API registration options 'Objecttype' helpText", @@ -2774,6 +2814,11 @@ "description": "Legacy objects API registration options: 'paymentStatusUpdateJson' helpText", "originalDefault": "This template is evaluated with the submission data when the payment is received. The resulting JSON is sent to the objects API to update (the payment fields of) the earlier created object." }, + "qPiUic": { + "defaultMessage": "Which service to send the data to", + "description": "JSON registration options 'serviceSelect' helpText", + "originalDefault": "Which service to send the data to" + }, "qUYLVg": { "defaultMessage": "Product & payment", "description": "Product & payments tab title", diff --git a/src/openforms/js/lang/nl.json b/src/openforms/js/lang/nl.json index a68bd3721d..d2a973cf30 100644 --- a/src/openforms/js/lang/nl.json +++ b/src/openforms/js/lang/nl.json @@ -276,6 +276,11 @@ "description": "Variable source group label for user defined variables", "originalDefault": "User defined" }, + "5rj0a+": { + "defaultMessage": "Service", + "description": "JSON registration options 'serviceSelect' label", + "originalDefault": "Service" + }, "5uaKBM": { "defaultMessage": "minus", "description": "\"-\" operator description", @@ -549,6 +554,11 @@ "description": "ZGW APIs registrations options: role type error", "originalDefault": "Something went wrong while retrieving the available role types defined in the selected case. Please check that the services in the selected API group are configured correctly." }, + "AkG8Zu": { + "defaultMessage": "Plugin configuration: JSON", + "description": "JSON registration options modal title", + "originalDefault": "Plugin configuration: JSON" + }, "AtBVAV": { "defaultMessage": "Bevestigen", "description": "Form definition select confirm button", @@ -655,6 +665,11 @@ "description": "\"literal\" operand type", "originalDefault": "value" }, + "CZ774U": { + "defaultMessage": "Which variables to include in the data to be sent", + "description": "JSON registration options 'variables' helpText", + "originalDefault": "Which variables to include in the data to be sent" + }, "CiaAYL": { "defaultMessage": "Selecteer de toegestane authenticatie-plugins om in te loggen aan het begin van het formulier.", "description": "Auth plugin field help text", @@ -1053,6 +1068,11 @@ "description": "Objects API prefill mappings fieldset title", "originalDefault": "Mappings" }, + "Ibejpf": { + "defaultMessage": "Path relative to the Service API root", + "description": "JSON registration options 'path' helpText", + "originalDefault": "Path relative to the Service API root" + }, "Igt0Rc": { "defaultMessage": "Sla eigenaarcontrole over", "description": "Objects API registration: skipOwnershipCheck label", @@ -1173,6 +1193,11 @@ "description": "JSON editor: \"manual\" variable source label", "originalDefault": "Manually defined" }, + "Kl9yvd": { + "defaultMessage": "Path", + "description": "JSON registration options 'path' label", + "originalDefault": "Path" + }, "KtVIGz": { "defaultMessage": "Documenttypen (oud)", "description": "Objects registration: document types (legacy)", @@ -1406,6 +1431,11 @@ "description": "Advanced logic type", "originalDefault": "Advanced" }, + "OpkUgV": { + "defaultMessage": "Whether to include this variable in the data to be sent.", + "description": "'Include variable' checkbox help text", + "originalDefault": "Whether to include this variable in the data to be sent." + }, "Orf0vr": { "defaultMessage": "Update", "description": "Update service fetch configuration button label", @@ -1832,6 +1862,11 @@ "description": "Objects registration variable mapping, addressNL component: 'options.houseNumber schema target' label", "originalDefault": "House number Schema target" }, + "XyDeaD": { + "defaultMessage": "Variables", + "description": "JSON registration options 'variables' label", + "originalDefault": "Variables" + }, "YDjQH8": { "defaultMessage": "Er ging iets fout bij het ophalen van de beschikbare catalogi en/of documenttypen.", "description": "Objects API registrations options: document types selection error", @@ -2655,6 +2690,11 @@ "description": "Objects API registrations options: object type version select error", "originalDefault": "Something went wrong while retrieving the available object type versions." }, + "nZZkHx": { + "defaultMessage": "Include variable", + "description": "'Include variable' checkbox label", + "originalDefault": "Include variable" + }, "neCqv9": { "defaultMessage": "Het registratieresultaat zal een object van dit type zijn.", "description": "Objects API registration options 'Objecttype' helpText", @@ -2795,6 +2835,11 @@ "description": "Legacy objects API registration options: 'paymentStatusUpdateJson' helpText", "originalDefault": "This template is evaluated with the submission data when the payment is received. The resulting JSON is sent to the objects API to update (the payment fields of) the earlier created object." }, + "qPiUic": { + "defaultMessage": "Which service to send the data to", + "description": "JSON registration options 'serviceSelect' helpText", + "originalDefault": "Which service to send the data to" + }, "qUYLVg": { "defaultMessage": "Product en betaling", "description": "Product & payments tab title", From 29b11fa815e4da5661d7b1095ab109b6d791dc9a Mon Sep 17 00:00:00 2001 From: Viktor van Wijk Date: Tue, 21 Jan 2025 12:31:20 +0100 Subject: [PATCH 22/22] :ok_hand: [#4980] Process PR feedback * Revise result in register_submission: submission.registration_result automatically gets written with the return value of register_submission, see https://github.com/open-formulieren/open-forms/blob/794bbdae3c62eb4e4ce429661b402b57f24f4605/src/openforms/registrations/tasks.py#L338 * Remove explicit boolean assignments to properties in Variables: this is not necessary in JSX * Add the json_dump app to the pyright.pyproject.toml, so it is type checked in CI. Also update the types where necessary --- pyright.pyproject.toml | 1 + .../json_dump/fields/Variables.js | 2 +- .../registrations/contrib/json_dump/plugin.py | 26 +++++++++---------- .../contrib/json_dump/tests/test_backend.py | 13 +++++++--- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/pyright.pyproject.toml b/pyright.pyproject.toml index 94b05b008d..5bae5c2f40 100644 --- a/pyright.pyproject.toml +++ b/pyright.pyproject.toml @@ -40,6 +40,7 @@ include = [ # Registrations "src/openforms/registrations/tasks.py", "src/openforms/registrations/contrib/email/", + "src/openforms/registrations/contrib/json_dump/", "src/openforms/registrations/contrib/stuf_zds/options.py", "src/openforms/registrations/contrib/stuf_zds/plugin.py", "src/openforms/registrations/contrib/stuf_zds/typing.py", diff --git a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js index 2d934a3ced..b06998a6df 100644 --- a/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js +++ b/src/openforms/js/components/admin/form_design/registrations/json_dump/fields/Variables.js @@ -33,7 +33,7 @@ const Variables = () => { isMulti required closeMenuOnSelect={false} - includeStaticVariables={true} + includeStaticVariables />
diff --git a/src/openforms/registrations/contrib/json_dump/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py index 1117cf13a3..896c272eb6 100644 --- a/src/openforms/registrations/contrib/json_dump/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -13,7 +13,7 @@ SubmissionFileAttachment, SubmissionValueVariable, ) -from openforms.typing import JSONObject +from openforms.typing import JSONObject, JSONValue from openforms.variables.constants import FormVariableSources from ...base import BasePlugin # openforms.registrations.base @@ -26,6 +26,7 @@ class JSONDumpRegistration(BasePlugin): verbose_name = _("JSON dump registration") configuration_options = JSONDumpOptionsSerializer + # TODO: add JSONDumpResult typed dict to properly indicate return value def register_submission( self, submission: Submission, options: JSONDumpOptions ) -> dict: @@ -62,17 +63,14 @@ def register_submission( # Send to the service data = json.dumps({"values": values, "schema": schema}, cls=DjangoJSONEncoder) service = options["service"] - submission.registration_result = result = {} with build_client(service) as client: if ".." in (path := options["path"]): raise SuspiciousOperation("Possible path traversal detected") - res = client.post(path, json=data) - res.raise_for_status() + result = client.post(path, json=data) + result.raise_for_status() - result["api_response"] = res.json() - - return result + return {"api_response": result.json()} def check_config(self) -> None: # Config checks are not really relevant for this plugin right now @@ -101,7 +99,7 @@ def process_variables(submission: Submission, values: JSONObject): if component is None or component["type"] != "file": continue - encoded_attachments = [ + encoded_attachments: list[JSONValue] = [ { "file_name": attachment.original_name, "content": encode_attachment(attachment), @@ -111,19 +109,19 @@ def process_variables(submission: Submission, values: JSONObject): ] match ( - multiple := component.get("multiple", False), - n_attachments := len(encoded_attachments) + component.get("multiple", False), + n_attachments := len(encoded_attachments), ): case False, 0: values[key] = None case False, 1: values[key] = encoded_attachments[0] - case True, _: + case True, int(): values[key] = encoded_attachments - case _: + case False, int(): # pragma: no cover raise ValueError( - f"Combination of multiple ({multiple}) and number of " - f"attachments ({n_attachments}) is not allowed." + f"Cannot have multiple attachments ({n_attachments}) for a " + "single file component." ) 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 c9157efc74..132a86be08 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -52,7 +52,7 @@ def test_submission_happy_flow(self): SubmissionFileAttachmentFactory.create( form_key="file", - submission_step=submission.submissionstep_set.get(), + submission_step=submission.steps[0], file_name="test_file.txt", original_name="test_file.txt", content_type="application/text", @@ -96,6 +96,7 @@ def test_submission_happy_flow(self): } result = json_plugin.register_submission(submission, options) + assert result is not None self.assertEqual(result["api_response"], expected_response) @@ -152,7 +153,7 @@ def test_multiple_file_uploads(self): SubmissionFileAttachmentFactory.create( form_key="file", - submission_step=submission.submissionstep_set.get(), + submission_step=submission.steps[0], file_name="file1.txt", original_name="file1.txt", content_type="application/text", @@ -163,7 +164,7 @@ def test_multiple_file_uploads(self): SubmissionFileAttachmentFactory.create( form_key="file", - submission_step=submission.submissionstep_set.get(), + submission_step=submission.steps[0], file_name="file2.txt", original_name="file2.txt", content_type="application/text", @@ -191,6 +192,7 @@ def test_multiple_file_uploads(self): ] result = json_plugin.register_submission(submission, options) + assert result is not None self.assertEqual( result["api_response"]["data"]["values"]["file"], expected_files @@ -215,7 +217,7 @@ def test_one_file_upload_for_multiple_files_component(self): SubmissionFileAttachmentFactory.create( form_key="file", - submission_step=submission.submissionstep_set.get(), + submission_step=submission.steps[0], file_name="file1.txt", original_name="file1.txt", content_type="application/text", @@ -232,6 +234,7 @@ def test_one_file_upload_for_multiple_files_component(self): json_plugin = JSONDumpRegistration("json_registration_plugin") result = json_plugin.register_submission(submission, options) + assert result is not None self.assertEqual( result["api_response"]["data"]["values"]["file"], @@ -261,6 +264,7 @@ def test_no_file_upload_for_single_file_component(self): json_plugin = JSONDumpRegistration("json_registration_plugin") result = json_plugin.register_submission(submission, options) + assert result is not None self.assertEqual(result["api_response"]["data"]["values"]["file"], None) @@ -282,6 +286,7 @@ def test_no_file_upload_for_multiple_files_component(self): json_plugin = JSONDumpRegistration("json_registration_plugin") result = json_plugin.register_submission(submission, options) + assert result is not None self.assertEqual(result["api_response"]["data"]["values"]["file"], [])