From 13938ae5997229cf8c4c2d37baf5fbac9900579d Mon Sep 17 00:00:00 2001 From: vasileios <zigras00@gmail.com> Date: Wed, 2 Oct 2024 09:18:29 +0200 Subject: [PATCH] [#4396] Moved public functions to prefill.service and added prefill plugin for ObjectsApi --- .../haal_centraal/tests/test_integration.py | 2 +- src/openforms/formio/service.py | 4 +- .../tests/test_component_translations.py | 5 +- ...efill_config_empty_or_complete_and_more.py | 12 +- src/openforms/forms/models/form_variable.py | 10 +- .../forms/tests/variables/test_model.py | 15 +- src/openforms/prefill/__init__.py | 206 ------------- src/openforms/prefill/base.py | 21 ++ .../prefill/contrib/objects_api/plugin.py | 30 ++ ...test_invalid_service_raises_exception.yaml | 75 +++++ ...tTests.test_list_available_attributes.yaml | 2 +- ...inEndpointTests.test_list_objecttypes.yaml | 7 +- ...tTests.test_list_objecttypes_versions.yaml | 2 +- ...nTests.test_prefill_values_happy_flow.yaml | 108 +++++++ ...efill_values_when_reference_not_found.yaml | 50 ++++ ...s_when_reference_returns_empty_values.yaml | 105 +++++++ ...lled_values_are_updated_in_the_object.yaml | 206 +++++++++++++ .../contrib/objects_api/tests/test_config.py | 74 +++++ .../objects_api/tests/test_endpoints.py | 9 - .../contrib/objects_api/tests/test_prefill.py | 270 ++++++++++++++++++ src/openforms/prefill/service.py | 172 +++++++++++ src/openforms/prefill/sources/__init__.py | 0 src/openforms/prefill/sources/component.py | 78 +++++ src/openforms/prefill/sources/user_defined.py | 36 +++ .../prefill/tests/test_prefill_hook.py | 2 +- .../prefill/tests/test_prefill_variables.py | 7 +- src/openforms/prefill/utils.py | 18 ++ src/openforms/submissions/api/viewsets.py | 2 +- .../tests/test_start_submission.py | 22 +- .../tests/test_submission_step_validate.py | 4 +- src/openforms/variables/tests/test_views.py | 2 + 31 files changed, 1302 insertions(+), 254 deletions(-) create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginConfigTests/ObjectsAPIPrefillPluginConfigTests.test_invalid_service_raises_exception.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefilled_values_are_updated_in_the_object.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/test_config.py create mode 100644 src/openforms/prefill/contrib/objects_api/tests/test_prefill.py create mode 100644 src/openforms/prefill/service.py create mode 100644 src/openforms/prefill/sources/__init__.py create mode 100644 src/openforms/prefill/sources/component.py create mode 100644 src/openforms/prefill/sources/user_defined.py create mode 100644 src/openforms/prefill/utils.py diff --git a/src/openforms/contrib/haal_centraal/tests/test_integration.py b/src/openforms/contrib/haal_centraal/tests/test_integration.py index b2b663b302..27532ccc22 100644 --- a/src/openforms/contrib/haal_centraal/tests/test_integration.py +++ b/src/openforms/contrib/haal_centraal/tests/test_integration.py @@ -8,8 +8,8 @@ from openforms.authentication.service import AuthAttribute from openforms.authentication.utils import store_auth_details, store_registrator_details from openforms.config.models import GlobalConfiguration -from openforms.prefill import prefill_variables from openforms.prefill.contrib.haalcentraal_brp.plugin import PLUGIN_IDENTIFIER +from openforms.prefill.service import prefill_variables from openforms.submissions.tests.factories import SubmissionFactory from openforms.typing import JSONValue from openforms.utils.tests.vcr import OFVCRMixin diff --git a/src/openforms/formio/service.py b/src/openforms/formio/service.py index f4bf4d8f9b..f7f2d0bfc7 100644 --- a/src/openforms/formio/service.py +++ b/src/openforms/formio/service.py @@ -14,7 +14,6 @@ import elasticapm from rest_framework.request import Request -from openforms.prefill import inject_prefill from openforms.submissions.models import Submission from openforms.typing import DataMapping @@ -73,6 +72,9 @@ def get_dynamic_configuration( The configuration is modified in the context of the provided ``submission`` parameter. """ + # Avoid circular imports + from openforms.prefill.service import inject_prefill + rewrite_formio_components(config_wrapper, submission=submission, data=data) # Add to each component the custom errors in the current locale diff --git a/src/openforms/formio/tests/test_component_translations.py b/src/openforms/formio/tests/test_component_translations.py index cc2a97dd1b..896efbc17b 100644 --- a/src/openforms/formio/tests/test_component_translations.py +++ b/src/openforms/formio/tests/test_component_translations.py @@ -28,7 +28,10 @@ def disable_prefill_injection(): """ Disable prefill to prevent prefill-related queries. """ - return patch("openforms.formio.service.inject_prefill", new=MagicMock) + return patch( + "openforms.prefill.service.inject_prefill", + new=MagicMock, + ) TEST_CONFIGURATION = { diff --git a/src/openforms/forms/migrations/0101_remove_formvariable_prefill_config_empty_or_complete_and_more.py b/src/openforms/forms/migrations/0101_remove_formvariable_prefill_config_empty_or_complete_and_more.py index eecf2b758b..89fbf95b85 100644 --- a/src/openforms/forms/migrations/0101_remove_formvariable_prefill_config_empty_or_complete_and_more.py +++ b/src/openforms/forms/migrations/0101_remove_formvariable_prefill_config_empty_or_complete_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.16 on 2024-10-02 07:06 +# Generated by Django 4.2.16 on 2024-10-02 10:03 from django.db import migrations, models @@ -34,19 +34,19 @@ class Migration(migrations.Migration): models.Q( models.Q(("prefill_plugin", ""), _negated=True), ("prefill_attribute", ""), - ("prefill_options", {}), + models.Q(("prefill_options", {}), _negated=True), ("source", "user_defined"), ), models.Q( models.Q(("prefill_plugin", ""), _negated=True), - models.Q(("prefill_attribute", ""), _negated=True), + ("prefill_attribute", ""), ("prefill_options", {}), + models.Q(("source", "user_defined"), _negated=True), ), models.Q( models.Q(("prefill_plugin", ""), _negated=True), - ("prefill_attribute", ""), - models.Q(("prefill_options", {}), _negated=True), - ("source", "user_defined"), + models.Q(("prefill_attribute", ""), _negated=True), + ("prefill_options", {}), ), _connector="OR", ) diff --git a/src/openforms/forms/models/form_variable.py b/src/openforms/forms/models/form_variable.py index 640c60b476..8c30ce56e9 100644 --- a/src/openforms/forms/models/form_variable.py +++ b/src/openforms/forms/models/form_variable.py @@ -216,19 +216,19 @@ class Meta: | ( ~EMPTY_PREFILL_PLUGIN & EMPTY_PREFILL_ATTRIBUTE - & EMPTY_PREFILL_OPTIONS + & ~EMPTY_PREFILL_OPTIONS & USER_DEFINED ) | ( ~EMPTY_PREFILL_PLUGIN - & ~EMPTY_PREFILL_ATTRIBUTE + & EMPTY_PREFILL_ATTRIBUTE & EMPTY_PREFILL_OPTIONS + & ~USER_DEFINED ) | ( ~EMPTY_PREFILL_PLUGIN - & EMPTY_PREFILL_ATTRIBUTE - & ~EMPTY_PREFILL_OPTIONS - & USER_DEFINED + & ~EMPTY_PREFILL_ATTRIBUTE + & EMPTY_PREFILL_OPTIONS ) ), name="prefill_config_component_or_user_defined", diff --git a/src/openforms/forms/tests/variables/test_model.py b/src/openforms/forms/tests/variables/test_model.py index 62557c610d..48c98b779c 100644 --- a/src/openforms/forms/tests/variables/test_model.py +++ b/src/openforms/forms/tests/variables/test_model.py @@ -32,10 +32,17 @@ def test_prefill_plugin_prefill_attribute_prefill_options_empty(self): ) def test_prefill_attribute_prefill_options_empty(self): - FormVariableFactory.create( - prefill_plugin="demo", - prefill_attribute="", - prefill_options={}, + FormStepFactory.create( + form_definition__configuration={ + "components": [ + { + "type": "textfield", + "key": "test-key", + "label": "Test label", + "prefill": {"plugin": "demo", "attribute": ""}, + } + ] + } ) def test_prefill_options_empty(self): diff --git a/src/openforms/prefill/__init__.py b/src/openforms/prefill/__init__.py index 177a8e13b4..e69de29bb2 100644 --- a/src/openforms/prefill/__init__.py +++ b/src/openforms/prefill/__init__.py @@ -1,206 +0,0 @@ -""" -This package holds the base module structure for the pre-fill plugins used in Open Forms. - -Various sources exist that can be consulted to fetch data for an active session, -where the BSN, CoC number... can be used to retrieve this data. Think of pre-filling -the address details of a person after logging in with DigiD. - -The package integrates with the form builder such that it's possible for every form -field to select which pre-fill plugin to use and which value to use from the fetched -result. Plugins can be registered using a similar approach to the registrations -package. Each plugin is responsible for exposing which attributes/data fragments are -available, and for performing the actual look-up. Plugins receive the -:class:`openforms.submissions.models.Submission` instance that represents the current -form session of an end-user. - -Prefill values are embedded as default values for form fields, dynamically for every -user session using the component rewrite functionality in the serializers. - -So, to recap: - -1. Plugins are defined and registered -2. When editing form definitions in the admin, content editors can opt-in to pre-fill - functionality. They select the desired plugin, and then the desired attribute from - that plugin. -3. End-user starts the form and logs in, thereby creating a session/``Submission`` -4. The submission-specific form definition configuration is enhanced with the pre-filled - form field default values. - -.. todo:: Move the public API into ``openforms.prefill.service``. - -""" - -from __future__ import annotations - -import logging -from collections import defaultdict -from typing import TYPE_CHECKING, Any - -import elasticapm -from glom import Path, PathAccessError, assign, glom -from zgw_consumers.concurrent import parallel - -from openforms.plugins.exceptions import PluginNotEnabled -from openforms.variables.constants import FormVariableSources - -if TYPE_CHECKING: - from openforms.formio.service import FormioConfigurationWrapper - from openforms.submissions.models import Submission - - from .registry import Registry - -logger = logging.getLogger(__name__) - - -@elasticapm.capture_span(span_type="app.prefill") -def _fetch_prefill_values( - grouped_fields: dict[str, dict[str, list[str]]], - submission: Submission, - register: Registry, -) -> dict[str, dict[str, Any]]: - # local import to prevent AppRegistryNotReady: - from openforms.logging import logevent - - @elasticapm.capture_span(span_type="app.prefill") - def invoke_plugin( - item: tuple[str, str, list[str]] - ) -> tuple[str, str, dict[str, Any]]: - plugin_id, identifier_role, fields = item - - plugin = register[plugin_id] - if not plugin.is_enabled: - raise PluginNotEnabled() - - try: - values = plugin.get_prefill_values(submission, fields, identifier_role) - except Exception as e: - logger.exception(f"exception in prefill plugin '{plugin_id}'") - logevent.prefill_retrieve_failure(submission, plugin, e) - values = {} - else: - if values: - logevent.prefill_retrieve_success(submission, plugin, fields) - else: - logevent.prefill_retrieve_empty(submission, plugin, fields) - - return plugin_id, identifier_role, values - - invoke_plugin_args = [] - for plugin_id, field_groups in grouped_fields.items(): - for identifier_role, fields in field_groups.items(): - invoke_plugin_args.append((plugin_id, identifier_role, fields)) - - with parallel() as executor: - results = executor.map(invoke_plugin, invoke_plugin_args) - - collected_results = {} - for plugin_id, identifier_role, values_dict in list(results): - assign( - collected_results, - Path(plugin_id, identifier_role), - values_dict, - missing=dict, - ) - - return collected_results - - -def inject_prefill( - configuration_wrapper: FormioConfigurationWrapper, submission: Submission -) -> None: - """ - Mutates each component found in configuration according to the prefilled values. - - :param configuration_wrapper: The Formiojs JSON schema wrapper describing an entire - form or an individual component within the form. - :param submission: The :class:`openforms.submissions.models.Submission` instance - that holds the values of the prefill data. The prefill data was fetched earlier, - see :func:`prefill_variables`. - - The prefill values are looped over by key: value, and for each value the matching - component is looked up to normalize it in the context of the component. - """ - from openforms.formio.service import normalize_value_for_component - - prefilled_data = submission.get_prefilled_data() - for key, prefill_value in prefilled_data.items(): - try: - component = configuration_wrapper[key] - except KeyError: - # The component to prefill is not in this step - continue - - if not (prefill := component.get("prefill")): - continue - if not prefill.get("plugin"): - continue - if not prefill.get("attribute"): - continue - - default_value = component.get("defaultValue") - # 1693: we need to normalize values according to the format expected by the - # component. For example, (some) prefill plugins return postal codes without - # space between the digits and the letters. - prefill_value = normalize_value_for_component(component, prefill_value) - - if prefill_value != default_value and default_value is not None: - logger.info( - "Overwriting non-null default value for component %r", - component, - ) - component["defaultValue"] = prefill_value - - -@elasticapm.capture_span(span_type="app.prefill") -def prefill_variables(submission: Submission, register: Registry | None = None) -> None: - """Update the submission variables state with the fetched attribute values. - - For each submission value variable that need to be prefilled, the according plugin will - be used to fetch the value. If ``register`` is not specified, the default registry instance - will be used. - """ - from openforms.formio.service import normalize_value_for_component - - from .registry import register as default_register - - register = register or default_register - - state = submission.load_submission_value_variables_state() - variables_to_prefill = state.get_prefill_variables() - - # grouped_fields is a dict of the following shape: - # {"plugin_id": {"identifier_role": ["attr_1", "attr_2"]}} - # "identifier_role" is either "main" or "authorizee" - grouped_fields: defaultdict[str, defaultdict[str, list[str]]] = defaultdict( - lambda: defaultdict(list) - ) - for variable in variables_to_prefill: - plugin_id = variable.form_variable.prefill_plugin - identifier_role = variable.form_variable.prefill_identifier_role - attribute_name = variable.form_variable.prefill_attribute - - grouped_fields[plugin_id][identifier_role].append(attribute_name) - - results = _fetch_prefill_values(grouped_fields, submission, register) - - total_config_wrapper = submission.total_configuration_wrapper - prefill_data = {} - for variable in variables_to_prefill: - try: - prefill_value = glom( - results, - Path( - variable.form_variable.prefill_plugin, - variable.form_variable.prefill_identifier_role, - variable.form_variable.prefill_attribute, - ), - ) - except PathAccessError: - continue - else: - if variable.form_variable.source == FormVariableSources.component: - component = total_config_wrapper[variable.key] - prefill_value = normalize_value_for_component(component, prefill_value) - prefill_data[variable.key] = prefill_value - - state.save_prefill_data(prefill_data) diff --git a/src/openforms/prefill/base.py b/src/openforms/prefill/base.py index 609c3cb772..b7b989f448 100644 --- a/src/openforms/prefill/base.py +++ b/src/openforms/prefill/base.py @@ -1,6 +1,7 @@ from typing import Any, Container, Iterable from openforms.authentication.service import AuthAttribute +from openforms.forms.models import FormVariable from openforms.plugins.plugin import AbstractBasePlugin from openforms.submissions.models import Submission from openforms.typing import JSONEncodable, JSONObject @@ -50,6 +51,26 @@ def get_prefill_values( """ raise NotImplementedError("You must implement the 'get_prefill_values' method.") + @classmethod + def get_prefill_values_from_mappings( + cls, submission: Submission, form_variable: FormVariable + ) -> dict[str, str]: + """ + Given the saved form variable, which contains the prefill_options, look up the appropriate + values and return them. + + :param submission: an active :class:`Submission` instance, which can supply + the required initial data reference to fetch the correct prefill values. + :param form_variable: The form variable for which we want to retrieve the data. Its + atribute prefill_options contains all the mappings that are needed for retrieving + and returning the values. + :return: a key-value dictionary, where the key is the mapped property and + the value is the prefill value to use for that property. + """ + raise NotImplementedError( + "You must implement the 'get_prefill_values_from_mappings' method." + ) + @classmethod def get_co_sign_values( cls, submission: Submission, identifier: str diff --git a/src/openforms/prefill/contrib/objects_api/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py index 5efa02d081..f831da28af 100644 --- a/src/openforms/prefill/contrib/objects_api/plugin.py +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -3,13 +3,19 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from glom import Path, glom + from openforms.contrib.objects_api.checks import check_config +from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig +from openforms.forms.models import FormVariable from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig +from openforms.submissions.models import Submission from openforms.typing import JSONObject from ...base import BasePlugin from ...registry import register +from ...utils import find_in_dicts logger = logging.getLogger(__name__) @@ -20,6 +26,30 @@ class ObjectsAPIPrefill(BasePlugin): verbose_name = _("Objects API") + @classmethod + def get_prefill_values_from_mappings( + cls, + submission: Submission, + form_variable: FormVariable, + ) -> dict[str, str]: + variables_mappings = form_variable.prefill_options.get("variables_mapping") + config_group_id = form_variable.prefill_options.get("objects_api_group") + config_group = ObjectsAPIGroupConfig.objects.get(id=config_group_id) + + with get_objects_client(config_group) as client: + obj = client.get_object(submission.initial_data_reference) + + obj_record = obj.get("record", {}) + obj_record_data = glom(obj, "record.data") + + results = {} + for mapping in variables_mappings: + path = Path(*mapping["target_path"]) + if value := find_in_dicts(obj_record, obj_record_data, path=path): + results[mapping["variable_key"]] = value + + return results + def check_config(self): check_config() diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginConfigTests/ObjectsAPIPrefillPluginConfigTests.test_invalid_service_raises_exception.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginConfigTests/ObjectsAPIPrefillPluginConfigTests.test_invalid_service_raises_exception.yaml new file mode 100644 index 0000000000..0931ad8d4c --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginConfigTests/ObjectsAPIPrefillPluginConfigTests.test_invalid_service_raises_exception.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token INVALID + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/invalid/objects?pageSize=1 + response: + body: + string: "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n + \ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, + shrink-to-fit=no\">\n\n <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/bundles/objects-css.d91e8a41885a.css\">\n + \ <link rel=\"stylesheet\" href=\"/static/vng_api_common/libs/fontawesome/css/all.min.8e6bafb03a21.css\">\n\n + \ <link rel=\"icon\" type=\"image/png\" href=\"/static/ico/favicon.3b1d8b596615.png\">\n\n + \ \n\n <title>Starting point | Objects</title>\n </head>\n <body>\n\n + \ \n\n \n\n <h1>Sorry, the requested page could not be found (404)</h1>\n\n\n\n\n + \ <footer class=\"footer container\">\n <div class=\"footer__row\">\n + \ <div class=\"footer__col footer__col--small\">\n <img src=\"/static/img/maykin_logo.d6ba51c82254.png\" + alt=\"Maykin Media logo\" height=\"48\">\n <img src=\"/static/img/opengem_logo.32e4cb40dc24.png\" + alt=\"Open Gemeente Initiatief logo\" height=\"48\">\n <p>\n Developed + by <a class=\"link\" href=\"https://www.maykinmedia.nl\">Maykin Media</a><br>\n + \ within the <a class=\"link\" href=\"https://opengem.nl\">Open Gemeente + Initiatief</a> © 2024<br>\n commissioned by the <a class=\"link\" + href=\"https://www.utrecht.nl\">Municipality of Utrecht</a>\n </p>\n + \ </div>\n <div class=\"footer__col\">\n <h5 class=\"footer__header\">Objects + API</h5>\n <ul class=\"footer__list\">\n <li><a class=\"link + link--muted\" href=\"https://objects-and-objecttypes-api.readthedocs.io/\">Documentation</a></li>\n + \ <li><a class=\"link link--muted\" href=\"https://commonground.nl/groups/view/54477963/objecten-en-objecttypen-api\">Community + on CommonGround.nl</a></li>\n <li><a class=\"link link--muted\" + href=\"https://hub.docker.com/r/maykinmedia/objects-api\">Docker images</a></li>\n + \ <li><a class=\"link link--muted\" href=\"https://github.com/maykinmedia/objects-api\">Code + on Github</a></li>\n </ul>\n </div>\n <div class=\"footer__col\">\n + \ <h5 class=\"footer__header\">Other</h5>\n <ul class=\"footer__list\">\n + \ <li>Report <a class=\"link link--muted\" href=\"https://github.com/maykinmedia/objects-api/issues\">issues</a> + for questions, bugs or wishes</li>\n <li>Read more on <a class=\"link + link--muted\" href=\"https://commonground.nl/\">Common Ground</a></li>\n </ul>\n + \ </div>\n </div>\n\n <div class=\"footer__row\">\n <div + class=\"footer__col\">\n <code></code>\n </div>\n <div + class=\"footer__col footer__col--right\">\n <code></code>\n </div>\n + \ </div>\n </footer>\n\n <script src=\"/static/bundles/objects-js.e2e1136a87f5.js\" + type=\"text/javascript\"></script>\n \n </body>\n</html>\n" + headers: + Connection: + - keep-alive + Content-Length: + - '2733' + Content-Type: + - text/html; charset=utf-8 + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:09 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 404 + message: Not Found +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_available_attributes.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_available_attributes.yaml index c0f7920960..ea1ed3ae86 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_available_attributes.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_available_attributes.yaml @@ -32,7 +32,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 16 Sep 2024 14:22:04 GMT + - Wed, 02 Oct 2024 11:26:10 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml index d20ef5fa22..1f0e4344c5 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml @@ -16,7 +16,8 @@ interactions: uri: http://localhost:8001/api/v2/objecttypes response: body: - string: '{"count":6,"next":null,"previous":null,"results":[{"url":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","uuid":"8faed0fa-7864-4409-aa6d-533a37616a9e","name":"Accepts + string: '{"count":7,"next":null,"previous":null,"results":[{"url":"http://objecttypes-web:8000/api/v2/objecttypes/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10","uuid":"ac1fa3f8-fb2a-4fcb-b715-d480aceeda10","name":"Person + (published)","namePlural":"Person (published)","description":"","dataClassification":"open","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2024-07-26","modifiedAt":"2024-07-26","allowGeometry":true,"versions":["http://objecttypes-web:8000/api/v2/objecttypes/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10/versions/1"]},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","uuid":"8faed0fa-7864-4409-aa6d-533a37616a9e","name":"Accepts everything","namePlural":"Accepts everything","description":"","dataClassification":"open","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2024-07-22","modifiedAt":"2024-07-22","allowGeometry":true,"versions":["http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e/versions/1"]},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/644ab597-e88c-43c0-8321-f12113510b0e","uuid":"644ab597-e88c-43c0-8321-f12113510b0e","name":"Fieldset component","namePlural":"Fieldset component","description":"","dataClassification":"confidential","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","allowGeometry":true,"versions":["http://objecttypes-web:8000/api/v2/objecttypes/644ab597-e88c-43c0-8321-f12113510b0e/versions/1"]},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/f1dde4fe-b7f9-46dc-84ae-429ae49e3705","uuid":"f1dde4fe-b7f9-46dc-84ae-429ae49e3705","name":"Geo in data","namePlural":"Geo in data","description":"","dataClassification":"confidential","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","allowGeometry":true,"versions":["http://objecttypes-web:8000/api/v2/objecttypes/f1dde4fe-b7f9-46dc-84ae-429ae49e3705/versions/1"]},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/527b8408-7421-4808-a744-43ccb7bdaaa2","uuid":"527b8408-7421-4808-a744-43ccb7bdaaa2","name":"File @@ -27,13 +28,13 @@ interactions: Connection: - keep-alive Content-Length: - - '3921' + - '4541' Content-Type: - application/json Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 16 Sep 2024 14:22:04 GMT + - Wed, 02 Oct 2024 11:26:10 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes_versions.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes_versions.yaml index 6f571e1507..4cc0b8b675 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes_versions.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes_versions.yaml @@ -35,7 +35,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 16 Sep 2024 14:22:04 GMT + - Wed, 02 Oct 2024 11:26:10 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml new file mode 100644 index 0000000000..71d48b4610 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml @@ -0,0 +1,108 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "record": {"typeVersion": 3, "data": {"name": {"last.name": "My last name"}, + "age": 45}, "startAt": "2024-10-02"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '210' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/888f84cc-867c-48c4-815e-c3274d3ecc5c","uuid":"888f84cc-867c-48c4-815e-c3274d3ecc5c","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"name":{"last.name":"My + last name"},"age":45},"geometry":null,"startAt":"2024-10-02","endAt":null,"registrationAt":"2024-10-02","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '437' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:10 GMT + Location: + - http://localhost:8002/api/v2/objects/888f84cc-867c-48c4-815e-c3274d3ecc5c + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/888f84cc-867c-48c4-815e-c3274d3ecc5c + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/888f84cc-867c-48c4-815e-c3274d3ecc5c","uuid":"888f84cc-867c-48c4-815e-c3274d3ecc5c","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"name":{"last.name":"My + last name"}},"geometry":null,"startAt":"2024-10-02","endAt":null,"registrationAt":"2024-10-02","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '437' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:10 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml new file mode 100644 index 0000000000..7ccdcbd721 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/048a37ca-a602-4158-9e60-9f06f3e47e2a + response: + body: + string: '{"detail":"Not found."}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '23' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:11 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 404 + message: Not Found +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml new file mode 100644 index 0000000000..11a70a4bfd --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml @@ -0,0 +1,105 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "record": {"typeVersion": 3, "data": {}, "startAt": "2024-10-02"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '162' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/fa95e317-9d82-4bcb-b9c7-e0d63460c29f","uuid":"fa95e317-9d82-4bcb-b9c7-e0d63460c29f","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{},"geometry":null,"startAt":"2024-10-02","endAt":null,"registrationAt":"2024-10-02","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '393' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:11 GMT + Location: + - http://localhost:8002/api/v2/objects/fa95e317-9d82-4bcb-b9c7-e0d63460c29f + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/fa95e317-9d82-4bcb-b9c7-e0d63460c29f + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/fa95e317-9d82-4bcb-b9c7-e0d63460c29f","uuid":"fa95e317-9d82-4bcb-b9c7-e0d63460c29f","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{},"geometry":null,"startAt":"2024-10-02","endAt":null,"registrationAt":"2024-10-02","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '393' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:11 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefilled_values_are_updated_in_the_object.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefilled_values_are_updated_in_the_object.yaml new file mode 100644 index 0000000000..b83b4504ed --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefilled_values_are_updated_in_the_object.yaml @@ -0,0 +1,206 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "record": {"typeVersion": 3, "data": {"name": {"last.name": "My last name"}, + "age": 45}, "startAt": "2024-10-02"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '210' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c1ef1a9c-d19b-4cc2-8b58-800ba00317f0","uuid":"c1ef1a9c-d19b-4cc2-8b58-800ba00317f0","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"name":{"last.name":"My + last name"},"age":45},"geometry":null,"startAt":"2024-10-02","endAt":null,"registrationAt":"2024-10-02","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '437' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:11 GMT + Location: + - http://localhost:8002/api/v2/objects/c1ef1a9c-d19b-4cc2-8b58-800ba00317f0 + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/c1ef1a9c-d19b-4cc2-8b58-800ba00317f0 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c1ef1a9c-d19b-4cc2-8b58-800ba00317f0","uuid":"c1ef1a9c-d19b-4cc2-8b58-800ba00317f0","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"name":{"last.name":"My + last name"}},"geometry":null,"startAt":"2024-10-02","endAt":null,"registrationAt":"2024-10-02","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '437' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:11 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 171be5abaf41e7856b423ad513df1ef8f867ff48 + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48 + response: + body: + string: '{"url":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","uuid":"8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","name":"Person","namePlural":"Persons","description":"","dataClassification":"open","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2023-10-24","modifiedAt":"2024-02-08","allowGeometry":true,"versions":["http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/1","http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/2","http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/3"]}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Length: + - '790' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:11 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: '{"record": {"typeVersion": 3, "data": {"age": 40, "name": {"last.name": + "My updated last name"}}, "startAt": "2024-10-02"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '123' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: PATCH + uri: http://localhost:8002/api/v2/objects/c1ef1a9c-d19b-4cc2-8b58-800ba00317f0 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c1ef1a9c-d19b-4cc2-8b58-800ba00317f0","uuid":"c1ef1a9c-d19b-4cc2-8b58-800ba00317f0","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":2,"typeVersion":3,"data":{"age":40,"name":{"last.name":"My + updated last name"}},"geometry":null,"startAt":"2024-10-02","endAt":null,"registrationAt":"2024-10-02","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '445' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 02 Oct 2024 11:26:11 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_config.py b/src/openforms/prefill/contrib/objects_api/tests/test_config.py new file mode 100644 index 0000000000..07708e0389 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/test_config.py @@ -0,0 +1,74 @@ +from pathlib import Path +from unittest.mock import patch + +from django.test import override_settings +from django.utils.translation import gettext as _ + +from rest_framework.test import APITestCase +from zgw_consumers.constants import APITypes, AuthTypes +from zgw_consumers.test.factories import ServiceFactory + +from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory +from openforms.plugins.exceptions import InvalidPluginConfiguration +from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig +from openforms.utils.tests.vcr import OFVCRMixin + +from ....registry import register + +plugin = register["objects_api"] + +VCR_TEST_FILES = Path(__file__).parent / "files" + + +class ObjectsAPIPrefillPluginConfigTests(OFVCRMixin, APITestCase): + """This test case requires the Objects & Objecttypes API to be running. + See the relevant Docker compose in the ``docker/`` folder. + """ + + VCR_TEST_FILES = VCR_TEST_FILES + + def setUp(self): + super().setUp() + + config_patcher = patch( + "openforms.registrations.contrib.objects_api.models.ObjectsAPIConfig.get_solo", + return_value=ObjectsAPIConfig(), + ) + self.mock_get_config = config_patcher.start() + self.addCleanup(config_patcher.stop) + + self.objects_api_group = ObjectsAPIGroupConfigFactory.create( + for_test_docker_compose=True + ) + + @override_settings(LANGUAGE_CODE="en") + def test_undefined_service_raises_exception(self): + self.objects_api_group.objects_service = None + self.objects_api_group.save() + + with self.assertRaisesMessage( + InvalidPluginConfiguration, + _( + "Objects API endpoint is not configured for Objects API group {objects_api_group}." + ).format(objects_api_group=self.objects_api_group), + ): + plugin.check_config() + + def test_invalid_service_raises_exception(self): + objects_service = ServiceFactory.create( + api_root="http://localhost:8002/api/v2/invalid", + api_type=APITypes.orc, + oas="https://example.com/", + header_key="Authorization", + header_value="Token INVALID", + auth_type=AuthTypes.api_key, + ) + self.objects_api_group.objects_service = objects_service + self.objects_api_group.save() + + with self.assertRaises( + InvalidPluginConfiguration, + ) as exc: + plugin.check_config() + + self.assertIn("404 Client Error", exc.exception.args[0]) diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_endpoints.py b/src/openforms/prefill/contrib/objects_api/tests/test_endpoints.py index 0e90728a30..e4c420b529 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_endpoints.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_endpoints.py @@ -45,17 +45,8 @@ def setUp(self): "openforms.registrations.contrib.objects_api.models.ObjectsAPIConfig.get_solo", return_value=ObjectsAPIConfig(), ) - attributes_patcher = patch( - "openforms.prefill.contrib.objects_api.plugin.ObjectsAPIPrefill.get_available_attributes", - return_value=[ - ("value one", "value one (string)"), - ("another value", "another value (string)"), - ], - ) self.mock_get_config = config_patcher.start() - self.mock_attributes = attributes_patcher.start() self.addCleanup(config_patcher.stop) - self.addCleanup(attributes_patcher.stop) self.objects_api_group = ObjectsAPIGroupConfigFactory.create( for_test_docker_compose=True diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py new file mode 100644 index 0000000000..829b9dfc4f --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py @@ -0,0 +1,270 @@ +from pathlib import Path +from unittest.mock import patch +from uuid import UUID + +from rest_framework.test import APITestCase + +from openforms.contrib.objects_api.clients import get_objects_client +from openforms.contrib.objects_api.helpers import prepare_data_for_registration +from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory +from openforms.forms.tests.factories import FormVariableFactory +from openforms.logging.models import TimelineLogProxy +from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig +from openforms.registrations.contrib.objects_api.plugin import ObjectsAPIRegistration +from openforms.registrations.contrib.objects_api.typing import RegistrationOptionsV2 +from openforms.submissions.tests.factories import SubmissionFactory +from openforms.utils.tests.vcr import OFVCRMixin + +from ....service import prefill_variables + +VCR_TEST_FILES = Path(__file__).parent / "files" + + +class ObjectsAPIPrefillPluginTests(OFVCRMixin, APITestCase): + """This test case requires the Objects & Objecttypes API to be running. + See the relevant Docker compose in the ``docker/`` folder. + """ + + VCR_TEST_FILES = VCR_TEST_FILES + + def setUp(self): + super().setUp() + + config_patcher = patch( + "openforms.registrations.contrib.objects_api.models.ObjectsAPIConfig.get_solo", + return_value=ObjectsAPIConfig(), + ) + self.mock_get_config = config_patcher.start() + self.addCleanup(config_patcher.stop) + + self.objects_api_group = ObjectsAPIGroupConfigFactory.create( + for_test_docker_compose=True + ) + + def test_prefill_values_happy_flow(self): + # We manually create the objects instance as if it was created upfront by some external party + with get_objects_client(self.objects_api_group) as client: + created_obj = client.create_object( + record_data=prepare_data_for_registration( + data={ + "name": {"last.name": "My last name"}, + "age": 45, + }, + objecttype_version=3, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + ) + + submission = SubmissionFactory.from_components( + initial_data_reference=created_obj["uuid"], + components_list=[ + { + "type": "textfield", + "key": "age", + "label": "Age", + }, + { + "type": "textfield", + "key": "lastName", + "label": "Last name", + }, + ], + ) + FormVariableFactory.create( + form=submission.form, + prefill_plugin="objects_api", + prefill_options={ + "objects_api_group": self.objects_api_group.pk, + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 3, + "variables_mapping": [ + {"variable_key": "lastName", "target_path": ["name", "last.name"]}, + {"variable_key": "age", "target_path": ["age"]}, + ], + }, + ) + + prefill_variables(submission=submission) + state = submission.load_submission_value_variables_state() + + self.assertEqual(TimelineLogProxy.objects.count(), 1) + logs = TimelineLogProxy.objects.get() + + self.assertEqual(logs.extra_data["log_event"], "prefill_retrieve_success") + self.assertEqual(logs.extra_data["plugin_id"], "objects_api") + self.assertEqual(state.variables["lastName"].value, "My last name") + self.assertEqual(state.variables["age"].value, 45) + + def test_prefill_values_when_reference_not_found(self): + submission = SubmissionFactory.from_components( + initial_data_reference="048a37ca-a602-4158-9e60-9f06f3e47e2a", + components_list=[ + { + "type": "textfield", + "key": "age", + "label": "Age", + }, + { + "type": "textfield", + "key": "lastName", + "label": "Last name", + }, + ], + ) + FormVariableFactory.create( + form=submission.form, + prefill_plugin="objects_api", + prefill_options={ + "objects_api_group": self.objects_api_group.pk, + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 3, + "variables_mapping": [ + {"variable_key": "lastName", "target_path": ["name", "last.name"]}, + {"variable_key": "age", "target_path": ["age"]}, + ], + }, + ) + + prefill_variables(submission=submission) + state = submission.load_submission_value_variables_state() + + self.assertEqual(TimelineLogProxy.objects.count(), 1) + logs = TimelineLogProxy.objects.get() + + self.assertEqual(logs.extra_data["log_event"], "prefill_retrieve_failure") + self.assertEqual(logs.extra_data["plugin_id"], "objects_api") + self.assertIsNone(state.variables["lastName"].value) + self.assertIsNone(state.variables["age"].value) + + def test_prefill_values_when_reference_returns_empty_values(self): + # We manually create the objects instance as if it was created upfront by some external party + with get_objects_client(self.objects_api_group) as client: + created_obj = client.create_object( + record_data=prepare_data_for_registration( + data={}, + objecttype_version=3, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + ) + + submission = SubmissionFactory.from_components( + initial_data_reference=created_obj["uuid"], + components_list=[ + { + "type": "textfield", + "key": "age", + "label": "Age", + }, + { + "type": "textfield", + "key": "lastName", + "label": "Last name", + }, + ], + ) + FormVariableFactory.create( + form=submission.form, + prefill_plugin="objects_api", + prefill_options={ + "objects_api_group": self.objects_api_group.pk, + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 3, + "variables_mapping": [ + {"variable_key": "lastName", "target_path": ["name", "last.name"]}, + {"variable_key": "age", "target_path": ["age"]}, + ], + }, + ) + + prefill_variables(submission=submission) + state = submission.load_submission_value_variables_state() + + self.assertEqual(TimelineLogProxy.objects.count(), 1) + logs = TimelineLogProxy.objects.get() + + self.assertEqual(logs.extra_data["log_event"], "prefill_retrieve_empty") + self.assertEqual(logs.extra_data["plugin_id"], "objects_api") + self.assertIsNone(state.variables["lastName"].value) + self.assertIsNone(state.variables["age"].value) + + def test_prefilled_values_are_updated_in_the_object(self): + """ + This tests that a created object in the ObjectsAPI prefills the form variables (components) as + expected and then (in the same submission) we make sure that we can update the object. + """ + # We manually create the objects instance as if it was created upfront by some external party + with get_objects_client(self.objects_api_group) as client: + created_obj = client.create_object( + record_data=prepare_data_for_registration( + data={ + "name": {"last.name": "My last name"}, + "age": 45, + }, + objecttype_version=3, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + ) + + submission = SubmissionFactory.from_components( + initial_data_reference=created_obj["uuid"], + components_list=[ + { + "type": "textfield", + "key": "age", + "label": "Age", + }, + { + "type": "textfield", + "key": "lastName", + "label": "Last name", + }, + ], + submitted_data={ + "age": 40, + "lastName": "My updated last name", + }, + form__registration_backend="objects_api", + with_report=False, + ) + FormVariableFactory.create( + form=submission.form, + prefill_plugin="objects_api", + prefill_options={ + "objects_api_group": self.objects_api_group.pk, + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 3, + "variables_mapping": [ + {"variable_key": "lastName", "target_path": ["name", "last.name"]}, + {"variable_key": "age", "target_path": ["age"]}, + ], + }, + ) + + prefill_variables(submission=submission) + + v2_options: RegistrationOptionsV2 = { + "version": 2, + "objects_api_group": self.objects_api_group, + # See the docker compose fixtures for more info on these values: + "objecttype": UUID("8e46e0a5-b1b4-449b-b9e9-fa3cea655f48"), + "objecttype_version": 3, + "variables_mapping": [ + { + "variable_key": "age", + "target_path": ["age"], + }, + {"variable_key": "lastName", "target_path": ["name", "last.name"]}, + ], + "update_existing_object": True, + } + + plugin = ObjectsAPIRegistration("objects_api") + result = plugin.register_submission(submission, v2_options) + + assert result is not None + + self.assertTrue(result["uuid"], created_obj["uuid"]) + self.assertEqual(result["record"]["data"]["age"], 40) + self.assertEqual( + result["record"]["data"]["name"]["last.name"], "My updated last name" + ) diff --git a/src/openforms/prefill/service.py b/src/openforms/prefill/service.py new file mode 100644 index 0000000000..d0f334bcd4 --- /dev/null +++ b/src/openforms/prefill/service.py @@ -0,0 +1,172 @@ +""" +This package holds the base module structure for the pre-fill plugins used in Open Forms. + +Various sources exist that can be consulted to fetch data for an active session, +where the BSN, CoC number... can be used to retrieve this data. Think of pre-filling +the address details of a person after logging in with DigiD. + +The package integrates with the form builder such that it's possible for every form +field to a) select which pre-fill plugin to use and which value to use from the fetched +result and b) define a user-defined variable in which the ``prefill_options`` are configured. +Plugins can be registered using a similar approach to the registrations +package. Each plugin is responsible for exposing which attributes/data fragments are +available, and for performing the actual look-up. Plugins receive the +:class:`openforms.submissions.models.Submission` instance that represents the current +form session of an end-user. + +Prefill values are embedded as default values for form fields, dynamically for every +user session using the component rewrite functionality in the serializers. + +So, to recap: + +1. Plugins are defined and registered +2. When editing form definitions in the admin, content editors can opt-in to pre-fill + functionality. They select the desired plugin, and then the desired attribute from + that plugin. +3. Content editors can also define a user-defined variable and configure the plugin and + the necessary options by selecting the desired choices for the ``prefill_options``. +4. End-user starts the form and logs in, thereby creating a session/``Submission`` +5. The submission-specific form definition configuration is enhanced with the pre-filled + form field default values. +""" + +import logging +from collections import defaultdict + +import elasticapm +from glom import Path, PathAccessError, glom + +from openforms.formio.service import FormioConfigurationWrapper +from openforms.forms.models import FormVariable +from openforms.submissions.models import Submission +from openforms.variables.constants import FormVariableSources + +from .registry import Registry +from .sources.component import ( + fetch_prefill_values as fetch_prefill_values_for_component, +) +from .sources.user_defined import ( + fetch_prefill_values as fetch_prefill_values_for_user_defined, +) + +logger = logging.getLogger(__name__) + + +def inject_prefill( + configuration_wrapper: FormioConfigurationWrapper, submission: Submission +) -> None: + """ + Mutates each component found in configuration according to the prefilled values. + + :param configuration_wrapper: The Formiojs JSON schema wrapper describing an entire + form or an individual component within the form. + :param submission: The :class:`openforms.submissions.models.Submission` instance + that holds the values of the prefill data. The prefill data was fetched earlier, + see :func:`prefill_variables`. + + The prefill values are looped over by key: value, and for each value the matching + component is looked up to normalize it in the context of the component. + """ + from openforms.formio.service import normalize_value_for_component + + prefilled_data = submission.get_prefilled_data() + for key, prefill_value in prefilled_data.items(): + try: + component = configuration_wrapper[key] + except KeyError: + # The component to prefill is not in this step + continue + + if not (prefill := component.get("prefill")): + continue + if not prefill.get("plugin"): + continue + if not prefill.get("attribute"): + continue + + default_value = component.get("defaultValue") + # 1693: we need to normalize values according to the format expected by the + # component. For example, (some) prefill plugins return postal codes without + # space between the digits and the letters. + prefill_value = normalize_value_for_component(component, prefill_value) + + if prefill_value != default_value and default_value is not None: + logger.info( + "Overwriting non-null default value for component %r", + component, + ) + component["defaultValue"] = prefill_value + + +@elasticapm.capture_span(span_type="app.prefill") +def prefill_variables(submission: Submission, register: Registry | None = None) -> None: + """Update the submission variables state with the fetched attribute values. + + For each submission value variable that need to be prefilled, the according plugin will + be used to fetch the value. If ``register`` is not specified, the default registry instance + will be used. + """ + from openforms.formio.service import normalize_value_for_component + + from .registry import register as default_register + + register = register or default_register + + state = submission.load_submission_value_variables_state() + variables_to_prefill = state.get_prefill_variables() + + component_variables: list[FormVariable] = [] + user_defined_variables: list[FormVariable] = [] + for variable in variables_to_prefill: + assert variable.form_variable is not None + + if ( + variable.form_variable.source == FormVariableSources.component + and variable.form_variable.prefill_attribute + ): + component_variables.append(variable.form_variable) + elif ( + variable.form_variable.source == FormVariableSources.user_defined + and variable.form_variable.prefill_options + ): + user_defined_variables.append(variable.form_variable) + + total_config_wrapper = submission.total_configuration_wrapper + prefill_data: defaultdict[str, (dict[str, str] | str)] = defaultdict(dict) + + # Component source prefill + if component_variables: + if results := fetch_prefill_values_for_component( + submission, register, component_variables + ): + + for form_variable in component_variables: + try: + prefill_value = glom( + results, + Path( + form_variable.prefill_plugin, + form_variable.prefill_identifier_role, + form_variable.prefill_attribute, + ), + ) + except PathAccessError: + continue + else: + component = total_config_wrapper[form_variable.key] + prefill_value = normalize_value_for_component( + component, prefill_value + ) + prefill_data[form_variable.key] = prefill_value + + # User defined source prefill + if user_defined_variables: + if results := fetch_prefill_values_for_user_defined( + submission, register, user_defined_variables + ): + for form_variable in user_defined_variables: + for mapping in form_variable.prefill_options["variables_mapping"]: + prefill_value = results[mapping["variable_key"]] + prefill_data[mapping["variable_key"]] = prefill_value + + state.save_prefill_data(prefill_data) diff --git a/src/openforms/prefill/sources/__init__.py b/src/openforms/prefill/sources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/prefill/sources/component.py b/src/openforms/prefill/sources/component.py new file mode 100644 index 0000000000..a6cd5f9e3e --- /dev/null +++ b/src/openforms/prefill/sources/component.py @@ -0,0 +1,78 @@ +import logging +from collections import defaultdict +from typing import Any + +import elasticapm +from glom import Path, assign +from zgw_consumers.concurrent import parallel + +from openforms.forms.models import FormVariable +from openforms.plugins.exceptions import PluginNotEnabled +from openforms.submissions.models import Submission + +from ..registry import Registry + +logger = logging.getLogger(__name__) + + +def fetch_prefill_values( + submission: Submission, register: Registry, form_variables: list[FormVariable] +) -> dict[str, dict[str, Any]]: + # local import to prevent AppRegistryNotReady: + from openforms.logging import logevent + + # grouped_fields is a dict of the following shape: + # {"plugin_id": {"identifier_role": ["attr_1", "attr_2"]}} + # "identifier_role" is either "main" or "authorizee" + grouped_fields: defaultdict[str, defaultdict[str, list[str]]] = defaultdict( + lambda: defaultdict(list) + ) + + for form_variable in form_variables: + plugin_id = form_variable.prefill_plugin + identifier_role = form_variable.prefill_identifier_role + attribute_name = form_variable.prefill_attribute + + grouped_fields[plugin_id][identifier_role].append(attribute_name) + + @elasticapm.capture_span(span_type="app.prefill") + def invoke_plugin( + item: tuple[str, str, list[str]] + ) -> tuple[str, str, dict[str, Any]]: + plugin_id, identifier_role, fields = item + plugin = register[plugin_id] + if not plugin.is_enabled: + raise PluginNotEnabled() + + try: + values = plugin.get_prefill_values(submission, fields, identifier_role) + except Exception as e: + logger.exception(f"exception in prefill plugin '{plugin_id}'") + logevent.prefill_retrieve_failure(submission, plugin, e) + values = {} + else: + if values: + logevent.prefill_retrieve_success(submission, plugin, fields) + else: + logevent.prefill_retrieve_empty(submission, plugin, fields) + + return plugin_id, identifier_role, values + + invoke_plugin_args = [] + for plugin_id, field_groups in grouped_fields.items(): + for identifier_role, fields in field_groups.items(): + invoke_plugin_args.append((plugin_id, identifier_role, fields)) + + with parallel() as executor: + results = executor.map(invoke_plugin, invoke_plugin_args) + + collected_results = {} + for plugin_id, identifier_role, values_dict in list(results): + assign( + collected_results, + Path(plugin_id, identifier_role), + values_dict, + missing=dict, + ) + + return collected_results diff --git a/src/openforms/prefill/sources/user_defined.py b/src/openforms/prefill/sources/user_defined.py new file mode 100644 index 0000000000..3b97cb22ce --- /dev/null +++ b/src/openforms/prefill/sources/user_defined.py @@ -0,0 +1,36 @@ +import logging +from typing import Any + +from openforms.forms.models import FormVariable +from openforms.submissions.models import Submission + +from ..registry import Registry + +logger = logging.getLogger(__name__) + + +def fetch_prefill_values( + submission: Submission, + register: Registry, + form_variables: list[FormVariable], +) -> dict[str, (dict[str, Any] | dict[str, str])]: + # local import to prevent AppRegistryNotReady: + from openforms.logging import logevent + + values = {} + for form_var in form_variables: + plugin = register[form_var.prefill_plugin] + + try: + values = plugin.get_prefill_values_from_mappings(submission, form_var) + except Exception as e: + logger.exception(f"exception in prefill plugin '{plugin.identifier}'") + logevent.prefill_retrieve_failure(submission, plugin, e) + values = {} + else: + if values: + logevent.prefill_retrieve_success(submission, plugin, values) + else: + logevent.prefill_retrieve_empty(submission, plugin, values) + + return values diff --git a/src/openforms/prefill/tests/test_prefill_hook.py b/src/openforms/prefill/tests/test_prefill_hook.py index b63968c8c8..d0302bd2dd 100644 --- a/src/openforms/prefill/tests/test_prefill_hook.py +++ b/src/openforms/prefill/tests/test_prefill_hook.py @@ -16,11 +16,11 @@ from openforms.submissions.models import Submission, SubmissionValueVariable from openforms.submissions.tests.factories import SubmissionFactory -from .. import inject_prefill, prefill_variables from ..base import BasePlugin from ..constants import IdentifierRoles from ..contrib.demo.plugin import DemoPrefill from ..registry import Registry, register as prefill_register +from ..service import inject_prefill, prefill_variables register = Registry() diff --git a/src/openforms/prefill/tests/test_prefill_variables.py b/src/openforms/prefill/tests/test_prefill_variables.py index eab98e54b1..747929b85c 100644 --- a/src/openforms/prefill/tests/test_prefill_variables.py +++ b/src/openforms/prefill/tests/test_prefill_variables.py @@ -20,7 +20,7 @@ SubmissionStepFactory, ) -from .. import prefill_variables +from ..service import prefill_variables CONFIGURATION = { "display": "form", @@ -50,8 +50,9 @@ class PrefillVariablesTests(TestCase): + @patch( - "openforms.prefill._fetch_prefill_values", + "openforms.prefill.service.fetch_prefill_values_for_component", return_value={ "demo": { "main": {"random_string": "Not so random string", "random_number": 123} @@ -87,7 +88,7 @@ def test_applying_prefill_plugins(self, m_prefill): ) @patch( - "openforms.prefill._fetch_prefill_values", + "openforms.prefill.service.fetch_prefill_values_for_component", return_value={ "postcode": {"main": {"static": "1015CJ"}}, "birthDate": {"main": {"static": "19990615"}}, diff --git a/src/openforms/prefill/utils.py b/src/openforms/prefill/utils.py new file mode 100644 index 0000000000..3c20251f47 --- /dev/null +++ b/src/openforms/prefill/utils.py @@ -0,0 +1,18 @@ +from typing import Any + +from glom import Path, PathAccessError, glom + + +def find_in_dicts(*dicts: dict[str, Any], path: Path) -> str | None: + """ + Given a specific path, look up the value in the specified sequence of dictionaries. + + :param dicts: a sequence of dictionaries to look up in. + :param path: an :class:`Path` instance which contains the segments of the path. + :return: an str (the found value) or None. + """ + for data in dicts: + try: + return glom(data, path) + except PathAccessError: + continue diff --git a/src/openforms/submissions/api/viewsets.py b/src/openforms/submissions/api/viewsets.py index b40acd9cfc..ebe974b574 100644 --- a/src/openforms/submissions/api/viewsets.py +++ b/src/openforms/submissions/api/viewsets.py @@ -24,7 +24,7 @@ from openforms.formio.service import FormioData from openforms.forms.models import FormStep from openforms.logging import logevent -from openforms.prefill import prefill_variables +from openforms.prefill.service import prefill_variables from openforms.utils.patches.rest_framework_nested.viewsets import NestedViewSetMixin from ..attachments import attach_uploads_to_submission_step diff --git a/src/openforms/submissions/tests/test_start_submission.py b/src/openforms/submissions/tests/test_start_submission.py index ec46588ad2..46351e1cf6 100644 --- a/src/openforms/submissions/tests/test_start_submission.py +++ b/src/openforms/submissions/tests/test_start_submission.py @@ -23,11 +23,7 @@ from rest_framework.test import APITestCase from openforms.authentication.service import FORM_AUTH_SESSION_KEY, AuthAttribute -from openforms.forms.tests.factories import ( - FormFactory, - FormStepFactory, - FormVariableFactory, -) +from openforms.forms.tests.factories import FormFactory, FormStepFactory from ..constants import SUBMISSIONS_SESSION_KEY, SubmissionValueVariableSources from ..models import Submission, SubmissionValueVariable @@ -203,12 +199,20 @@ def test_start_submission_bad_form_url(self): @patch("openforms.logging.logevent._create_log") def test_start_submission_with_prefill(self, mock_logevent): - FormVariableFactory.create( + FormStepFactory.create( form=self.form, - form_definition=self.step.form_definition, - prefill_plugin="demo", - prefill_attribute="random_string", + form_definition__configuration={ + "components": [ + { + "type": "textfield", + "key": "test-key", + "label": "Test label", + "prefill": {"plugin": "demo", "attribute": "random_string"}, + } + ] + }, ) + body = { "form": f"http://testserver.com{self.form_url}", "formUrl": "http://testserver.com/my-form", diff --git a/src/openforms/submissions/tests/test_submission_step_validate.py b/src/openforms/submissions/tests/test_submission_step_validate.py index 7b1077d10a..d007e731ed 100644 --- a/src/openforms/submissions/tests/test_submission_step_validate.py +++ b/src/openforms/submissions/tests/test_submission_step_validate.py @@ -16,7 +16,7 @@ FormStepFactory, FormVariableFactory, ) -from openforms.prefill import prefill_variables +from openforms.prefill.service import prefill_variables from openforms.variables.constants import FormVariableDataTypes from ..models import SubmissionValueVariable @@ -209,7 +209,7 @@ def test_prefilled_data_normalised(self): @tag("gh-1899") @patch( - "openforms.prefill._fetch_prefill_values", + "openforms.prefill.service.fetch_prefill_values_for_component", return_value={ "postcode": {"main": {"static": "1015CJ"}}, }, diff --git a/src/openforms/variables/tests/test_views.py b/src/openforms/variables/tests/test_views.py index 3168aceb3f..368f582669 100644 --- a/src/openforms/variables/tests/test_views.py +++ b/src/openforms/variables/tests/test_views.py @@ -80,6 +80,7 @@ def get_initial_value(self, *args, **kwargs): "prefill_plugin": "", "prefill_attribute": "", "prefill_identifier_role": IdentifierRoles.main, + "prefill_options": {}, "data_type": FormVariableDataTypes.datetime, "data_format": "", "is_sensitive_data": False, @@ -169,6 +170,7 @@ def get_variables(self) -> list[FormVariable]: "prefill_plugin": "", "prefill_attribute": "", "prefill_identifier_role": IdentifierRoles.main, + "prefill_options": {}, "data_type": FormVariableDataTypes.string, "data_format": "", "is_sensitive_data": False,