From a912c98fca99e7f737ccf062b579d5d0a0ebeb8d Mon Sep 17 00:00:00 2001 From: vasileios Date: Thu, 22 Aug 2024 16:43:34 +0200 Subject: [PATCH] [#4396] First attempt for objects_api prefill plugin --- docs/configuration/prefill/index.rst | 1 + docs/developers/plugins/prefill_plugins.rst | 3 + src/openforms/conf/base.py | 1 + src/openforms/forms/admin/form_variable.py | 2 + .../0098_formvariable_prefill_options.py | 20 +++ src/openforms/forms/models/form_variable.py | 5 + .../prefill/PrefillConfigurationForm.js | 5 +- .../formio_builder/WebformBuilder.js | 5 +- .../js/components/formio_builder/plugins.js | 32 ++++- src/openforms/prefill/api/serializers.py | 33 +++++ src/openforms/prefill/api/urls.py | 24 +++- src/openforms/prefill/api/views.py | 106 +++++++++++++++- src/openforms/prefill/base.py | 7 +- .../prefill/contrib/objects_api/__init__.py | 3 + .../prefill/contrib/objects_api/apps.py | 12 ++ .../prefill/contrib/objects_api/constants.py | 19 +++ .../prefill/contrib/objects_api/plugin.py | 119 ++++++++++++++++++ .../submissions/models/submission.py | 6 + 18 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 src/openforms/forms/migrations/0098_formvariable_prefill_options.py create mode 100644 src/openforms/prefill/contrib/objects_api/__init__.py create mode 100644 src/openforms/prefill/contrib/objects_api/apps.py create mode 100644 src/openforms/prefill/contrib/objects_api/constants.py create mode 100644 src/openforms/prefill/contrib/objects_api/plugin.py diff --git a/docs/configuration/prefill/index.rst b/docs/configuration/prefill/index.rst index a090bbf023..410ec0edd1 100644 --- a/docs/configuration/prefill/index.rst +++ b/docs/configuration/prefill/index.rst @@ -12,3 +12,4 @@ Prefill plugins kvk stuf_bg suwinet + objects_api diff --git a/docs/developers/plugins/prefill_plugins.rst b/docs/developers/plugins/prefill_plugins.rst index e906ef3f5d..004929714e 100644 --- a/docs/developers/plugins/prefill_plugins.rst +++ b/docs/developers/plugins/prefill_plugins.rst @@ -25,6 +25,9 @@ You can find an example implementation in :mod:`openforms.prefill.contrib.demo`. Implementation -------------- +Plugins must be added to the INSTALLED_APPS :mod:`openforms.conf.base`. See the demo app as an example +("openforms.prefill.contrib.demo.apps.DemoApp") + Plugins must implement the interface from :class:`openforms.prefill.base.BasePlugin`. It's safe to use this as a base class. diff --git a/src/openforms/conf/base.py b/src/openforms/conf/base.py index e6411494aa..53c8847e0c 100644 --- a/src/openforms/conf/base.py +++ b/src/openforms/conf/base.py @@ -228,6 +228,7 @@ "openforms.prefill.contrib.stufbg.apps.StufBgApp", "openforms.prefill.contrib.haalcentraal_brp.apps.HaalCentraalBRPApp", "openforms.prefill.contrib.suwinet.apps.SuwinetApp", + "openforms.prefill.contrib.objects_api.apps.ObjectsApiApp", "openforms.authentication", "openforms.authentication.contrib.demo.apps.DemoApp", "openforms.authentication.contrib.outage.apps.DemoOutageApp", diff --git a/src/openforms/forms/admin/form_variable.py b/src/openforms/forms/admin/form_variable.py index 64674c37be..99402fa0dd 100644 --- a/src/openforms/forms/admin/form_variable.py +++ b/src/openforms/forms/admin/form_variable.py @@ -18,6 +18,7 @@ class FormVariableAdmin(admin.ModelAdmin): "prefill_plugin", "prefill_attribute", "prefill_identifier_role", + "prefill_options", "data_type", "is_sensitive_data", "initial_value", @@ -31,6 +32,7 @@ class FormVariableAdmin(admin.ModelAdmin): "prefill_plugin", "prefill_attribute", "prefill_identifier_role", + "prefill_options", "data_type", "data_format", "is_sensitive_data", diff --git a/src/openforms/forms/migrations/0098_formvariable_prefill_options.py b/src/openforms/forms/migrations/0098_formvariable_prefill_options.py new file mode 100644 index 0000000000..35b28e3fb2 --- /dev/null +++ b/src/openforms/forms/migrations/0098_formvariable_prefill_options.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.15 on 2024-08-21 11:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("forms", "0097_v267_to_v270"), + ] + + operations = [ + migrations.AddField( + model_name="formvariable", + name="prefill_options", + field=models.JSONField( + blank=True, default=dict, verbose_name="prefill options" + ), + ), + ] diff --git a/src/openforms/forms/models/form_variable.py b/src/openforms/forms/models/form_variable.py index 1aa0493cd8..e9112ba22b 100644 --- a/src/openforms/forms/models/form_variable.py +++ b/src/openforms/forms/models/form_variable.py @@ -161,6 +161,11 @@ class FormVariable(models.Model): default=IdentifierRoles.main, max_length=100, ) + prefill_options = models.JSONField( + _("prefill options"), + default=dict, + blank=True, + ) data_type = models.CharField( verbose_name=_("data type"), help_text=_("The type of the value that will be associated with this variable"), diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js index a107cc29e9..6fec2fd925 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js @@ -143,9 +143,10 @@ const AttributeField = () => { } = useAsync(async () => { if (!plugin) return []; - const endpoint = `/api/v2/prefill/plugins/${plugin}/attributes`; + // const endpoint = `/api/v2/prefill/plugins/${plugin}/attributes`; + const endpoint = `/api/v2/prefill/plugins/objects-api/objecttypes/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10/versions/1/attributes`; // XXX: clean up error handling here at some point... - const response = await get(endpoint); + const response = await get(endpoint, {objects_api_group: '2'}); if (!response.ok) throw response.data; return response.data.map(attribute => [attribute.id, attribute.label]); }, [plugin]); diff --git a/src/openforms/js/components/formio_builder/WebformBuilder.js b/src/openforms/js/components/formio_builder/WebformBuilder.js index 88f3a4f826..d73f2e1e18 100644 --- a/src/openforms/js/components/formio_builder/WebformBuilder.js +++ b/src/openforms/js/components/formio_builder/WebformBuilder.js @@ -20,6 +20,9 @@ import {currentTheme} from 'utils/theme'; import { getPrefillAttributes, + getPrefillObjectsAPIGroups, + getPrefillObjectsAPIObjecttypeVersions, + getPrefillObjectsAPIObjecttypes, getPrefillPlugins, getRegistrationAttributes, getValidatorPlugins, @@ -166,7 +169,7 @@ class WebformBuilder extends WebformBuilderFormio { getValidatorPlugins={getValidatorPlugins} getRegistrationAttributes={getRegistrationAttributes} getPrefillPlugins={getPrefillPlugins} - getPrefillAttributes={getPrefillAttributes} + getPrefillAttributes={getPrefillObjectsAPIObjecttypeVersions} getFileTypes={async () => FILE_TYPES} serverUploadLimit={MAX_FILE_UPLOAD_SIZE} getDocumentTypes={async () => await getAvailableDocumentTypes(this)} diff --git a/src/openforms/js/components/formio_builder/plugins.js b/src/openforms/js/components/formio_builder/plugins.js index 50986a9d55..cc877311c1 100644 --- a/src/openforms/js/components/formio_builder/plugins.js +++ b/src/openforms/js/components/formio_builder/plugins.js @@ -18,7 +18,35 @@ export const getPrefillPlugins = async () => { return resp.data; }; -export const getPrefillAttributes = async plugin => { - const resp = await get(`/api/v2/prefill/plugins/${plugin}/attributes`); +// export const getPrefillAttributes = async plugin => { +// // const resp = await get(`/api/v2/prefill/plugins/${plugin}/attributes`); +// const resp = await get(`/api/v2/prefill/plugins/objects-api/groups`); +// return resp.data; +// }; + +// export const getPrefillObjectsAPIGroups = async () => { +// const resp = await get(`/api/v2/prefill/plugins/objects-api/groups`); +// return resp.data; +// }; + +// export const getPrefillObjectsAPIObjecttypes = async () => { +// const resp = await get(`/api/v2/prefill/plugins/objects-api/objecttypes/2`); +// return resp.data; +// }; + +// export const getPrefillObjectsAPIObjecttypeVersions = async () => { +// const resp = await get( +// `/api/v2/prefill/plugins/objects-api/objecttypes/2/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10/versions` +// ); +// return resp.data; +// }; + +export const getPrefillObjectsAPIObjecttypeVersions = async () => { + const resp = await get( + `/api/v2/prefill/plugins/objects-api/objecttypes/ac1fa3f8-fb2a-4fcb-b715-d480aceeda10/versions/1/attributes`, + { + objects_api_group: '2', + } + ); return resp.data; }; diff --git a/src/openforms/prefill/api/serializers.py b/src/openforms/prefill/api/serializers.py index a5f902ca43..108891c5df 100644 --- a/src/openforms/prefill/api/serializers.py +++ b/src/openforms/prefill/api/serializers.py @@ -64,3 +64,36 @@ class PrefillAttributeSerializer(serializers.Serializer): label=_("Label"), help_text=_("The human-readable name for an attribute."), ) + + +class PrefillObjectsAPIObjecttypeSerializer(serializers.Serializer): + # Keys are defined in camel case as this is what we get from the Objecttype API + url = serializers.URLField( + label=_( + "URL reference to this object. This is the unique identification and location of this object." + ), + ) + uuid = serializers.UUIDField(label=_("Unique identifier (UUID4).")) + name = serializers.CharField(label=_("Name of the object type.")) + namePlural = serializers.CharField(label=_("Plural name of the object type.")) + dataClassification = serializers.CharField( + label=_("Confidential level of the object type.") + ) + + +class PrefillObjectsAPIObjecttypeVersionSerializer(serializers.Serializer): + version = serializers.IntegerField( + label=_("Integer version of the Objecttype."), + ) + status = serializers.CharField(label=_("Status of the object type version")) + + +class PrefillObjectsAPIAttributeSerializer(serializers.Serializer): + id = serializers.CharField( + label=_("ID"), + help_text=_("The unique attribute identifier"), + ) + label = serializers.CharField( + label=_("Label"), + help_text=_("The human-readable name for an attribute."), + ) diff --git a/src/openforms/prefill/api/urls.py b/src/openforms/prefill/api/urls.py index a43ea07f59..0974bdb6a8 100644 --- a/src/openforms/prefill/api/urls.py +++ b/src/openforms/prefill/api/urls.py @@ -1,8 +1,30 @@ from django.urls import path -from .views import PluginAttributesListView, PluginListView +from .views import ( + PluginAttributesListView, + PluginListView, + PluginObjectsAPIAttributesListView, + PluginObjectsAPIObjecttypeListView, + PluginObjectsAPIObjecttypeVersionListView, +) urlpatterns = [ + path( + # "plugins/objects-api/objecttypes/", + "plugins/objects-api/objecttypes", + PluginObjectsAPIObjecttypeListView.as_view(), + name="prefill-objects-api-objecttype-list", + ), + path( + "plugins/objects-api/objecttypes//versions", + PluginObjectsAPIObjecttypeVersionListView.as_view(), + name="prefill-objects-api-objecttype-version-list", + ), + path( + "plugins/objects-api/objecttypes//versions//attributes", + PluginObjectsAPIAttributesListView.as_view(), + name="prefill-objects-api-objecttype-attribute-list", + ), path("plugins", PluginListView.as_view(), name="prefill-plugin-list"), path( "plugins//attributes", diff --git a/src/openforms/prefill/api/views.py b/src/openforms/prefill/api/views.py index b0ecba2f6b..ec0f51df22 100644 --- a/src/openforms/prefill/api/views.py +++ b/src/openforms/prefill/api/views.py @@ -1,20 +1,37 @@ +from typing import Any + from django.utils.translation import gettext_lazy as _ -from drf_spectacular.utils import extend_schema, extend_schema_view +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view from rest_framework import authentication, permissions from rest_framework.exceptions import NotFound from rest_framework.views import APIView from openforms.api.views import ListMixin +from openforms.registrations.contrib.objects_api.api.serializers import ( + ObjectsAPIGroupInputSerializer, +) +from openforms.registrations.contrib.objects_api.client import get_objecttypes_client from ..registry import register from .serializers import ( ChoiceWrapper, PrefillAttributeSerializer, + PrefillObjectsAPIAttributeSerializer, + PrefillObjectsAPIObjecttypeSerializer, + PrefillObjectsAPIObjecttypeVersionSerializer, PrefillPluginQueryParameterSerializer, PrefillPluginSerializer, ) +OBJECTS_API_GROUP_QUERY_PARAMETER = OpenApiParameter( + name="objects_api_group", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + description=_("Which Objects API group to use."), +) + @extend_schema_view( get=extend_schema( @@ -67,3 +84,90 @@ def get_objects(self): choices = plugin.get_available_attributes() return [ChoiceWrapper(choice) for choice in choices] + + +@extend_schema_view( + get=extend_schema(summary=_("List available objecttypes for Objects API")), + parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], +) +class PluginObjectsAPIObjecttypeListView(ListMixin, APIView): + """ + List the available prefill objecttypes for Objects API plugin. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = PrefillObjectsAPIObjecttypeSerializer + + def get_objects(self) -> list[dict[str, Any]]: + input_serializer = ObjectsAPIGroupInputSerializer( + data=self.request.query_params + ) + input_serializer.is_valid(raise_exception=True) + + config_group = input_serializer.validated_data["objects_api_group"] + + with get_objecttypes_client(config_group) as client: + return client.list_objecttypes() + + +@extend_schema_view( + get=extend_schema(summary=_("List available objecttype versions for Objects API")), + parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], +) +class PluginObjectsAPIObjecttypeVersionListView(ListMixin, APIView): + """ + List the available prefill objecttype versions for Objects API plugin. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = PrefillObjectsAPIObjecttypeVersionSerializer + + def get_objects(self): + input_serializer = ObjectsAPIGroupInputSerializer( + data=self.request.query_params + ) + input_serializer.is_valid(raise_exception=True) + + config_group = input_serializer.validated_data["objects_api_group"] + objecttype_uuid = self.kwargs["objects_api_objecttype_uuid"] + + with get_objecttypes_client(config_group) as client: + return client.list_objecttype_versions(objecttype_uuid) + + +@extend_schema_view( + get=extend_schema(summary=_("List available attributes for Objects API")), + parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], +) +class PluginObjectsAPIAttributesListView(ListMixin, APIView): + """ + List the available attributes for Objects API plugin. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = PrefillObjectsAPIAttributeSerializer + + def get_objects(self): + plugin = register["objects_api"] + input_serializer = ObjectsAPIGroupInputSerializer( + data=self.request.query_params + ) + input_serializer.is_valid(raise_exception=True) + + config_group = input_serializer.validated_data["objects_api_group"] + choices = plugin.get_available_attributes( + reference={ + "objects_api_group": config_group, + "objects_api_objecttype_uuid": self.kwargs[ + "objects_api_objecttype_uuid" + ], + "objects_api_objecttype_version": self.kwargs[ + "objects_api_objecttype_version" + ], + } + ) + + return choices diff --git a/src/openforms/prefill/base.py b/src/openforms/prefill/base.py index 4d7eacda8c..0eb373770c 100644 --- a/src/openforms/prefill/base.py +++ b/src/openforms/prefill/base.py @@ -19,9 +19,14 @@ class BasePlugin(AbstractBasePlugin): for_components: Container[str] = AllComponentTypes() @staticmethod - def get_available_attributes() -> Iterable[tuple[str, str]]: + def get_available_attributes( + reference: dict[str, str] | None = None, + ) -> Iterable[tuple[str, str]]: """ Return a choice list of available attributes this plugin offers. + + :param reference: a dict based on which we retrieve the available attributes. + Can be used when we have dynamic lists of attributes. """ raise NotImplementedError( "You must implement the 'get_available_attributes' method." diff --git a/src/openforms/prefill/contrib/objects_api/__init__.py b/src/openforms/prefill/contrib/objects_api/__init__.py new file mode 100644 index 0000000000..7c438b5c34 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/__init__.py @@ -0,0 +1,3 @@ +""" +Objects API prefill plugin. +""" diff --git a/src/openforms/prefill/contrib/objects_api/apps.py b/src/openforms/prefill/contrib/objects_api/apps.py new file mode 100644 index 0000000000..816875ae66 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class ObjectsApiApp(AppConfig): + name = "openforms.prefill.contrib.objects_api" + label = "objects_api" + verbose_name = _("Objects API prefill plugin") + + def ready(self): + # register the plugin + from . import plugin # noqa diff --git a/src/openforms/prefill/contrib/objects_api/constants.py b/src/openforms/prefill/contrib/objects_api/constants.py new file mode 100644 index 0000000000..69a439d01c --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/constants.py @@ -0,0 +1,19 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class ObjectsAPIAttributes(models.TextChoices): + url = "url", _("Url") + uuid = "uuid", _("UUID") + type = "type", _("Type") + record_index = "record.index", _("Record > Index") + record_typeVersion = "record.typeVersion", _("Record > Type version") + + record_data_ = "", _("Record > Data") + + record_geometry = "record.geometry", _("Record > Geometry") + record_startAt = "record.startAt", _("Record > Start at") + record_endAt = "record.endAt", _("Record > End at") + record_registrationAt = "record.registrationAt", _("Record > Registration at") + record_correctionFor = "record.correctionFor", _("Record > Correction for") + record_correctedBy = "record_correctedBy", _("Record > Corrected by") diff --git a/src/openforms/prefill/contrib/objects_api/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py new file mode 100644 index 0000000000..6d5b626ed8 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -0,0 +1,119 @@ +import logging +from typing import Any, Iterable + +from django.utils.translation import gettext_lazy as _ + +from openforms.authentication.service import AuthAttribute +from openforms.registrations.contrib.objects_api.client import get_objecttypes_client +from openforms.registrations.contrib.objects_api.models import ObjectsAPIGroupConfig +from openforms.submissions.models import Submission +from openforms.typing import JSONEncodable + +from ...base import BasePlugin +from ...constants import IdentifierRoles +from ...registry import register + +logger = logging.getLogger(__name__) + +PLUGIN_IDENTIFIER = "objects_api" + + +def parse_schema_properties(schema, parent_key: str = "") -> list[tuple[str, str]]: + properties = [] + + if schema["type"] == "object": + for prop, prop_schema in schema.get("properties", {}).items(): + full_key = f"{parent_key}.{prop}" if parent_key else prop + prop_type = prop_schema.get("type", "unknown") + properties.append((full_key, prop_type)) + if prop_type == "object" or ( + prop_type == "array" and "items" in prop_schema + ): + properties.extend(parse_schema_properties(prop_schema, full_key)) + elif schema["type"] == "array": + items_schema = schema.get("items", {}) + if isinstance(items_schema, dict): + properties.extend(parse_schema_properties(items_schema, f"{parent_key}[]")) + elif isinstance(items_schema, list): + for i, item_schema in enumerate(items_schema): + properties.extend( + parse_schema_properties(item_schema, f"{parent_key}[{i}]") + ) + else: + properties.append((parent_key, schema["type"])) + + return properties + + +def fetch_schema( + group: ObjectsAPIGroupConfig, + objecttype_uuid: str, + objecttype_version: int, +): + with get_objecttypes_client(group) as client: + version = client.get_objecttype_version(objecttype_uuid, objecttype_version) + return version["jsonSchema"] + + +def retrieve_properties( + reference: dict[str, Any] | None = None, +): + schema = fetch_schema( + reference["objects_api_group"], + reference["objects_api_objecttype_uuid"], + reference["objects_api_objecttype_version"], + ) + properties = parse_schema_properties(schema) + return [{"id": prop[0], "label": f"{prop[0]} ({prop[1]})"} for prop in properties] + + +@register(PLUGIN_IDENTIFIER) +class ObjectsAPIPrefill(BasePlugin): + verbose_name = _("Objects API") + requires_auth = AuthAttribute.bsn + + @staticmethod + def get_available_attributes( + reference: dict[str, Any] | None = None, + ) -> Iterable[tuple[str, str]]: + assert reference is not None + return retrieve_properties(reference) + + @classmethod + def get_prefill_values( + cls, + submission: Submission, + attributes: list[str], + identifier_role: IdentifierRoles = IdentifierRoles.main, + ) -> dict[str, JSONEncodable]: + # for testing purposes, this will be defined in the modal by the user + # frontend is not ready yet + # options = submission.registration_backend.options + # config = ObjectsAPIGroupConfig.objects.get(pk=options["objects_api_group"]) + + # if object_uuid := submission.initial_data_reference: + # with get_objecttypes_client(config) as objecttypes_client: + # objecttype = objecttypes_client.get_objecttype_version( + # options["objecttype"], options["objecttype_version"] + # ) + + # if objecttype["status"] != "published": + # logger.warning( + # "object type '%s' is not published yet", objecttype["url"] + # ) + # return {} + + # if json_schema := objecttype.get("jsonSchema"): + # properties = parse_schema_properties(json_schema) + + # return {} + pass + + @classmethod + def get_co_sign_values( + cls, submission: Submission, identifier: str + ) -> tuple[dict[str, Any], str]: + pass + + def check_config(self): + pass diff --git a/src/openforms/submissions/models/submission.py b/src/openforms/submissions/models/submission.py index c3092db628..a6e19c5e98 100644 --- a/src/openforms/submissions/models/submission.py +++ b/src/openforms/submissions/models/submission.py @@ -360,6 +360,12 @@ class Meta: ), ] + # for testing purposes-need the initial_data_reference + # should be removed + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.initial_data_reference = "ac1fa3f8-fb2a-4fcb-b715-d480aceeda10" + def __str__(self): return _("{pk} - started on {started}").format( pk=self.pk or _("(unsaved)"),