diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml index bfafff3a..128f2c6f 100644 --- a/docker/setup_configuration/data.yaml +++ b/docker/setup_configuration/data.yaml @@ -1,6 +1,15 @@ zgw_consumers_config_enable: true zgw_consumers: services: + - identifier: objecttypes-api + label: Objecttypes API + api_root: http://objecttypes.local/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: api_key + header_key: Authorization + header_value: Token b9f100590925b529664ed9d370f5f8da124b2c20 + - identifier: notifications-api label: Notificaties API api_root: http://notificaties.local/api/v1/ @@ -16,3 +25,10 @@ notifications_config: notification_delivery_max_retries: 1 notification_delivery_retry_backoff: 2 notification_delivery_retry_backoff_max: 3 + +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: objecttypes-api diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 5ea762b8..f18aeeae 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -31,14 +31,81 @@ format, use by each step. Objects API =========== +Objecttypes configuration +------------------------- + +To configure objecttypes the following configuration could be used: + +.. code-block:: yaml + ... + zgw_consumers_config_enable: true + zgw_consumers: + services: + - identifier: objecttypen-foo + label: Objecttypen API Foo + api_root: http://objecttypen.foo/api/v1/ + api_type: orc + auth_type: api_key + header_key: Authorization + header_value: Token ba9d233e95e04c4a8a661a27daffe7c9bd019067 + + - identifier: objecttypen-bar + label: Objecttypen API Bar + api_root: http://objecttypen.bar/api/v1/ + api_type: orc + auth_type: api_key + header_key: Authorization + header_value: Token b9f100590925b529664ed9d370f5f8da124b2c20 + + objecttypes_config_enable: true + objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: objecttypen-foo + + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: objecttypen-bar + ... +.. note:: The ``uuid`` field will be used to lookup existing ``ObjectType``'s. + +Objecttypes require a corresponding ``Service`` to work correctly. Creating +these ``Service``'s can be done by defining these in the same yaml file. ``Service`` +instances will be created before the ``ObjectType``'s are created. + Objecttypes connection configuration ------------------------------------ -Tokens configuration -------------------- +In order to be able to retrieve objecttypes, a corresponding ``Service`` should be +created. An example of a configuration could be seen below: -Notifications configuration -------------------------- +.. code-block:: yaml + ... + + zgw_consumers_config_enable: true + zgw_consumers: + services: + - identifier: objecttypes-api-1 + label: Objecttypes API 1 + api_root: http://objecttypes-1.local/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: api_key + header_key: Authorization + header_value: Token ba9d233e95e04c4a8a661a27daffe7c9bd019067 + - identifier: objecttypes-api-2 + label: Objecttypes API 2 + api_root: http://objecttypes-2.local/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: api_key + header_key: Authorization + header_value: Token b9f100590925b529664ed9d370f5f8da124b2c20 + .... + +Tokens configuration +-------------------- Mozilla-django-oidc-db ---------------------- diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index 90e507b6..9562ec04 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -17,6 +17,7 @@ "rest_framework_gis", # Project applications. "objects.accounts", + "objects.setup_configuration", "objects.api", "objects.core", "objects.token", @@ -85,4 +86,5 @@ SETUP_CONFIGURATION_STEPS = ( "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", "notifications_api_common.contrib.setup_configuration.steps.NotificationConfigurationStep", + "objects.setup_configuration.steps.objecttypes.ObjectTypesConfigurationStep", ) diff --git a/src/objects/core/models.py b/src/objects/core/models.py index ccf4fd36..03addf37 100644 --- a/src/objects/core/models.py +++ b/src/objects/core/models.py @@ -1,5 +1,6 @@ import datetime import uuid +from typing import Iterable from django.contrib.gis.db.models import GeometryField from django.core.exceptions import ValidationError @@ -41,8 +42,14 @@ def url(self): # zds_client.get_operation_url() can be used here but it increases HTTP overhead return f"{self.service.api_root}objecttypes/{self.uuid}" - def clean(self): + def clean_fields(self, exclude: Iterable[str] | None = None) -> None: + super().clean_fields(exclude=exclude) + + if exclude and "service" in exclude: + return + client = build_client(self.service) + try: response = client.get(url=self.url) except (requests.RequestException, ConnectionError, ValueError) as exc: @@ -51,7 +58,7 @@ def clean(self): try: object_type_data = response.json() except requests.exceptions.JSONDecodeError: - ValidationError("Object type version didn't have any data") + raise ValidationError("Object type version didn't have any data") if not self._name: self._name = object_type_data["name"] diff --git a/src/objects/core/tests/files/objecttypes_empty_database.yaml b/src/objects/core/tests/files/objecttypes_empty_database.yaml new file mode 100644 index 00000000..b969949e --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_empty_database.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml b/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml new file mode 100644 index 00000000..f93e005f --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: 7229549b-7b41-47d1-8106-414b2a69751b + name: Object Type 3 + service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_idempotent.yaml b/src/objects/core/tests/files/objecttypes_idempotent.yaml new file mode 100644 index 00000000..b969949e --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_idempotent.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml b/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml new file mode 100644 index 00000000..2a360c8e --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: foobar + name: Object Type 2 + service_identifier: service-1 diff --git a/src/objects/core/tests/files/objecttypes_unknown_service.yaml b/src/objects/core/tests/files/objecttypes_unknown_service.yaml new file mode 100644 index 00000000..8348427c --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_unknown_service.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: unknown + + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: service-1 diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py new file mode 100644 index 00000000..5fb40bb5 --- /dev/null +++ b/src/objects/core/tests/test_objecttype_config.py @@ -0,0 +1,172 @@ +from pathlib import Path + +from django.db.models import QuerySet +from django.test import TestCase + +from django_setup_configuration.exceptions import ConfigurationRunFailed +from django_setup_configuration.test_utils import execute_single_step +from zgw_consumers.models import Service +from zgw_consumers.test.factories import ServiceFactory + +from objects.core.models import ObjectType +from objects.core.tests.factories import ObjectTypeFactory +from objects.setup_configuration.steps.objecttypes import ObjectTypesConfigurationStep + +TEST_FILES = (Path(__file__).parent / "files").resolve() + + +class ObjectTypesConfigurationStepTests(TestCase): + def test_empty_database(self): + service_1 = ServiceFactory(slug="service-1") + service_2 = ServiceFactory(slug="service-2") + + test_file_path = str(TEST_FILES / "objecttypes_empty_database.yaml") + + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + objecttypes: QuerySet[ObjectType] = ObjectType.objects.order_by("_name") + + self.assertEqual(objecttypes.count(), 2) + + objecttype_1: ObjectType = objecttypes.first() + + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + objecttype_2: ObjectType = objecttypes.last() + + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") + self.assertEqual(objecttype_2._name, "Object Type 2") + self.assertEqual(objecttype_2.service, service_2) + + def test_existing_objecttype(self): + test_file_path = str(TEST_FILES / "objecttypes_existing_objecttype.yaml") + + service_1: Service = ServiceFactory(slug="service-1") + service_2: Service = ServiceFactory(slug="service-2") + + objecttype_1: ObjectType = ObjectTypeFactory( + service=service_1, + uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", + _name="Object Type 001", + ) + objecttype_2: ObjectType = ObjectTypeFactory( + service=service_2, + uuid="b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2", + _name="Object Type 002", + ) + + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + self.assertEqual(ObjectType.objects.count(), 3) + + objecttype_1.refresh_from_db() + + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + objecttype_2.refresh_from_db() + + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") + self.assertEqual(objecttype_2._name, "Object Type 002") + self.assertEqual(objecttype_2.service, service_2) + + objecttype_3: ObjectType = ObjectType.objects.get( + uuid="7229549b-7b41-47d1-8106-414b2a69751b" + ) + + self.assertEqual(str(objecttype_3.uuid), "7229549b-7b41-47d1-8106-414b2a69751b") + self.assertEqual(objecttype_3._name, "Object Type 3") + self.assertEqual(objecttype_3.service, service_2) + + def test_unknown_service(self): + service = ServiceFactory(slug="service-1") + + objecttype: ObjectType = ObjectTypeFactory( + uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", + _name="Object Type 001", + service=service, + ) + + test_file_path = str(TEST_FILES / "objecttypes_unknown_service.yaml") + + with self.assertRaises(ConfigurationRunFailed): + execute_single_step( + ObjectTypesConfigurationStep, yaml_source=test_file_path + ) + + self.assertEqual(ObjectType.objects.count(), 1) + + objecttype.refresh_from_db() + + self.assertEqual(str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") + self.assertEqual(objecttype._name, "Object Type 001") + self.assertEqual(objecttype.service, service) + + def test_invalid_uuid(self): + test_file_path = str(TEST_FILES / "objecttypes_invalid_uuid.yaml") + + service: Service = ServiceFactory(slug="service-1") + + objecttype: ObjectType = ObjectTypeFactory( + service=service, + uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", + _name="Object Type 001", + ) + + with self.assertRaises(ConfigurationRunFailed): + execute_single_step( + ObjectTypesConfigurationStep, yaml_source=test_file_path + ) + + self.assertEqual(ObjectType.objects.count(), 1) + + objecttype.refresh_from_db() + + self.assertEqual(str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") + self.assertEqual(objecttype._name, "Object Type 1") + self.assertEqual(objecttype.service, service) + + def test_idempotent_step(self): + service_1 = ServiceFactory(slug="service-1") + service_2 = ServiceFactory(slug="service-2") + + test_file_path = str(TEST_FILES / "objecttypes_idempotent.yaml") + + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + objecttypes: QuerySet[ObjectType] = ObjectType.objects.order_by("_name") + + self.assertEqual(objecttypes.count(), 2) + + objecttype_1: ObjectType = objecttypes.first() + + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + objecttype_2: ObjectType = objecttypes.last() + + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") + self.assertEqual(objecttype_2._name, "Object Type 2") + self.assertEqual(objecttype_2.service, service_2) + + # Rerun + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + objecttype_1.refresh_from_db() + objecttype_2.refresh_from_db() + + self.assertEqual(ObjectType.objects.count(), 2) + + # objecttype 1 + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + # objecttype 2 + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") + self.assertEqual(objecttype_2._name, "Object Type 2") + self.assertEqual(objecttype_2.service, service_2) diff --git a/src/objects/setup_configuration/models/__init__.py b/src/objects/setup_configuration/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objects/setup_configuration/models/objecttypes.py b/src/objects/setup_configuration/models/objecttypes.py new file mode 100644 index 00000000..4125848a --- /dev/null +++ b/src/objects/setup_configuration/models/objecttypes.py @@ -0,0 +1,17 @@ +from django_setup_configuration.fields import DjangoModelRef +from django_setup_configuration.models import ConfigurationModel +from zgw_consumers.models import Service + +from objects.core.models import ObjectType + + +class ObjectTypeConfigurationModel(ConfigurationModel): + service_identifier: str = DjangoModelRef(Service, "slug") + name: str = DjangoModelRef(ObjectType, "_name") + + class Meta: + django_model_refs = {ObjectType: ("uuid",)} + + +class ObjectTypesConfigurationModel(ConfigurationModel): + items: list[ObjectTypeConfigurationModel] diff --git a/src/objects/setup_configuration/steps/__init__.py b/src/objects/setup_configuration/steps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objects/setup_configuration/steps/objecttypes.py b/src/objects/setup_configuration/steps/objecttypes.py new file mode 100644 index 00000000..91914ac4 --- /dev/null +++ b/src/objects/setup_configuration/steps/objecttypes.py @@ -0,0 +1,57 @@ +from django.core.exceptions import ValidationError +from django.db import IntegrityError + +from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import ConfigurationRunFailed +from zgw_consumers.models import Service + +from objects.core.models import ObjectType +from objects.setup_configuration.models.objecttypes import ObjectTypesConfigurationModel + + +class ObjectTypesConfigurationStep(BaseConfigurationStep): + config_model = ObjectTypesConfigurationModel + verbose_name = "Objecttypes Configuration" + + namespace = "objecttypes" + enable_setting = "objecttypes_config_enable" + + def execute(self, model: ObjectTypesConfigurationModel) -> None: + for item in model.items: + try: + service = Service.objects.get(slug=item.service_identifier) + except Service.DoesNotExist: + raise ConfigurationRunFailed( + f"No service found with identifier {item.service_identifier}" + ) + + objecttype_kwargs = dict( + service=service, + uuid=item.uuid, + _name=item.name, + ) + + objecttype_instance = ObjectType(**objecttype_kwargs) + + try: + objecttype_instance.full_clean( + exclude=("id", "service"), validate_unique=False + ) + except ValidationError as exception: + exception_message = ( + f"Validation error(s) occured for objecttype {item.uuid}." + ) + raise ConfigurationRunFailed(exception_message) from exception + + try: + ObjectType.objects.update_or_create( + uuid=item.uuid, + defaults={ + key: value + for key, value in objecttype_kwargs.items() + if key != "uuid" + }, + ) + except IntegrityError as exception: + exception_message = f"Failed configuring ObjectType {item.uuid}." + raise ConfigurationRunFailed(exception_message) from exception