From 59304cb0fb65e1cce270e3c382159a35cdca8f99 Mon Sep 17 00:00:00 2001 From: vasileios Date: Fri, 13 Sep 2024 15:41:42 +0200 Subject: [PATCH] [#4396] Moved ObjectsAPI views and urls to contrib, added basic plugin structure --- docs/configuration/prefill/index.rst | 1 + docs/configuration/prefill/objects_api.rst | 43 ++ docs/developers/plugins/prefill_plugins.rst | 3 + src/openapi.yaml | 381 ++++++++++-------- src/openforms/api/urls.py | 1 + src/openforms/conf/base.py | 1 + .../contrib/objects_api/api/__init__.py | 0 .../contrib/objects_api/api/filters.py | 44 ++ .../contrib/objects_api/api/serializers.py | 37 ++ src/openforms/contrib/objects_api/api/urls.py | 33 ++ .../contrib/objects_api/api/views.py | 108 +++++ .../contrib/objects_api/checks.py | 9 +- .../contrib/objects_api/json_schema.py | 0 ...lientTest.test_get_objecttype_version.yaml | 2 +- ...entTest.test_list_objecttype_versions.yaml | 2 +- ...typesClientTest.test_list_objecttypes.yaml | 9 +- ...tTest.test_list_objectypes_pagination.yaml | 6 +- .../objects_api/tests/test_json_schema.py | 0 .../tests/test_objecttypes_client.py | 28 +- .../components/admin/form_design/constants.js | 3 +- .../objectsapi/fields/DocumentTypes.js | 4 +- .../objectsapi/fields/ObjectTypeSelect.js | 4 +- .../fields/ObjectTypeVersionSelect.js | 6 +- .../registrations/objectsapi/mocks.js | 40 +- src/openforms/js/components/form/file.js | 2 +- src/openforms/prefill/api/urls.py | 11 +- .../prefill/contrib/objects_api/__init__.py | 3 + .../contrib/objects_api/api/__init__.py | 0 .../contrib/objects_api/api/serializers.py | 17 + .../prefill/contrib/objects_api/api/urls.py | 13 + .../prefill/contrib/objects_api/api/views.py | 64 +++ .../prefill/contrib/objects_api/apps.py | 12 + .../prefill/contrib/objects_api/plugin.py | 39 ++ .../contrib/objects_api/tests/__init__.py | 0 ...tTests.test_list_available_attributes.yaml | 49 +++ ...inEndpointTests.test_list_objecttypes.yaml | 51 +++ ...tTests.test_list_objecttypes_versions.yaml | 52 +++ .../objects_api/tests/test_endpoints.py | 161 ++++++++ .../contrib/objects_api/api/serializers.py | 34 +- .../contrib/objects_api/api/urls.py | 32 +- .../contrib/objects_api/api/views.py | 110 +---- .../contrib/objects_api/plugin.py | 2 +- .../objects_api/tests/test_api_endpoints.py | 20 +- 43 files changed, 1039 insertions(+), 398 deletions(-) create mode 100644 docs/configuration/prefill/objects_api.rst create mode 100644 src/openforms/contrib/objects_api/api/__init__.py create mode 100644 src/openforms/contrib/objects_api/api/filters.py create mode 100644 src/openforms/contrib/objects_api/api/serializers.py create mode 100644 src/openforms/contrib/objects_api/api/urls.py create mode 100644 src/openforms/contrib/objects_api/api/views.py rename src/openforms/{registrations => }/contrib/objects_api/checks.py (97%) rename src/openforms/{registrations => }/contrib/objects_api/json_schema.py (100%) rename src/openforms/{registrations => }/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_get_objecttype_version.yaml (97%) rename src/openforms/{registrations => }/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttype_versions.yaml (98%) rename src/openforms/{registrations => }/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttypes.yaml (81%) rename src/openforms/{registrations => }/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objectypes_pagination.yaml (56%) rename src/openforms/{registrations => }/contrib/objects_api/tests/test_json_schema.py (100%) rename src/openforms/{registrations => }/contrib/objects_api/tests/test_objecttypes_client.py (69%) create mode 100644 src/openforms/prefill/contrib/objects_api/__init__.py create mode 100644 src/openforms/prefill/contrib/objects_api/api/__init__.py create mode 100644 src/openforms/prefill/contrib/objects_api/api/serializers.py create mode 100644 src/openforms/prefill/contrib/objects_api/api/urls.py create mode 100644 src/openforms/prefill/contrib/objects_api/api/views.py create mode 100644 src/openforms/prefill/contrib/objects_api/apps.py create mode 100644 src/openforms/prefill/contrib/objects_api/plugin.py create mode 100644 src/openforms/prefill/contrib/objects_api/tests/__init__.py create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_available_attributes.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes_versions.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/test_endpoints.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/configuration/prefill/objects_api.rst b/docs/configuration/prefill/objects_api.rst new file mode 100644 index 0000000000..71ca2e45ff --- /dev/null +++ b/docs/configuration/prefill/objects_api.rst @@ -0,0 +1,43 @@ +.. todo:: This feature is still in development and not ready for production yet. + +.. _configuration_prefill_objects_api: + +=========== +Objects API +=========== + +The `Objects API`_ stores data records of which the structure and shape are defined by a particular object type +definition in the `Objecttypes API`_. These records can be used to pre-fill form fields if an object reference is +passed when the form is started. + +.. note:: + + The service may contain sensitive data. It is advised to require DigiD/eHerkenning authentication on the form. You + should be careful with how you pass the object references to the customers and set up the object type in a way that + makes authentication checks possible (e.g. by storing the expected BSN or KVK number). + +.. _`Objects API`: https://objects-and-objecttypes-api.readthedocs.io/en/latest/ +.. _`Objecttypes API`: https://objects-and-objecttypes-api.readthedocs.io/en/latest/ + + +Configuration +============= + +1. In Open Forms, navigate to: **Forms** +2. Click **Add form** +3. Define the necessary form details and add the desired components +4. Navigate to: **Variables** tab +5. Navigate to: **User defined** subtab +6. Click **Add variable** and fill in the data from the available options: + * **Plugin**: Choose the *Objects API* plugin + * **API Group**: Select the appropriate API group. These API groups should be set up by an administrator, + via **Admin** > **Configuration** > **Prefill plugins** > **Objects API** > **Manage API groups** + * **Objecttype**: Select the expected object type from the dropdown. + * **Mappings**: Configure which property from the Objects API record needs to be assigned to which form variable. + For each form variable you want to pre-fill, add a new mapping. Then, on the left select the desired form variable, + and on the right you can specify which property from the object type contains the value. + +7. Click **Save** +8. Save the form + +The Objects API configuration is now complete. diff --git a/docs/developers/plugins/prefill_plugins.rst b/docs/developers/plugins/prefill_plugins.rst index e906ef3f5d..6341320cc2 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`` setting in :mod:`openforms.conf.base`. See the demo +app (:class:`openforms.prefill.contrib.demo.apps.DemoApp`) as an example. + 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/openapi.yaml b/src/openapi.yaml index bbd7350ba5..3474414286 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -3451,6 +3451,166 @@ paths: $ref: '#/components/headers/X-Is-Form-Designer' Content-Language: $ref: '#/components/headers/Content-Language' + /api/v2/objects-api/catalogues: + get: + operationId: objects_api_catalogues_list + description: List the available catalogues. + summary: List available Catalogi from the provided Objects API group + parameters: + - in: query + name: objects_api_group + schema: + type: integer + description: The primary key of the Objects API group to use. The informatieobjecttypen + from the Catalogi API in this group will be returned. + required: true + tags: + - objects-api + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Catalogue' + description: '' + headers: + X-Session-Expires-In: + $ref: '#/components/headers/X-Session-Expires-In' + X-CSRFToken: + $ref: '#/components/headers/X-CSRFToken' + X-Is-Form-Designer: + $ref: '#/components/headers/X-Is-Form-Designer' + Content-Language: + $ref: '#/components/headers/Content-Language' + /api/v2/objects-api/informatieobjecttypen: + get: + operationId: objects_api_informatieobjecttypen_list + description: |- + List the available InformatieObjectTypen. + + Each InformatieObjectType is uniquely identified by its 'omschrijving', 'catalogus', + and beginning and end date. If multiple same InformatieObjectTypen exist for different dates, + only one entry is returned. + summary: List the available InformatieObjectTypen from the provided Objects + API group + parameters: + - in: query + name: catalogus_url + schema: + type: string + format: uri + default: '' + minLength: 1 + description: Filter informatieobjecttypen against this catalogus URL. + - in: query + name: objects_api_group + schema: + type: integer + description: The primary key of the Objects API group to use. The informatieobjecttypen + from the Catalogi API in this group will be returned. + required: true + tags: + - objects-api + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/InformatieObjectType' + description: '' + headers: + X-Session-Expires-In: + $ref: '#/components/headers/X-Session-Expires-In' + X-CSRFToken: + $ref: '#/components/headers/X-CSRFToken' + X-Is-Form-Designer: + $ref: '#/components/headers/X-Is-Form-Designer' + Content-Language: + $ref: '#/components/headers/Content-Language' + /api/v2/objects-api/object-types: + get: + operationId: objects_api_object_types_list + description: |- + List the available Objecttypes. + + Note that the response data is essentially proxied from the configured Objecttypes API. + parameters: + - in: query + name: objects_api_group + schema: + type: string + description: Which Objects API group to use. + tags: + - registration + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Objecttype' + description: '' + headers: + X-Session-Expires-In: + $ref: '#/components/headers/X-Session-Expires-In' + X-CSRFToken: + $ref: '#/components/headers/X-CSRFToken' + X-Is-Form-Designer: + $ref: '#/components/headers/X-Is-Form-Designer' + Content-Language: + $ref: '#/components/headers/Content-Language' + /api/v2/objects-api/object-types/{objecttype_uuid}/versions: + get: + operationId: objects_api_object_types_versions_list + description: |- + List the available versions for an Objecttype. + + Note that the response data is essentially proxied from the configured Objecttypes API. + parameters: + - in: query + name: objects_api_group + schema: + type: string + description: Which Objects API group to use. + - in: path + name: objecttype_uuid + schema: + type: string + format: uuid + required: true + tags: + - registration + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ObjecttypeVersion' + description: '' + headers: + X-Session-Expires-In: + $ref: '#/components/headers/X-Session-Expires-In' + X-CSRFToken: + $ref: '#/components/headers/X-CSRFToken' + X-Is-Form-Designer: + $ref: '#/components/headers/X-Is-Form-Designer' + Content-Language: + $ref: '#/components/headers/Content-Language' /api/v2/payment/plugins: get: operationId: payment_plugins_list @@ -3586,6 +3746,50 @@ paths: $ref: '#/components/headers/X-Is-Form-Designer' Content-Language: $ref: '#/components/headers/Content-Language' + /api/v2/prefill/plugins/objects-api/objecttypes/{objecttype_uuid}/versions/{objecttype_version}/properties: + get: + operationId: prefill_plugins_objects_api_objecttypes_versions_properties_list + description: List the available JSON properties defined on a particular objecttype. + summary: List available attributes for Objects API + parameters: + - in: query + name: objects_api_group + schema: + type: string + description: Which Objects API group to use. + - in: path + name: objecttype_uuid + schema: + type: string + format: uuid + required: true + - in: path + name: objecttype_version + schema: + type: integer + required: true + tags: + - prefill + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PrefillTargetPaths' + description: '' + headers: + X-Session-Expires-In: + $ref: '#/components/headers/X-Session-Expires-In' + X-CSRFToken: + $ref: '#/components/headers/X-CSRFToken' + X-Is-Form-Designer: + $ref: '#/components/headers/X-Is-Form-Designer' + Content-Language: + $ref: '#/components/headers/Content-Language' /api/v2/products: get: operationId: products_list @@ -3853,166 +4057,6 @@ paths: $ref: '#/components/headers/X-Is-Form-Designer' Content-Language: $ref: '#/components/headers/Content-Language' - /api/v2/registration/plugins/objects-api/catalogues: - get: - operationId: registration_plugins_objects_api_catalogues_list - description: List the available catalogues. - summary: List available Catalogi from the provided Objects API group - parameters: - - in: query - name: objects_api_group - schema: - type: integer - description: The primary key of the Objects API group to use. The informatieobjecttypen - from the Catalogi API in this group will be returned. - required: true - tags: - - registration - security: - - cookieAuth: [] - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Catalogue' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - /api/v2/registration/plugins/objects-api/informatieobjecttypen: - get: - operationId: registration_plugins_objects_api_informatieobjecttypen_list - description: |- - List the available InformatieObjectTypen. - - Each InformatieObjectType is uniquely identified by its 'omschrijving', 'catalogus', - and beginning and end date. If multiple same InformatieObjectTypen exist for different dates, - only one entry is returned. - summary: List the available InformatieObjectTypen from the provided Objects - API group - parameters: - - in: query - name: catalogus_url - schema: - type: string - format: uri - default: '' - minLength: 1 - description: Filter informatieobjecttypen against this catalogus URL. - - in: query - name: objects_api_group - schema: - type: integer - description: The primary key of the Objects API group to use. The informatieobjecttypen - from the Catalogi API in this group will be returned. - required: true - tags: - - registration - security: - - cookieAuth: [] - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/InformatieObjectType' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - /api/v2/registration/plugins/objects-api/object-types: - get: - operationId: registration_plugins_objects_api_object_types_list - description: |- - List the available Objecttypes. - - Note that the response data is essentially proxied from the configured Objecttypes API. - parameters: - - in: query - name: objects_api_group - schema: - type: string - description: Which Objects API group to use. - tags: - - registration - security: - - cookieAuth: [] - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Objecttype' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' - /api/v2/registration/plugins/objects-api/object-types/{submission_uuid}/versions: - get: - operationId: registration_plugins_objects_api_object_types_versions_list - description: |- - List the available versions for an Objecttype. - - Note that the response data is essentially proxied from the configured Objecttypes API. - parameters: - - in: query - name: objects_api_group - schema: - type: string - description: Which Objects API group to use. - - in: path - name: submission_uuid - schema: - type: string - format: uuid - required: true - tags: - - registration - security: - - cookieAuth: [] - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/ObjecttypeVersion' - description: '' - headers: - X-Session-Expires-In: - $ref: '#/components/headers/X-Session-Expires-In' - X-CSRFToken: - $ref: '#/components/headers/X-CSRFToken' - X-Is-Form-Designer: - $ref: '#/components/headers/X-Is-Form-Designer' - Content-Language: - $ref: '#/components/headers/Content-Language' /api/v2/registration/plugins/objects-api/target-paths: post: operationId: registration_plugins_objects_api_target_paths_create @@ -9629,6 +9673,23 @@ components: - id - label - requiresAuth + PrefillTargetPaths: + type: object + properties: + targetPath: + type: array + items: + type: string + title: Segment of a JSON path + description: Representation of the JSON target location as a list of string + segments. + jsonSchema: + type: object + additionalProperties: {} + description: Corresponding (sub) JSON Schema of the target path. + required: + - jsonSchema + - targetPath PrivacyPolicyInfo: type: object properties: diff --git a/src/openforms/api/urls.py b/src/openforms/api/urls.py index da9928bcad..332abb248a 100644 --- a/src/openforms/api/urls.py +++ b/src/openforms/api/urls.py @@ -115,6 +115,7 @@ ), path("authentication/", include("openforms.authentication.api.urls")), path("registration/", include("openforms.registrations.api.urls")), + path("objects-api/", include("openforms.contrib.objects_api.api.urls")), path("payment/", include("openforms.payments.api.urls")), path("dmn/", include("openforms.dmn.api.urls")), path("translations/", include("openforms.translations.urls")), diff --git a/src/openforms/conf/base.py b/src/openforms/conf/base.py index 3a7c3ddad3..70cec28102 100644 --- a/src/openforms/conf/base.py +++ b/src/openforms/conf/base.py @@ -229,6 +229,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/contrib/objects_api/api/__init__.py b/src/openforms/contrib/objects_api/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/contrib/objects_api/api/filters.py b/src/openforms/contrib/objects_api/api/filters.py new file mode 100644 index 0000000000..9c576308ed --- /dev/null +++ b/src/openforms/contrib/objects_api/api/filters.py @@ -0,0 +1,44 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers +from zgw_consumers.client import build_client + +from openforms.api.fields import PrimaryKeyRelatedAsChoicesField +from openforms.contrib.zgw.api.filters import ( + DocumentTypesFilter, + ProvidesCatalogiClientQueryParamsSerializer, +) +from openforms.contrib.zgw.clients.catalogi import CatalogiClient + +from ..models import ObjectsAPIGroupConfig + + +class ObjectsAPIGroupMixin(serializers.Serializer): + objects_api_group = PrimaryKeyRelatedAsChoicesField( + queryset=ObjectsAPIGroupConfig.objects.exclude(catalogi_service=None), + help_text=_( + "The primary key of the Objects API group to use. The informatieobjecttypen from the Catalogi API " + "in this group will be returned." + ), + label=_("Objects API group"), + ) + + def get_ztc_client(self) -> CatalogiClient: + objects_api_group: ObjectsAPIGroupConfig = self.validated_data[ + "objects_api_group" + ] + return build_client( + objects_api_group.catalogi_service, client_factory=CatalogiClient + ) + + +class APIGroupQueryParamsSerializer( + ObjectsAPIGroupMixin, ProvidesCatalogiClientQueryParamsSerializer +): + pass + + +class ListInformatieObjectTypenQueryParamsSerializer( + ObjectsAPIGroupMixin, DocumentTypesFilter +): + pass diff --git a/src/openforms/contrib/objects_api/api/serializers.py b/src/openforms/contrib/objects_api/api/serializers.py new file mode 100644 index 0000000000..0d94a4a98b --- /dev/null +++ b/src/openforms/contrib/objects_api/api/serializers.py @@ -0,0 +1,37 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers + +from openforms.api.fields import PrimaryKeyRelatedAsChoicesField + +from ..models import ObjectsAPIGroupConfig + + +class ObjectsAPIGroupInputSerializer(serializers.Serializer): + objects_api_group = PrimaryKeyRelatedAsChoicesField( + queryset=ObjectsAPIGroupConfig.objects.exclude(objecttypes_service=None), + label=("Objects API group"), + help_text=_("Which Objects API group to use."), + ) + + +class ObjecttypeSerializer(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 ObjecttypeVersionSerializer(serializers.Serializer): + version = serializers.IntegerField( + label=_("Integer version of the Objecttype."), + ) + status = serializers.CharField(label=_("Status of the object type version")) diff --git a/src/openforms/contrib/objects_api/api/urls.py b/src/openforms/contrib/objects_api/api/urls.py new file mode 100644 index 0000000000..c57da5a15c --- /dev/null +++ b/src/openforms/contrib/objects_api/api/urls.py @@ -0,0 +1,33 @@ +from django.urls import path + +from .views import ( + CatalogueListView, + InformatieObjectTypenListView, + ObjecttypesListView, + ObjecttypeVersionsListView, +) + +app_name = "objects_api" + +urlpatterns = [ + path( + "object-types", + ObjecttypesListView.as_view(), + name="object-types", + ), + path( + "object-types//versions", + ObjecttypeVersionsListView.as_view(), + name="object-type-versions", + ), + path( + "catalogues", + CatalogueListView.as_view(), + name="catalogue-list", + ), + path( + "informatieobjecttypen", + InformatieObjectTypenListView.as_view(), + name="iotypen-list", + ), +] diff --git a/src/openforms/contrib/objects_api/api/views.py b/src/openforms/contrib/objects_api/api/views.py new file mode 100644 index 0000000000..70ab61e68a --- /dev/null +++ b/src/openforms/contrib/objects_api/api/views.py @@ -0,0 +1,108 @@ +from typing import Any + +from django.utils.translation import gettext_lazy as _ + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view +from rest_framework import authentication, permissions, views + +from openforms.api.views import ListMixin +from openforms.contrib.zgw.api.views import ( + BaseCatalogueListView, + BaseInformatieObjectTypenListView, +) + +from ..clients import get_objecttypes_client +from .filters import ( + APIGroupQueryParamsSerializer, + ListInformatieObjectTypenQueryParamsSerializer, +) +from .serializers import ( + ObjectsAPIGroupInputSerializer, + ObjecttypeSerializer, + ObjecttypeVersionSerializer, +) + +# TODO: https://github.com/open-formulieren/open-forms/issues/611 +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( + tags=["registration"], + parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], +) +class ObjecttypesListView(ListMixin, views.APIView): + """ + List the available Objecttypes. + + Note that the response data is essentially proxied from the configured Objecttypes API. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = ObjecttypeSerializer + + 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( + tags=["registration"], + parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], +) +class ObjecttypeVersionsListView(ListMixin, views.APIView): + """ + List the available versions for an Objecttype. + + Note that the response data is essentially proxied from the configured Objecttypes API. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = ObjecttypeVersionSerializer + + 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_objecttype_versions(self.kwargs["objecttype_uuid"]) + + +@extend_schema_view( + get=extend_schema( + summary=_("List available Catalogi from the provided Objects API group"), + parameters=[APIGroupQueryParamsSerializer], + ), +) +class CatalogueListView(BaseCatalogueListView): + filter_serializer_class = APIGroupQueryParamsSerializer + + +@extend_schema_view( + get=extend_schema( + summary=_( + "List the available InformatieObjectTypen from the provided Objects API group" + ), + parameters=[ListInformatieObjectTypenQueryParamsSerializer], + ), +) +class InformatieObjectTypenListView(BaseInformatieObjectTypenListView): + filter_serializer_class = ListInformatieObjectTypenQueryParamsSerializer diff --git a/src/openforms/registrations/contrib/objects_api/checks.py b/src/openforms/contrib/objects_api/checks.py similarity index 97% rename from src/openforms/registrations/contrib/objects_api/checks.py rename to src/openforms/contrib/objects_api/checks.py index d8c17991ac..e4ba78d092 100644 --- a/src/openforms/registrations/contrib/objects_api/checks.py +++ b/src/openforms/contrib/objects_api/checks.py @@ -2,15 +2,15 @@ import requests -from openforms.contrib.objects_api.clients import ( +from openforms.plugins.exceptions import InvalidPluginConfiguration + +from .clients import ( NoServiceConfigured, get_catalogi_client, get_documents_client, get_objects_client, get_objecttypes_client, ) -from openforms.plugins.exceptions import InvalidPluginConfiguration - from .models import ObjectsAPIGroupConfig @@ -47,7 +47,8 @@ def check_documents_service(config: ObjectsAPIGroupConfig): def check_catalogi_service(config: ObjectsAPIGroupConfig): with get_catalogi_client(config) as client: resp = client.get("informatieobjecttypen") - resp.raise_for_status() + + resp.raise_for_status() def check_config(): diff --git a/src/openforms/registrations/contrib/objects_api/json_schema.py b/src/openforms/contrib/objects_api/json_schema.py similarity index 100% rename from src/openforms/registrations/contrib/objects_api/json_schema.py rename to src/openforms/contrib/objects_api/json_schema.py diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_get_objecttype_version.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_get_objecttype_version.yaml similarity index 97% rename from src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_get_objecttype_version.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_get_objecttype_version.yaml index c9e6188548..c709dff715 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_get_objecttype_version.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_get_objecttype_version.yaml @@ -32,7 +32,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Fri, 09 Aug 2024 08:19:06 GMT + - Fri, 13 Sep 2024 12:39:19 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttype_versions.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttype_versions.yaml similarity index 98% rename from src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttype_versions.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttype_versions.yaml index 6a86a93229..c16360e058 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttype_versions.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttype_versions.yaml @@ -35,7 +35,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Fri, 09 Aug 2024 08:19:06 GMT + - Fri, 13 Sep 2024 12:39:19 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttypes.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttypes.yaml similarity index 81% rename from src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttypes.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttypes.yaml index 083aca378a..ce7ea840d8 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttypes.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objecttypes.yaml @@ -16,24 +16,25 @@ 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 - Uploads","namePlural":"File Uploads","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/527b8408-7421-4808-a744-43ccb7bdaaa2/versions/1"]},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","uuid":"3edfdaf7-f469-470b-a391-bb7ea015bd6f","name":"Tree","namePlural":"Trees","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/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1"]},{"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/2","http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/3","http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/1"]}]}' + Uploads","namePlural":"File Uploads","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/527b8408-7421-4808-a744-43ccb7bdaaa2/versions/1"]},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","uuid":"3edfdaf7-f469-470b-a391-bb7ea015bd6f","name":"Tree","namePlural":"Trees","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/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1"]},{"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, POST, HEAD, OPTIONS Connection: - keep-alive Content-Length: - - '3921' + - '4541' Content-Type: - application/json Cross-Origin-Opener-Policy: - same-origin Date: - - Fri, 09 Aug 2024 08:19:06 GMT + - Fri, 13 Sep 2024 12:39:19 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objectypes_pagination.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objectypes_pagination.yaml similarity index 56% rename from src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objectypes_pagination.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objectypes_pagination.yaml index 5a687efa0c..7c305b23bf 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objectypes_pagination.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesClientTest/ObjecttypesClientTest.test_list_objectypes_pagination.yaml @@ -16,8 +16,8 @@ interactions: uri: http://localhost:8001/api/v2/objecttypes?page=1&pageSize=1 response: body: - string: '{"count":6,"next":"http://objecttypes-web:8000/api/v2/objecttypes?page=2&pageSize=1","previous":null,"results":[{"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"]}]}' + string: '{"count":7,"next":"http://objecttypes-web:8000/api/v2/objecttypes?page=2&pageSize=1","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"]}]}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -30,7 +30,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Fri, 09 Aug 2024 08:19:06 GMT + - Fri, 13 Sep 2024 12:39:19 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_json_schema.py b/src/openforms/contrib/objects_api/tests/test_json_schema.py similarity index 100% rename from src/openforms/registrations/contrib/objects_api/tests/test_json_schema.py rename to src/openforms/contrib/objects_api/tests/test_json_schema.py diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_objecttypes_client.py b/src/openforms/contrib/objects_api/tests/test_objecttypes_client.py similarity index 69% rename from src/openforms/registrations/contrib/objects_api/tests/test_objecttypes_client.py rename to src/openforms/contrib/objects_api/tests/test_objecttypes_client.py index 0824e5d7e1..4515a2611b 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_objecttypes_client.py +++ b/src/openforms/contrib/objects_api/tests/test_objecttypes_client.py @@ -5,27 +5,12 @@ from zgw_consumers.constants import APITypes, AuthTypes from zgw_consumers.models import Service -from openforms.contrib.objects_api.clients import get_objecttypes_client from openforms.utils.tests.vcr import OFVCRMixin +from ..clients import get_objecttypes_client from ..models import ObjectsAPIGroupConfig -def get_test_config() -> ObjectsAPIGroupConfig: - """Returns a preconfigured ``ObjectsAPIGroupConfig`` instance matching the docker compose configuration.""" - - return ObjectsAPIGroupConfig( - objecttypes_service=Service( - api_root="http://localhost:8001/api/v2/", - api_type=APITypes.orc, - oas="https://example.com/", - header_key="Authorization", - header_value="Token 171be5abaf41e7856b423ad513df1ef8f867ff48", - auth_type=AuthTypes.api_key, - ) - ) - - class ObjecttypesClientTest(OFVCRMixin, TestCase): VCR_TEST_FILES = Path(__file__).parent / "files" @@ -33,7 +18,16 @@ class ObjecttypesClientTest(OFVCRMixin, TestCase): @classmethod def setUpTestData(cls) -> None: super().setUpTestData() - cls.test_config = get_test_config() + cls.test_config = ObjectsAPIGroupConfig( + objecttypes_service=Service( + api_root="http://localhost:8001/api/v2/", + api_type=APITypes.orc, + oas="https://example.com/", + header_key="Authorization", + header_value="Token 171be5abaf41e7856b423ad513df1ef8f867ff48", + auth_type=AuthTypes.api_key, + ) + ) def test_list_objecttypes(self): with get_objecttypes_client(self.test_config) as client: diff --git a/src/openforms/js/components/admin/form_design/constants.js b/src/openforms/js/components/admin/form_design/constants.js index e3f25697dd..e5518c95c0 100644 --- a/src/openforms/js/components/admin/form_design/constants.js +++ b/src/openforms/js/components/admin/form_design/constants.js @@ -1,8 +1,7 @@ export const FORM_ENDPOINT = '/api/v2/forms'; export const FORM_DEFINITIONS_ENDPOINT = '/api/v2/form-definitions'; export const REGISTRATION_BACKENDS_ENDPOINT = '/api/v2/registration/plugins'; -export const REGISTRATION_OBJECTTYPES_ENDPOINT = - '/api/v2/registration/plugins/objects-api/object-types'; +export const OBJECTS_API_OBJECTTYPES_ENDPOINT = '/api/v2/objects-api/object-types'; export const REGISTRATION_OBJECTS_TARGET_PATHS = '/api/v2/registration/plugins/objects-api/target-paths'; export const AUTH_PLUGINS_ENDPOINT = '/api/v2/authentication/plugins'; diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/DocumentTypes.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/DocumentTypes.js index 58170cbca2..63381cfb44 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/DocumentTypes.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/DocumentTypes.js @@ -20,8 +20,8 @@ import {get} from 'utils/fetch'; // Data fetching -const CATALOGUES_ENDPOINT = '/api/v2/registration/plugins/objects-api/catalogues'; -const IOT_ENDPOINT = '/api/v2/registration/plugins/objects-api/informatieobjecttypen'; +const CATALOGUES_ENDPOINT = '/api/v2/objects-api/catalogues'; +const IOT_ENDPOINT = '/api/v2/objects-api/informatieobjecttypen'; const getCatalogues = async apiGroupID => { const response = await get(CATALOGUES_ENDPOINT, {objects_api_group: apiGroupID}); diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeSelect.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeSelect.js index 566ae3ff0b..997ea60ed6 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeSelect.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeSelect.js @@ -4,7 +4,7 @@ import {FormattedMessage} from 'react-intl'; import {usePrevious, useUpdateEffect} from 'react-use'; import useAsync from 'react-use/esm/useAsync'; -import {REGISTRATION_OBJECTTYPES_ENDPOINT} from 'components/admin/form_design/constants'; +import {OBJECTS_API_OBJECTTYPES_ENDPOINT} from 'components/admin/form_design/constants'; import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; import ReactSelect from 'components/admin/forms/ReactSelect'; @@ -13,7 +13,7 @@ import {get} from 'utils/fetch'; import {useSynchronizeSelect} from './hooks'; const getAvailableObjectTypes = async apiGroupID => { - const response = await get(REGISTRATION_OBJECTTYPES_ENDPOINT, {objects_api_group: apiGroupID}); + const response = await get(OBJECTS_API_OBJECTTYPES_ENDPOINT, {objects_api_group: apiGroupID}); if (!response.ok) { throw new Error('Loading available object types failed'); } diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeVersionSelect.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeVersionSelect.js index 9785df149b..9bcf208eb7 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeVersionSelect.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/ObjectTypeVersionSelect.js @@ -10,11 +10,7 @@ import {get} from 'utils/fetch'; import {useSynchronizeSelect} from './hooks'; const getObjecttypeVersionsEndpoint = uuid => { - const bits = [ - '/api/v2/registration/plugins/objects-api/object-types', - encodeURIComponent(uuid), - 'versions', - ]; + const bits = ['/api/v2/objects-api/object-types', encodeURIComponent(uuid), 'versions']; return bits.join('/'); }; diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js index e4b12070f0..a44f4a5ce3 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js @@ -3,20 +3,14 @@ import {rest} from 'msw'; import {API_BASE_URL} from 'utils/fetch'; export const mockObjecttypesGet = objecttypes => - rest.get( - `${API_BASE_URL}/api/v2/registration/plugins/objects-api/object-types`, - (req, res, ctx) => { - return res(ctx.json(objecttypes)); - } - ); + rest.get(`${API_BASE_URL}/api/v2/objects-api/object-types`, (req, res, ctx) => { + return res(ctx.json(objecttypes)); + }); export const mockObjecttypeVersionsGet = versions => - rest.get( - `${API_BASE_URL}/api/v2/registration/plugins/objects-api/object-types/:uuid/versions`, - (req, res, ctx) => { - return res(ctx.json(versions)); - } - ); + rest.get(`${API_BASE_URL}/api/v2/objects-api/object-types/:uuid/versions`, (req, res, ctx) => { + return res(ctx.json(versions)); + }); export const mockObjecttypesError = () => rest.all(`${API_BASE_URL}/api/v2/*`, (req, res, ctx) => { @@ -57,12 +51,9 @@ const CATALOGUES = [ ]; export const mockCataloguesGet = () => - rest.get( - `${API_BASE_URL}/api/v2/registration/plugins/objects-api/catalogues`, - (req, res, ctx) => { - return res(ctx.json(CATALOGUES)); - } - ); + rest.get(`${API_BASE_URL}/api/v2/objects-api/catalogues`, (req, res, ctx) => { + return res(ctx.json(CATALOGUES)); + }); const DOCUMENT_TYPES = { 'https://example.com/catalogi/api/v1/catalogussen/1': [ @@ -122,11 +113,8 @@ const DOCUMENT_TYPES = { }; export const mockDocumentTypesGet = () => - rest.get( - `${API_BASE_URL}/api/v2/registration/plugins/objects-api/informatieobjecttypen`, - (req, res, ctx) => { - const catalogusUrl = req.url.searchParams.get('catalogus_url'); - const match = DOCUMENT_TYPES[catalogusUrl] ?? []; - return res(ctx.json(match)); - } - ); + rest.get(`${API_BASE_URL}/api/v2/objects-api/informatieobjecttypen`, (req, res, ctx) => { + const catalogusUrl = req.url.searchParams.get('catalogus_url'); + const match = DOCUMENT_TYPES[catalogusUrl] ?? []; + return res(ctx.json(match)); + }); diff --git a/src/openforms/js/components/form/file.js b/src/openforms/js/components/form/file.js index bf010f62fe..4fafdb5162 100644 --- a/src/openforms/js/components/form/file.js +++ b/src/openforms/js/components/form/file.js @@ -23,7 +23,7 @@ const getInformatieObjectTypen = async (backend, options) => { }); } case 'objects_api': - return await get('/api/v2/registration/plugins/objects-api/informatieobjecttypen', { + return await get('/api/v2/objects-api/informatieobjecttypen', { objects_api_group: options.objectsApiGroup, }); default: diff --git a/src/openforms/prefill/api/urls.py b/src/openforms/prefill/api/urls.py index a43ea07f59..de81aa2a12 100644 --- a/src/openforms/prefill/api/urls.py +++ b/src/openforms/prefill/api/urls.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import include, path from .views import PluginAttributesListView, PluginListView @@ -10,3 +10,12 @@ name="prefill-attribute-list", ), ] + +# add plugin URL patterns +# TODO: make this dynamic and include it through the registry? +urlpatterns += [ + path( + "plugins/objects-api/", + include("openforms.prefill.contrib.objects_api.api.urls"), + ), +] 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/api/__init__.py b/src/openforms/prefill/contrib/objects_api/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/prefill/contrib/objects_api/api/serializers.py b/src/openforms/prefill/contrib/objects_api/api/serializers.py new file mode 100644 index 0000000000..d4067cc131 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/api/serializers.py @@ -0,0 +1,17 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers + + +class PrefillTargetPathsSerializer(serializers.Serializer): + target_path = serializers.ListField( + child=serializers.CharField(label=_("Segment of a JSON path")), + label=_("target path"), + help_text=_( + "Representation of the JSON target location as a list of string segments." + ), + ) + json_schema = serializers.DictField( + label=_("json schema"), + help_text=_("Corresponding (sub) JSON Schema of the target path."), + ) diff --git a/src/openforms/prefill/contrib/objects_api/api/urls.py b/src/openforms/prefill/contrib/objects_api/api/urls.py new file mode 100644 index 0000000000..b97a96c773 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/api/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from .views import ObjecttypePropertiesListView + +app_name = "prefill_objects_api" + +urlpatterns = [ + path( + "objecttypes//versions//properties", + ObjecttypePropertiesListView.as_view(), + name="objecttype-property-list", + ), +] diff --git a/src/openforms/prefill/contrib/objects_api/api/views.py b/src/openforms/prefill/contrib/objects_api/api/views.py new file mode 100644 index 0000000000..984ee30f91 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/api/views.py @@ -0,0 +1,64 @@ +from typing import Any + +from django.utils.translation import gettext_lazy as _ + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework import authentication, permissions, views + +from openforms.api.views import ListMixin +from openforms.contrib.objects_api.api.serializers import ObjectsAPIGroupInputSerializer +from openforms.contrib.objects_api.clients import get_objecttypes_client +from openforms.contrib.objects_api.json_schema import ( + InvalidReference, + iter_json_schema_paths, +) + +from .serializers import PrefillTargetPathsSerializer + +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( + summary=_("List available attributes for Objects API"), + parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], +) +class ObjecttypePropertiesListView(ListMixin, views.APIView): + """ + List the available JSON properties defined on a particular objecttype. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = PrefillTargetPathsSerializer + + 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: + json_schema = client.get_objecttype_version( + self.kwargs["objecttype_uuid"], + self.kwargs["objecttype_version"], + )["jsonSchema"] + + return [ + { + "target_path": json_path.segments, + "json_schema": json_schema, + } + for json_path, json_schema in iter_json_schema_paths( + json_schema, fail_fast=False + ) + if not isinstance(json_schema, InvalidReference) + if not len(json_path.segments) == 0 + ] 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..1574e47571 --- /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 = "prefill_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/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py new file mode 100644 index 0000000000..2320515d1f --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -0,0 +1,39 @@ +import logging + +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from openforms.contrib.objects_api.checks import check_config +from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig + +from ...base import BasePlugin +from ...registry import register + +logger = logging.getLogger(__name__) + +PLUGIN_IDENTIFIER = "objects_api" + + +@register(PLUGIN_IDENTIFIER) +class ObjectsAPIPrefill(BasePlugin): + verbose_name = _("Objects API") + + def check_config(self): + check_config() + + def get_config_actions(self): + return [ + ( + _("Manage API groups"), + reverse( + "admin:registrations_objects_api_objectsapigroupconfig_changelist" + ), + ), + ( + _("Defaults configuration"), + reverse( + "admin:registrations_objects_api_objectsapiconfig_change", + args=(ObjectsAPIConfig.singleton_instance_id,), + ), + ), + ] diff --git a/src/openforms/prefill/contrib/objects_api/tests/__init__.py b/src/openforms/prefill/contrib/objects_api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..3c07e5738f --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_available_attributes.yaml @@ -0,0 +1,49 @@ +interactions: +- 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/versions/2 + response: + body: + string: '{"url":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/2","version":2,"objectType":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","status":"published","jsonSchema":{"$id":"https://example.com/person.schema.json","type":"object","title":"Person","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"age":{"type":"integer","minimum":18,"description":"Age + in years which must be equal to or greater than 18."},"lastName":{"type":"string","description":"The + person''s last name."},"firstName":{"type":"string","description":"The person''s + first name."}}},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","publishedAt":"2024-02-08"}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Length: + - '731' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Fri, 13 Sep 2024 13:20:37 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/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml new file mode 100644 index 0000000000..ad29cfd5a6 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes.yaml @@ -0,0 +1,51 @@ +interactions: +- 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 + response: + body: + 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 + Uploads","namePlural":"File Uploads","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/527b8408-7421-4808-a744-43ccb7bdaaa2/versions/1"]},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","uuid":"3edfdaf7-f469-470b-a391-bb7ea015bd6f","name":"Tree","namePlural":"Trees","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/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1"]},{"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, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Length: + - '4541' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Fri, 13 Sep 2024 13:15:38 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/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 new file mode 100644 index 0000000000..1e3a148b01 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginEndpointTests/ObjectsAPIPrefillPluginEndpointTests.test_list_objecttypes_versions.yaml @@ -0,0 +1,52 @@ +interactions: +- 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/versions + response: + body: + string: '{"count":3,"next":null,"previous":null,"results":[{"url":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/3","version":3,"objectType":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","status":"draft","jsonSchema":{"$id":"https://example.com/person.schema.json","type":"object","title":"Person","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"age":{"type":"integer","minimum":18},"name":{"type":"object","properties":{"last.name":{"type":"string"}}},"nested":{"type":"object","properties":{"unrelated":{"type":"string"},"submission_payment_amount":{"type":"number","multipleOf":0.01}}},"submission_date":{"type":"string","format":"date-time"},"submission_csv_url":{"type":"string","format":"uri"},"submission_pdf_url":{"type":"string","format":"uri"},"submission_payment_completed":{"type":"boolean"},"submission_payment_public_ids":{"type":"array"}}},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","publishedAt":"2024-02-08"},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/2","version":2,"objectType":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","status":"published","jsonSchema":{"$id":"https://example.com/person.schema.json","type":"object","title":"Person","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"age":{"type":"integer","minimum":18,"description":"Age + in years which must be equal to or greater than 18."},"lastName":{"type":"string","description":"The + person''s last name."},"firstName":{"type":"string","description":"The person''s + first name."}}},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","publishedAt":"2024-02-08"},{"url":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/1","version":1,"objectType":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","status":"published","jsonSchema":{"$id":"https://example.com/person.schema.json","type":"object","title":"Person","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"age":{"type":"integer","minimum":0,"description":"Age + in years which must be equal to or greater than zero."},"lastName":{"type":"string","description":"The + person''s last name."},"firstName":{"type":"string","description":"The person''s + first name."}}},"createdAt":"2023-10-24","modifiedAt":"2024-02-08","publishedAt":"2024-02-08"}]}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Length: + - '2502' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Fri, 13 Sep 2024 13:15:38 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_endpoints.py b/src/openforms/prefill/contrib/objects_api/tests/test_endpoints.py new file mode 100644 index 0000000000..0e90728a30 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/test_endpoints.py @@ -0,0 +1,161 @@ +from pathlib import Path +from unittest.mock import patch + +from rest_framework import status +from rest_framework.reverse import reverse_lazy +from rest_framework.test import APITestCase + +from openforms.accounts.tests.factories import StaffUserFactory +from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory +from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig +from openforms.utils.tests.vcr import OFVCRMixin + +VCR_TEST_FILES = Path(__file__).parent / "files" + + +class ObjectsAPIPrefillPluginEndpointTests(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 + endpoints = { + "objecttype_list": reverse_lazy( + "api:objects_api:object-types", + ), + "objecttype_versions_list": reverse_lazy( + "api:objects_api:object-type-versions", + kwargs={ + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + }, + ), + "attribute_list": reverse_lazy( + "api:prefill_objects_api:objecttype-property-list", + kwargs={ + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 2, + }, + ), + } + + def setUp(self): + super().setUp() + + config_patcher = patch( + "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 + ) + + def test_auth_required(self): + for endpoint in self.endpoints.values(): + with self.subTest(endpoint=endpoint): + response = self.client.get( + endpoint, {"objects_api_group": self.objects_api_group.pk} + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_endpoints_missing_api_group(self): + staff_user = StaffUserFactory.create() + self.client.force_authenticate(user=staff_user) + + for endpoint in self.endpoints.values(): + with self.subTest(endpoint=endpoint): + response = self.client.get(endpoint) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_list_objecttypes(self): + staff_user = StaffUserFactory.create() + self.client.force_authenticate(user=staff_user) + + response = self.client.get( + self.endpoints["objecttype_list"], + {"objects_api_group": self.objects_api_group.pk}, + ) + + tree_objecttype = next(obj for obj in response.json() if obj["name"] == "Tree") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + tree_objecttype, + { + "dataClassification": "confidential", + "name": "Tree", + "namePlural": "Trees", + "url": "http://objecttypes-web:8000/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "uuid": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + }, + ) + + def test_list_objecttypes_versions(self): + staff_user = StaffUserFactory.create() + self.client.force_authenticate(user=staff_user) + + response = self.client.get( + self.endpoints["objecttype_versions_list"], + {"objects_api_group": self.objects_api_group.pk}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json(), + [ + {"version": 3, "status": "draft"}, + {"version": 2, "status": "published"}, + {"version": 1, "status": "published"}, + ], + ) + + def test_list_available_attributes(self): + staff_user = StaffUserFactory.create() + self.client.force_authenticate(user=staff_user) + + response = self.client.get( + self.endpoints["attribute_list"], + {"objects_api_group": self.objects_api_group.pk}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json(), + [ + { + "targetPath": ["age"], + "jsonSchema": { + "type": "integer", + "minimum": 18, + "description": "Age in years which must be equal to or greater than 18.", + }, + }, + { + "targetPath": ["lastName"], + "jsonSchema": { + "type": "string", + "description": "The person's last name.", + }, + }, + { + "targetPath": ["firstName"], + "jsonSchema": { + "type": "string", + "description": "The person's first name.", + }, + }, + ], + ) diff --git a/src/openforms/registrations/contrib/objects_api/api/serializers.py b/src/openforms/registrations/contrib/objects_api/api/serializers.py index 7f391b9317..d163179627 100644 --- a/src/openforms/registrations/contrib/objects_api/api/serializers.py +++ b/src/openforms/registrations/contrib/objects_api/api/serializers.py @@ -2,39 +2,7 @@ from rest_framework import serializers -from openforms.api.fields import PrimaryKeyRelatedAsChoicesField - -from ..models import ObjectsAPIGroupConfig - - -class ObjectsAPIGroupInputSerializer(serializers.Serializer): - objects_api_group = PrimaryKeyRelatedAsChoicesField( - queryset=ObjectsAPIGroupConfig.objects.exclude(objecttypes_service=None), - label=("Objects API group"), - help_text=_("Which Objects API group to use."), - ) - - -class ObjecttypeSerializer(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 ObjecttypeVersionSerializer(serializers.Serializer): - version = serializers.IntegerField( - label=_("Integer version of the Objecttype."), - ) - status = serializers.CharField(label=_("Status of the object type version")) +from openforms.contrib.objects_api.api.serializers import ObjectsAPIGroupInputSerializer class TargetPathsSerializer(serializers.Serializer): diff --git a/src/openforms/registrations/contrib/objects_api/api/urls.py b/src/openforms/registrations/contrib/objects_api/api/urls.py index 55f81b3a8f..91edd9daa4 100644 --- a/src/openforms/registrations/contrib/objects_api/api/urls.py +++ b/src/openforms/registrations/contrib/objects_api/api/urls.py @@ -1,39 +1,13 @@ from django.urls import path -from .views import ( - CatalogueListView, - InformatieObjectTypenListView, - ObjecttypesListView, - ObjecttypeVersionsListView, - TargetPathsListView, -) +from .views import TargetPathsListView -app_name = "objects_api" +app_name = "registrations_objects_api" urlpatterns = [ - path( - "object-types", - ObjecttypesListView.as_view(), - name="object-types", - ), - path( - "object-types//versions", - ObjecttypeVersionsListView.as_view(), - name="object-type-versions", - ), path( "target-paths", TargetPathsListView.as_view(), name="target-paths", - ), - path( - "catalogues", - CatalogueListView.as_view(), - name="catalogue-list", - ), - path( - "informatieobjecttypen", - InformatieObjectTypenListView.as_view(), - name="iotypen-list", - ), + ) ] diff --git a/src/openforms/registrations/contrib/objects_api/api/views.py b/src/openforms/registrations/contrib/objects_api/api/views.py index 08059916d3..cbdfe5c61d 100644 --- a/src/openforms/registrations/contrib/objects_api/api/views.py +++ b/src/openforms/registrations/contrib/objects_api/api/views.py @@ -1,94 +1,18 @@ from typing import Any -from django.utils.translation import gettext_lazy as _ - -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view +from drf_spectacular.utils import extend_schema from rest_framework import authentication, permissions, views from rest_framework.request import Request from rest_framework.response import Response -from openforms.api.views import ListMixin from openforms.contrib.objects_api.clients import get_objecttypes_client -from openforms.contrib.zgw.api.views import ( - BaseCatalogueListView, - BaseInformatieObjectTypenListView, -) - -from ..json_schema import InvalidReference, iter_json_schema_paths, json_schema_matches -from .filters import ( - APIGroupQueryParamsSerializer, - ListInformatieObjectTypenQueryParamsSerializer, -) -from .serializers import ( - ObjectsAPIGroupInputSerializer, - ObjecttypeSerializer, - ObjecttypeVersionSerializer, - TargetPathsInputSerializer, - TargetPathsSerializer, -) - -# TODO: https://github.com/open-formulieren/open-forms/issues/611 -OBJECTS_API_GROUP_QUERY_PARAMETER = OpenApiParameter( - name="objects_api_group", - type=OpenApiTypes.STR, - location=OpenApiParameter.QUERY, - description=_("Which Objects API group to use."), +from openforms.contrib.objects_api.json_schema import ( + InvalidReference, + iter_json_schema_paths, + json_schema_matches, ) - -@extend_schema( - tags=["registration"], - parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], -) -class ObjecttypesListView(ListMixin, views.APIView): - """ - List the available Objecttypes. - - Note that the response data is essentially proxied from the configured Objecttypes API. - """ - - authentication_classes = (authentication.SessionAuthentication,) - permission_classes = (permissions.IsAdminUser,) - serializer_class = ObjecttypeSerializer - - 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( - tags=["registration"], - parameters=[OBJECTS_API_GROUP_QUERY_PARAMETER], -) -class ObjecttypeVersionsListView(ListMixin, views.APIView): - """ - List the available versions for an Objecttype. - - Note that the response data is essentially proxied from the configured Objecttypes API. - """ - - authentication_classes = (authentication.SessionAuthentication,) - permission_classes = (permissions.IsAdminUser,) - serializer_class = ObjecttypeVersionSerializer - - 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_objecttype_versions(self.kwargs["submission_uuid"]) +from .serializers import TargetPathsInputSerializer, TargetPathsSerializer class TargetPathsListView(views.APIView): @@ -128,25 +52,3 @@ def post(self, request: Request, *args: Any, **kwargs: Any): output_serializer = TargetPathsSerializer(many=True, instance=return_data) return Response(data=output_serializer.data) - - -@extend_schema_view( - get=extend_schema( - summary=_("List available Catalogi from the provided Objects API group"), - parameters=[APIGroupQueryParamsSerializer], - ), -) -class CatalogueListView(BaseCatalogueListView): - filter_serializer_class = APIGroupQueryParamsSerializer - - -@extend_schema_view( - get=extend_schema( - summary=_( - "List the available InformatieObjectTypen from the provided Objects API group" - ), - parameters=[ListInformatieObjectTypenQueryParamsSerializer], - ), -) -class InformatieObjectTypenListView(BaseInformatieObjectTypenListView): - filter_serializer_class = ListInformatieObjectTypenQueryParamsSerializer diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index 59e35c6d70..2767da37b9 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _ from openforms.config.data import Action +from openforms.contrib.objects_api.checks import check_config from openforms.contrib.objects_api.clients import ( get_objects_client, get_objecttypes_client, @@ -17,7 +18,6 @@ from ...base import BasePlugin from ...registry import register -from .checks import check_config from .config import ObjectsAPIOptionsSerializer from .models import ObjectsAPIConfig from .registration_variables import register as variables_registry diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_api_endpoints.py b/src/openforms/registrations/contrib/objects_api/tests/test_api_endpoints.py index 435c89956f..9fc515bf40 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_api_endpoints.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_api_endpoints.py @@ -4,6 +4,7 @@ from rest_framework.reverse import reverse_lazy from rest_framework.test import APITestCase from zgw_consumers.constants import APITypes, AuthTypes +from zgw_consumers.models import Service from zgw_consumers.test.factories import ServiceFactory from openforms.accounts.tests.factories import StaffUserFactory, UserFactory @@ -11,11 +12,26 @@ from openforms.utils.tests.feature_flags import enable_feature_flag from openforms.utils.tests.vcr import OFVCRMixin -from .test_objecttypes_client import get_test_config +from ..models import ObjectsAPIGroupConfig TEST_FILES = Path(__file__).parent / "files" +def get_test_config() -> ObjectsAPIGroupConfig: + """Returns a preconfigured ``ObjectsAPIGroupConfig`` instance matching the docker compose configuration.""" + + return ObjectsAPIGroupConfig( + objecttypes_service=Service( + api_root="http://localhost:8001/api/v2/", + api_type=APITypes.orc, + oas="https://example.com/", + header_key="Authorization", + header_value="Token 171be5abaf41e7856b423ad513df1ef8f867ff48", + auth_type=AuthTypes.api_key, + ) + ) + + class ObjecttypesAPIEndpointTests(OFVCRMixin, APITestCase): VCR_TEST_FILES = TEST_FILES @@ -135,7 +151,7 @@ def test_list_objecttype_versions_unknown_objecttype(self): class TargetPathsAPIEndpointTests(OFVCRMixin, APITestCase): VCR_TEST_FILES = TEST_FILES - endpoint = reverse_lazy("api:objects_api:target-paths") + endpoint = reverse_lazy("api:registrations_objects_api:target-paths") @classmethod def setUpTestData(cls) -> None: