diff --git a/docker-compose.yml b/docker-compose.yml
index e427ba0a..7c86f3eb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -37,7 +37,7 @@ services:
SUBPATH: ${SUBPATH}
DB_CONN_MAX_AGE: "0"
DB_POOL_ENABLED: True
-
+
# Enabling Open Telemetry requires the services in docker/docker-compose.observability.yaml
# to be up and running.
OTEL_SDK_DISABLED: ${OTEL_SDK_DISABLED:-true}
@@ -89,44 +89,6 @@ services:
networks:
- objects-dev
- objecttypes-web:
- image: maykinmedia/objecttypes-api:latest
- environment: &app-env
- DB_NAME: objecttypes
- DB_USER: objecttypes
- DJANGO_SETTINGS_MODULE: objecttypes.conf.docker
- SECRET_KEY: ${SECRET_KEY:-fgv=c0hz&tl*8*3m3893@m+1pstrvidc9e^5@fpspmg%cyf15d}
- ALLOWED_HOSTS: '*'
- CACHE_DEFAULT: redis:6379/0
- CACHE_AXES: redis:6379/0
- DISABLE_2FA: yes
- SUBPATH: ${SUBPATH:-/}
- volumes:
- - ./docker/objecttypes/objecttypes.json:/app/fixtures/objecttypes.json
- ports:
- - 8001:8000
- depends_on:
- objecttypes-web-init:
- condition: service_completed_successfully
- networks:
- - objects-dev
-
- objecttypes-web-init:
- image: maykinmedia/objecttypes-api:latest
- environment:
- <<: *app-env
- #
- # Django-setup-configuration
- RUN_SETUP_CONFIG: ${RUN_SETUP_CONFIG:-true}
- command: /setup_configuration.sh
- volumes:
- - ./docker/setup_configuration/objecttypes_data.yaml:/app/setup_configuration/data.yaml
- depends_on:
- - db
- - redis
- networks:
- - objects-dev
-
celery:
image: maykinmedia/objects-api:latest
build: *web_build
diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml
index 54614547..0e70002c 100644
--- a/docker/setup_configuration/data.yaml
+++ b/docker/setup_configuration/data.yaml
@@ -8,15 +8,6 @@ sites_config:
zgw_consumers_config_enable: true
zgw_consumers:
services:
- - identifier: objecttypes-api
- label: Objecttypes API
- api_root: http://objecttypes-web:8000/api/v2/
- 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/
@@ -40,11 +31,9 @@ objecttypes:
items:
- uuid: b427ef84-189d-43aa-9efd-7bb2c459e281
name: Object Type 1
- service_identifier: objecttypes-api
- uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2
name: Object Type 2
- service_identifier: objecttypes-api
tokenauth_config_enable: true
@@ -60,11 +49,6 @@ tokenauth:
permissions:
- object_type: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2
mode: read_only
- use_fields: true
- fields:
- '1':
- - record__data__leeftijd
- - record__data__kiemjaar
# additional permissions can be added like this:
# - object_type: b427ef84-189d-43aa-9efd-7bb2c459e281
# mode: read_and_write
@@ -119,4 +103,4 @@ oidc_db_config_admin_auth:
default_groups: []
make_users_staff: true
superuser_group_names:
- - Registreerders
\ No newline at end of file
+ - Registreerders
diff --git a/docs/installation/config.rst b/docs/installation/config.rst
index 407063b7..75cf37b6 100644
--- a/docs/installation/config.rst
+++ b/docs/installation/config.rst
@@ -98,12 +98,6 @@ Content Security Policy
* ``CSP_REPORT_PERCENTAGE``: Fraction (between 0 and 1) of requests to include report-uri directive. Defaults to: ``0.0``.
-Cache
------
-
-* ``OBJECTTYPE_VERSION_CACHE_TIMEOUT``: Timeout in seconds for cache when retrieving objecttype versions. Defaults to: ``300``.
-
-
Optional
--------
diff --git a/performance_test/create_data.py b/performance_test/create_data.py
index 6edde525..0cad8c22 100644
--- a/performance_test/create_data.py
+++ b/performance_test/create_data.py
@@ -10,11 +10,10 @@
from objects.token.tests.factories import PermissionFactory, TokenAuthFactory
object_type = ObjectTypeFactory.create(
- service__api_root="http://localhost:8001/api/v2/",
uuid="f1220670-8ab7-44f1-a318-bd0782e97662",
)
-token = TokenAuthFactory(token="secret", is_superuser=False)
+token = TokenAuthFactory.create(token="secret", is_superuser=False)
PermissionFactory.create(
object_type=object_type,
mode=PermissionModes.read_only,
diff --git a/requirements/base.in b/requirements/base.in
index e980947d..3894b9f0 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -8,6 +8,8 @@ furl
# Django libraries
django-capture-tag
+django-jsonsuit
+
# Common ground libraries
django-setup-configuration>=0.5.0
notifications-api-common[setup-configuration]
diff --git a/requirements/base.txt b/requirements/base.txt
index 1be9fee2..022592cb 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -82,6 +82,7 @@ django==5.2.11
# django-filter
# django-formtools
# django-jsonform
+ # django-jsonsuit
# django-log-outgoing-requests
# django-markup
# django-otp
@@ -138,6 +139,8 @@ django-jsonform==2.22.0
# via
# mozilla-django-oidc-db
# open-api-framework
+django-jsonsuit==0.5.0
+ # via -r requirements/base.in
django-log-outgoing-requests==0.6.1
# via open-api-framework
django-markup==1.8.1
diff --git a/requirements/ci.txt b/requirements/ci.txt
index dd6b1aea..23c2036c 100644
--- a/requirements/ci.txt
+++ b/requirements/ci.txt
@@ -145,6 +145,7 @@ django==5.2.11
# django-filter
# django-formtools
# django-jsonform
+ # django-jsonsuit
# django-log-outgoing-requests
# django-markup
# django-otp
@@ -222,6 +223,10 @@ django-jsonform==2.22.0
# -r requirements/base.txt
# mozilla-django-oidc-db
# open-api-framework
+django-jsonsuit==0.5.0
+ # via
+ # -c requirements/base.txt
+ # -r requirements/base.txt
django-log-outgoing-requests==0.6.1
# via
# -c requirements/base.txt
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 015d22d0..de4140e5 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -184,6 +184,7 @@ django==5.2.11
# django-filter
# django-formtools
# django-jsonform
+ # django-jsonsuit
# django-log-outgoing-requests
# django-markup
# django-otp
@@ -266,6 +267,10 @@ django-jsonform==2.22.0
# -r requirements/ci.txt
# mozilla-django-oidc-db
# open-api-framework
+django-jsonsuit==0.5.0
+ # via
+ # -c requirements/ci.txt
+ # -r requirements/ci.txt
django-log-outgoing-requests==0.6.1
# via
# -c requirements/ci.txt
diff --git a/src/objects/accounts/tests/factories.py b/src/objects/accounts/tests/factories.py
index 77ed12b2..420aa78c 100644
--- a/src/objects/accounts/tests/factories.py
+++ b/src/objects/accounts/tests/factories.py
@@ -29,6 +29,11 @@ class Params:
)
+class SuperUserFactory(UserFactory):
+ is_staff = True
+ is_superuser = True
+
+
class StaffUserFactory(UserFactory):
is_staff = True
diff --git a/src/objects/api/fields.py b/src/objects/api/fields.py
index e06763c5..2dcb483d 100644
--- a/src/objects/api/fields.py
+++ b/src/objects/api/fields.py
@@ -1,13 +1,9 @@
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.utils.encoding import smart_str
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from vng_api_common.serializers import CachedHyperlinkedIdentityField
-from vng_api_common.utils import get_uuid_from_path
-from zgw_consumers.models import Service
-from objects.core.models import ObjectRecord
+from objects.core.models import ObjectRecord, ObjectType
class ObjectSlugRelatedField(serializers.SlugRelatedField):
@@ -15,7 +11,6 @@ def get_queryset(self):
queryset = ObjectRecord.objects.select_related(
"object",
"object__object_type",
- "object__object_type__service",
"correct",
"corrected",
).order_by("-pk")
@@ -27,18 +22,21 @@ def get_queryset(self):
return queryset.filter(object=record_instance.object)
-class ObjectTypeField(serializers.RelatedField):
+class ObjectTypeField(serializers.HyperlinkedRelatedField):
default_error_messages = {
"max_length": _("The value has too many characters"),
"min_length": _("The value has too few characters"),
- "does_not_exist": _("ObjectType with url={value} is not configured."),
- "invalid": _("Invalid value."),
}
def __init__(self, **kwargs):
self.max_length = kwargs.pop("max_length", None)
self.min_length = kwargs.pop("min_length", None)
+ kwargs.setdefault("queryset", ObjectType.objects.all())
+ kwargs.setdefault("view_name", "objecttype-detail")
+ kwargs.setdefault("lookup_field", "uuid")
+ kwargs.setdefault("lookup_url_kwarg", "uuid")
+
super().__init__(**kwargs)
def to_internal_value(self, data):
@@ -48,43 +46,7 @@ def to_internal_value(self, data):
if self.min_length and len(data) < self.min_length:
self.fail("min_length")
- try:
- return self.get_queryset().get_by_url(data)
- except ObjectDoesNotExist:
- # if service is configured, but object_type is missing
- # let's try to create an ObjectType
- service = Service.get_service(data)
- if not service:
- self.fail("does_not_exist", value=smart_str(data))
-
- uuid = get_uuid_from_path(data)
- object_type = self.get_queryset().model(service=service, uuid=uuid)
-
- try:
- object_type.clean()
- except ValidationError:
- self.fail("does_not_exist", value=smart_str(data))
-
- object_type.save()
- return object_type
-
- except (TypeError, ValueError):
- self.fail("invalid")
-
- def to_representation(self, obj):
- return obj.url
-
-
-class ObjectUrlField(serializers.HyperlinkedIdentityField):
- lookup_field = "uuid"
-
- def get_url(self, obj, view_name, request, format):
- if hasattr(obj, "pk") and obj.pk in (None, ""):
- return None
-
- lookup_value = getattr(obj.object, "uuid")
- kwargs = {self.lookup_url_kwarg: lookup_value}
- return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
+ return super().to_internal_value(data)
class CachedObjectUrlField(CachedHyperlinkedIdentityField):
diff --git a/src/objects/api/kanalen.py b/src/objects/api/kanalen.py
index 86e9b660..a91580e9 100644
--- a/src/objects/api/kanalen.py
+++ b/src/objects/api/kanalen.py
@@ -7,6 +7,7 @@
from rest_framework.request import Request
from objects.core.models import ObjectRecord
+from objects.tests.v2.utils import reverse
class ObjectKanaal(Kanaal):
@@ -32,7 +33,10 @@ def get_kenmerken(
data = data or {}
return {
kenmerk: (
- data.get("type") or obj._object_type.url
+ data.get("type")
+ or request.build_absolute_url(
+ reverse("objecttype-detail", args=[data.object_type.uuid])
+ )
if kenmerk == "object_type"
else data.get(kenmerk, getattr(obj, kenmerk))
)
diff --git a/src/objects/api/metrics.py b/src/objects/api/metrics.py
index ad9de2f8..06a23d72 100644
--- a/src/objects/api/metrics.py
+++ b/src/objects/api/metrics.py
@@ -1,19 +1,37 @@
from opentelemetry import metrics
-meter = metrics.get_meter("objects.api")
+object_meter = metrics.get_meter("objects.api")
-objects_create_counter = meter.create_counter(
+objects_create_counter = object_meter.create_counter(
"objects.object.creates",
description="Amount of objects created (via the API).",
unit="1",
)
-objects_update_counter = meter.create_counter(
+objects_update_counter = object_meter.create_counter(
"objects.object.updates",
description="Amount of objects updated (via the API).",
unit="1",
)
-objects_delete_counter = meter.create_counter(
+objects_delete_counter = object_meter.create_counter(
"objects.object.deletes",
description="Amount of objects deleted (via the API).",
unit="1",
)
+
+objecttype_meter = metrics.get_meter("objecttypes.api.v2")
+
+objecttype_create_counter = objecttype_meter.create_counter(
+ "objecttypes.objecttype.creates",
+ description="Amount of objecttypes created (via the API).",
+ unit="1",
+)
+objecttype_update_counter = objecttype_meter.create_counter(
+ "objecttypes.objecttype.updates",
+ description="Amount of objecttypes updated (via the API).",
+ unit="1",
+)
+objecttype_delete_counter = objecttype_meter.create_counter(
+ "objecttypes.objecttype.deletes",
+ description="Amount of objecttypes deleted (via the API).",
+ unit="1",
+)
diff --git a/src/objects/api/mixins.py b/src/objects/api/mixins.py
index 00d48470..e759088e 100644
--- a/src/objects/api/mixins.py
+++ b/src/objects/api/mixins.py
@@ -1,4 +1,6 @@
+from django.core.exceptions import ValidationError
from django.db import models
+from django.http import Http404
from notifications_api_common.viewsets import (
NotificationCreateMixin,
@@ -8,6 +10,7 @@
)
from rest_framework.exceptions import NotAcceptable
from rest_framework.renderers import BrowsableAPIRenderer
+from rest_framework_nested.viewsets import NestedViewSetMixin as _NestedViewSetMixin
from vng_api_common.exceptions import PreconditionFailed
from vng_api_common.geo import (
DEFAULT_CRS,
@@ -18,6 +21,20 @@
)
+class NestedViewSetMixin(_NestedViewSetMixin):
+ def get_queryset(self):
+ """
+ catch validation errors if parent_lookup_kwargs have incorrect format
+ and return 404
+ """
+ try:
+ queryset = super().get_queryset()
+ except ValidationError:
+ raise Http404
+
+ return queryset
+
+
class GeoMixin(_GeoMixin):
def perform_crs_negotation(self, request):
# don't cripple the browsable API...
diff --git a/src/objects/api/serializers.py b/src/objects/api/serializers.py
index e6f18a5b..332cdac2 100644
--- a/src/objects/api/serializers.py
+++ b/src/objects/api/serializers.py
@@ -2,21 +2,160 @@
from django.utils.translation import gettext_lazy as _
import structlog
+from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from rest_framework_gis.serializers import GeometryField
+from rest_framework_nested.relations import NestedHyperlinkedRelatedField
+from rest_framework_nested.serializers import NestedHyperlinkedModelSerializer
+from vng_api_common.utils import get_help_text
-from objects.core.models import Object, ObjectRecord, ObjectType, Reference
+from objects.core.models import (
+ Object,
+ ObjectRecord,
+ ObjectType,
+ ObjectTypeVersion,
+ Reference,
+)
from objects.token.models import Permission, TokenAuth
from objects.utils.serializers import DynamicFieldsMixin
from .fields import CachedObjectUrlField, ObjectSlugRelatedField, ObjectTypeField
from .utils import merge_patch
-from .validators import GeometryValidator, IsImmutableValidator, JsonSchemaValidator
+from .validators import (
+ GeometryValidator,
+ IsImmutableValidator,
+ JsonSchemaValidator,
+ ObjectTypeSchemaValidator,
+ VersionUpdateValidator,
+)
logger = structlog.stdlib.get_logger(__name__)
+class ObjectTypeVersionSerializer(NestedHyperlinkedModelSerializer):
+ parent_lookup_kwargs = {"objecttype_uuid": "object_type__uuid"}
+
+ class Meta:
+ model = ObjectTypeVersion
+ fields = (
+ "url",
+ "version",
+ "objectType",
+ "status",
+ "jsonSchema",
+ "createdAt",
+ "modifiedAt",
+ "publishedAt",
+ )
+ extra_kwargs = {
+ "url": {"lookup_field": "version"},
+ "version": {"read_only": True},
+ "objectType": {
+ "source": "object_type",
+ "lookup_field": "uuid",
+ "read_only": True,
+ },
+ "jsonSchema": {
+ "source": "json_schema",
+ "validators": [JsonSchemaValidator()],
+ },
+ "createdAt": {"source": "created_at", "read_only": True},
+ "modifiedAt": {"source": "modified_at", "read_only": True},
+ "publishedAt": {"source": "published_at", "read_only": True},
+ }
+ validators = [VersionUpdateValidator()]
+
+ def validate(self, attrs):
+ valid_attrs = super().validate(attrs)
+
+ # check parent url
+ kwargs = self.context["request"].resolver_match.kwargs
+ if not ObjectType.objects.filter(uuid=kwargs["objecttype_uuid"]).exists():
+ msg = _("Objecttype url is invalid")
+ raise serializers.ValidationError(msg, code="invalid-objecttype")
+
+ return valid_attrs
+
+ def create(self, validated_data):
+ kwargs = self.context["request"].resolver_match.kwargs
+ object_type = ObjectType.objects.get(uuid=kwargs["objecttype_uuid"])
+ validated_data["object_type"] = object_type
+
+ return super().create(validated_data)
+
+
+@extend_schema_field(
+ {
+ "type": "object",
+ "additionalProperties": {"type": "string"},
+ }
+)
+class LabelsField(serializers.JSONField):
+ pass
+
+
+class ObjectTypeSerializer(serializers.HyperlinkedModelSerializer):
+ labels = LabelsField(
+ required=False,
+ help_text=get_help_text("core.ObjectType", "labels"),
+ )
+
+ versions = NestedHyperlinkedRelatedField(
+ many=True,
+ read_only=True,
+ lookup_field="version",
+ view_name="objecttypeversion-detail",
+ parent_lookup_kwargs={"objecttype_uuid": "object_type__uuid"},
+ help_text=_("list of URLs for the OBJECTTYPE versions"),
+ )
+
+ class Meta:
+ model = ObjectType
+ fields = (
+ "url",
+ "uuid",
+ "name",
+ "namePlural",
+ "description",
+ "dataClassification",
+ "maintainerOrganization",
+ "maintainerDepartment",
+ "contactPerson",
+ "contactEmail",
+ "source",
+ "updateFrequency",
+ "providerOrganization",
+ "documentationUrl",
+ "labels",
+ "createdAt",
+ "modifiedAt",
+ "allowGeometry",
+ "versions",
+ )
+ extra_kwargs = {
+ "url": {"lookup_field": "uuid"},
+ "uuid": {
+ "validators": [
+ IsImmutableValidator(),
+ UniqueValidator(queryset=ObjectType.objects.all()),
+ ]
+ },
+ "namePlural": {"source": "name_plural"},
+ "dataClassification": {"source": "data_classification"},
+ "maintainerOrganization": {"source": "maintainer_organization"},
+ "maintainerDepartment": {"source": "maintainer_department"},
+ "contactPerson": {"source": "contact_person"},
+ "contactEmail": {"source": "contact_email"},
+ "updateFrequency": {"source": "update_frequency"},
+ "providerOrganization": {"source": "provider_organization"},
+ "documentationUrl": {"source": "documentation_url"},
+ "allowGeometry": {"source": "allow_geometry"},
+ "createdAt": {"source": "created_at", "read_only": True},
+ "modifiedAt": {"source": "modified_at", "read_only": True},
+ }
+
+
class ReferenceSerializer(serializers.ModelSerializer):
class Meta:
model = Reference
@@ -116,8 +255,7 @@ class ObjectSerializer(DynamicFieldsMixin, serializers.HyperlinkedModelSerialize
min_length=1,
max_length=1000,
source="_object_type",
- queryset=ObjectType.objects.all(),
- help_text=_("Url reference to OBJECTTYPE in Objecttypes API"),
+ help_text=_("Url reference to OBJECTTYPE"),
validators=[IsImmutableValidator()],
)
record = ObjectRecordSerializer(
@@ -130,7 +268,7 @@ class Meta:
extra_kwargs = {
"url": {"lookup_field": "object.uuid"},
}
- validators = [JsonSchemaValidator(), GeometryValidator()]
+ validators = [ObjectTypeSchemaValidator(), GeometryValidator()]
@transaction.atomic
def create(self, validated_data):
@@ -209,8 +347,8 @@ class PermissionSerializer(serializers.ModelSerializer):
min_length=1,
max_length=1000,
source="object_type",
- queryset=ObjectType.objects.all(),
- help_text=_("Url reference to OBJECTTYPE in Objecttypes API"),
+ help_text=_("Url reference to OBJECTTYPE"),
+ validators=[IsImmutableValidator()],
)
class Meta:
diff --git a/src/objects/api/v2/filters.py b/src/objects/api/v2/filters.py
index e97b851f..7311e0f7 100644
--- a/src/objects/api/v2/filters.py
+++ b/src/objects/api/v2/filters.py
@@ -8,10 +8,12 @@
from django_filters import filters
from rest_framework import serializers
from vng_api_common.filtersets import FilterSet
+from vng_api_common.utils import get_help_text
from objects.core.models import ObjectRecord, ObjectType
from objects.utils.filters import ManyCharFilter, ObjectTypeFilter
+from ...core.constants import DataClassificationChoices
from ..constants import Operators
from ..utils import display_choice_values_for_help_text, string_to_value
from ..validators import validate_data_attr, validate_data_attrs
@@ -136,6 +138,18 @@ def filter_data_attr_value_part(
)
+class ObjectTypeFilterSet(FilterSet):
+ dataClassification = filters.ChoiceFilter(
+ field_name="data_classification",
+ choices=DataClassificationChoices.choices,
+ help_text=get_help_text("core.ObjectType", "data_classification"),
+ )
+
+ class Meta:
+ model = ObjectType
+ fields = ("dataClassification",)
+
+
class ObjectRecordFilterForm(forms.Form):
def clean(self):
cleaned_data = super().clean()
@@ -156,7 +170,7 @@ def clean(self):
class ObjectRecordFilterSet(FilterSet):
type = ObjectTypeFilter(
field_name="_object_type",
- help_text=_("Url reference to OBJECTTYPE in Objecttypes API"),
+ help_text=_("Url reference to OBJECTTYPE"),
queryset=ObjectType.objects.all(),
min_length=1,
max_length=1000,
diff --git a/src/objects/api/v2/openapi.yaml b/src/objects/api/v2/openapi.yaml
index cc94168b..7c9d61d6 100644
--- a/src/objects/api/v2/openapi.yaml
+++ b/src/objects/api/v2/openapi.yaml
@@ -216,7 +216,7 @@ paths:
format: uri
maxLength: 1000
minLength: 1
- description: Url reference to OBJECTTYPE in Objecttypes API
+ description: Url reference to OBJECTTYPE
- in: query
name: typeVersion
schema:
@@ -737,7 +737,7 @@ paths:
format: uri
maxLength: 1000
minLength: 1
- description: Url reference to OBJECTTYPE in Objecttypes API
+ description: Url reference to OBJECTTYPE
data_attrs:
type: string
description: |
@@ -849,6 +849,472 @@ paths:
schema:
$ref: '#/components/schemas/PaginatedObjectList'
description: OK
+ /objecttypes:
+ get:
+ operationId: objecttype_list
+ parameters:
+ - in: query
+ name: dataClassification
+ schema:
+ type: string
+ enum:
+ - confidential
+ - intern
+ - open
+ - strictly_confidential
+ description: |-
+ Confidential level of the object type
+
+ * `open` - Open
+ * `intern` - Intern
+ * `confidential` - Confidential
+ * `strictly_confidential` - Strictly confidential
+ - name: page
+ required: false
+ in: query
+ description: A page number within the paginated result set.
+ schema:
+ type: integer
+ - name: pageSize
+ required: false
+ in: query
+ description: 'Het aantal resultaten terug te geven per pagina. (default: 100,
+ maximum: 500).'
+ schema:
+ type: integer
+ tags:
+ - objecttypes
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PaginatedObjectTypeList'
+ description: OK
+ post:
+ operationId: objecttype_create
+ parameters:
+ - in: header
+ name: Content-Type
+ schema:
+ type: string
+ enum:
+ - application/json
+ description: Content type of the request body.
+ required: true
+ tags:
+ - objecttypes
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectType'
+ required: true
+ security:
+ - tokenAuth: []
+ responses:
+ '201':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectType'
+ description: Created
+ /objecttypes/{objecttype_uuid}/versions:
+ get:
+ operationId: objecttypeversion_list
+ description: Retrieve all versions of an OBJECTTYPE
+ parameters:
+ - in: path
+ name: objecttype_uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ - name: page
+ required: false
+ in: query
+ description: A page number within the paginated result set.
+ schema:
+ type: integer
+ - name: pageSize
+ required: false
+ in: query
+ description: 'Het aantal resultaten terug te geven per pagina. (default: 100,
+ maximum: 500).'
+ schema:
+ type: integer
+ tags:
+ - objecttypes
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PaginatedObjectTypeVersionList'
+ description: OK
+ post:
+ operationId: objecttypeversion_create
+ description: Create an OBJECTTYPE with the given version.
+ parameters:
+ - in: header
+ name: Content-Type
+ schema:
+ type: string
+ enum:
+ - application/json
+ description: Content type of the request body.
+ required: true
+ - in: path
+ name: objecttype_uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ tags:
+ - objecttypes
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectTypeVersion'
+ security:
+ - tokenAuth: []
+ responses:
+ '201':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectTypeVersion'
+ description: Created
+ /objecttypes/{objecttype_uuid}/versions/{version}:
+ get:
+ operationId: objecttypeversion_read
+ description: Retrieve an OBJECTTYPE with the given version.
+ parameters:
+ - in: path
+ name: objecttype_uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ - in: path
+ name: version
+ schema:
+ type: integer
+ maximum: 32767
+ minimum: 0
+ description: Integer version of the OBJECTTYPE
+ required: true
+ tags:
+ - objecttypes
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectTypeVersion'
+ description: OK
+ put:
+ operationId: objecttypeversion_update
+ description: Update an OBJECTTYPE with the given version.
+ parameters:
+ - in: header
+ name: Content-Type
+ schema:
+ type: string
+ enum:
+ - application/json
+ description: Content type of the request body.
+ required: true
+ - in: path
+ name: objecttype_uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ - in: path
+ name: version
+ schema:
+ type: integer
+ maximum: 32767
+ minimum: 0
+ description: Integer version of the OBJECTTYPE
+ required: true
+ tags:
+ - objecttypes
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectTypeVersion'
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectTypeVersion'
+ description: OK
+ patch:
+ operationId: objecttypeversion_partial_update
+ description: Partially update an OBJECTTYPE with the given version.
+ parameters:
+ - in: header
+ name: Content-Type
+ schema:
+ type: string
+ enum:
+ - application/json
+ description: Content type of the request body.
+ required: true
+ - in: path
+ name: objecttype_uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ - in: path
+ name: version
+ schema:
+ type: integer
+ maximum: 32767
+ minimum: 0
+ description: Integer version of the OBJECTTYPE
+ required: true
+ tags:
+ - objecttypes
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PatchedObjectTypeVersion'
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectTypeVersion'
+ description: OK
+ delete:
+ operationId: objecttypeversion_delete
+ description: Destroy the given OBJECTTYPE.
+ parameters:
+ - in: path
+ name: objecttype_uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ - in: path
+ name: version
+ schema:
+ type: integer
+ maximum: 32767
+ minimum: 0
+ description: Integer version of the OBJECTTYPE
+ required: true
+ tags:
+ - objecttypes
+ security:
+ - tokenAuth: []
+ responses:
+ '204':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ description: No response body
+ /objecttypes/{uuid}:
+ get:
+ operationId: objecttype_read
+ parameters:
+ - in: path
+ name: uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ tags:
+ - objecttypes
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectType'
+ description: OK
+ put:
+ operationId: objecttype_update
+ parameters:
+ - in: header
+ name: Content-Type
+ schema:
+ type: string
+ enum:
+ - application/json
+ description: Content type of the request body.
+ required: true
+ - in: path
+ name: uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ tags:
+ - objecttypes
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectType'
+ required: true
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectType'
+ description: OK
+ patch:
+ operationId: objecttype_partial_update
+ parameters:
+ - in: header
+ name: Content-Type
+ schema:
+ type: string
+ enum:
+ - application/json
+ description: Content type of the request body.
+ required: true
+ - in: path
+ name: uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ tags:
+ - objecttypes
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PatchedObjectType'
+ security:
+ - tokenAuth: []
+ responses:
+ '200':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ObjectType'
+ description: OK
+ delete:
+ operationId: objecttype_delete
+ parameters:
+ - in: path
+ name: uuid
+ schema:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ required: true
+ tags:
+ - objecttypes
+ security:
+ - tokenAuth: []
+ responses:
+ '204':
+ headers:
+ API-version:
+ schema:
+ type: string
+ description: 'Geeft een specifieke API-versie aan in de context van
+ een specifieke aanroep. Voorbeeld: 1.2.1.'
+ description: No response body
/permissions:
get:
operationId: permission_list
@@ -898,6 +1364,18 @@ components:
description: Kept OBJECT URLs
required:
- behouden
+ DataClassificationEnum:
+ enum:
+ - open
+ - intern
+ - confidential
+ - strictly_confidential
+ type: string
+ description: |-
+ * `open` - Open
+ * `intern` - Intern
+ * `confidential` - Confidential
+ * `strictly_confidential` - Strictly confidential
GeoJSONGeometry:
title: GeoJSONGeometry
type: object
@@ -1108,7 +1586,7 @@ components:
format: uri
minLength: 1
maxLength: 1000
- description: Url reference to OBJECTTYPE in Objecttypes API
+ description: Url reference to OBJECTTYPE
record:
allOf:
- $ref: '#/components/schemas/ObjectRecord'
@@ -1180,6 +1658,183 @@ components:
properties:
geometry:
$ref: '#/components/schemas/GeoWithin'
+ ObjectType:
+ type: object
+ properties:
+ url:
+ type: string
+ format: uri
+ readOnly: true
+ minLength: 1
+ maxLength: 1000
+ description: URL reference to this object. This is the unique identification
+ and location of this object.
+ uuid:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ name:
+ type: string
+ description: Name of the object type
+ maxLength: 100
+ namePlural:
+ type: string
+ description: Plural name of the object type
+ maxLength: 100
+ description:
+ type: string
+ description: The description of the object type
+ maxLength: 1000
+ dataClassification:
+ allOf:
+ - $ref: '#/components/schemas/DataClassificationEnum'
+ description: |-
+ Confidential level of the object type
+
+ * `open` - Open
+ * `intern` - Intern
+ * `confidential` - Confidential
+ * `strictly_confidential` - Strictly confidential
+ maintainerOrganization:
+ type: string
+ description: Organization which is responsible for the object type
+ maxLength: 200
+ maintainerDepartment:
+ type: string
+ description: Business department which is responsible for the object type
+ maxLength: 200
+ contactPerson:
+ type: string
+ description: Name of the person in the organization who can provide information
+ about the object type
+ maxLength: 200
+ contactEmail:
+ type: string
+ description: Email of the person in the organization who can provide information
+ about the object type
+ maxLength: 200
+ source:
+ type: string
+ description: Name of the system from which the object type originates
+ maxLength: 200
+ updateFrequency:
+ allOf:
+ - $ref: '#/components/schemas/UpdateFrequencyEnum'
+ description: |-
+ Indicates how often the object type is updated
+
+ * `real_time` - Real-time
+ * `hourly` - Hourly
+ * `daily` - Daily
+ * `weekly` - Weekly
+ * `monthly` - Monthly
+ * `yearly` - Yearly
+ * `unknown` - Unknown
+ providerOrganization:
+ type: string
+ description: Organization which is responsible for publication of the object
+ type
+ maxLength: 200
+ documentationUrl:
+ type: string
+ format: uri
+ description: Link to the documentation for the object type
+ maxLength: 200
+ labels:
+ type: object
+ additionalProperties:
+ type: string
+ description: Key-value pairs of keywords related for the object type
+ createdAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Date when the object type was created
+ modifiedAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Last date when the object type was modified
+ allowGeometry:
+ type: boolean
+ description: 'Shows whether the related objects can have geographic coordinates.
+ If the value is ''false'' the related objects are not allowed to have
+ coordinates and the creation/update of objects with `geometry` property
+ will raise an error '
+ versions:
+ type: array
+ items:
+ type: string
+ format: uri
+ minLength: 1
+ maxLength: 1000
+ readOnly: true
+ description: list of URLs for the OBJECTTYPE versions
+ required:
+ - name
+ - namePlural
+ ObjectTypeVersion:
+ type: object
+ description: |-
+ A type of `ModelSerializer` that uses hyperlinked relationships with compound keys instead
+ of primary key relationships. Specifically:
+
+ * A 'url' field is included instead of the 'id' field.
+ * Relationships to other instances are hyperlinks, instead of primary keys.
+
+ NOTE: this only works with DRF 3.1.0 and above.
+ properties:
+ url:
+ type: string
+ format: uri
+ minLength: 1
+ maxLength: 1000
+ description: URL reference to this object. This is the unique identification
+ and location of this object.
+ readOnly: true
+ version:
+ type: integer
+ readOnly: true
+ description: Integer version of the OBJECTTYPE
+ objectType:
+ type: string
+ format: uri
+ minLength: 1
+ maxLength: 1000
+ description: URL reference to this object. This is the unique identification
+ and location of this object.
+ readOnly: true
+ status:
+ allOf:
+ - $ref: '#/components/schemas/StatusEnum'
+ description: |-
+ Status of the object type version
+
+ * `published` - Published
+ * `draft` - Draft
+ * `deprecated` - Deprecated
+ jsonSchema:
+ type: object
+ additionalProperties: true
+ title: JSON schema
+ description: JSON schema for Object validation
+ createdAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Date when the version was created
+ modifiedAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Last date when the version was modified
+ publishedAt:
+ type: string
+ format: date
+ readOnly: true
+ nullable: true
+ title: Published_at
+ description: Date when the version was published
PaginatedHistoryRecordList:
type: object
required:
@@ -1226,6 +1881,52 @@ components:
type: array
items:
$ref: '#/components/schemas/Object'
+ PaginatedObjectTypeList:
+ type: object
+ required:
+ - count
+ - results
+ properties:
+ count:
+ type: integer
+ example: 123
+ next:
+ type: string
+ nullable: true
+ format: uri
+ example: http://api.example.org/accounts/?page=4
+ previous:
+ type: string
+ nullable: true
+ format: uri
+ example: http://api.example.org/accounts/?page=2
+ results:
+ type: array
+ items:
+ $ref: '#/components/schemas/ObjectType'
+ PaginatedObjectTypeVersionList:
+ type: object
+ required:
+ - count
+ - results
+ properties:
+ count:
+ type: integer
+ example: 123
+ next:
+ type: string
+ nullable: true
+ format: uri
+ example: http://api.example.org/accounts/?page=4
+ previous:
+ type: string
+ nullable: true
+ format: uri
+ example: http://api.example.org/accounts/?page=2
+ results:
+ type: array
+ items:
+ $ref: '#/components/schemas/ObjectTypeVersion'
PaginatedPermissionList:
type: object
required:
@@ -1272,11 +1973,185 @@ components:
format: uri
minLength: 1
maxLength: 1000
- description: Url reference to OBJECTTYPE in Objecttypes API
+ description: Url reference to OBJECTTYPE
record:
allOf:
- $ref: '#/components/schemas/ObjectRecord'
description: State of the OBJECT at a certain time
+ PatchedObjectType:
+ type: object
+ properties:
+ url:
+ type: string
+ format: uri
+ readOnly: true
+ minLength: 1
+ maxLength: 1000
+ description: URL reference to this object. This is the unique identification
+ and location of this object.
+ uuid:
+ type: string
+ format: uuid
+ description: Unique identifier (UUID4)
+ name:
+ type: string
+ description: Name of the object type
+ maxLength: 100
+ namePlural:
+ type: string
+ description: Plural name of the object type
+ maxLength: 100
+ description:
+ type: string
+ description: The description of the object type
+ maxLength: 1000
+ dataClassification:
+ allOf:
+ - $ref: '#/components/schemas/DataClassificationEnum'
+ description: |-
+ Confidential level of the object type
+
+ * `open` - Open
+ * `intern` - Intern
+ * `confidential` - Confidential
+ * `strictly_confidential` - Strictly confidential
+ maintainerOrganization:
+ type: string
+ description: Organization which is responsible for the object type
+ maxLength: 200
+ maintainerDepartment:
+ type: string
+ description: Business department which is responsible for the object type
+ maxLength: 200
+ contactPerson:
+ type: string
+ description: Name of the person in the organization who can provide information
+ about the object type
+ maxLength: 200
+ contactEmail:
+ type: string
+ description: Email of the person in the organization who can provide information
+ about the object type
+ maxLength: 200
+ source:
+ type: string
+ description: Name of the system from which the object type originates
+ maxLength: 200
+ updateFrequency:
+ allOf:
+ - $ref: '#/components/schemas/UpdateFrequencyEnum'
+ description: |-
+ Indicates how often the object type is updated
+
+ * `real_time` - Real-time
+ * `hourly` - Hourly
+ * `daily` - Daily
+ * `weekly` - Weekly
+ * `monthly` - Monthly
+ * `yearly` - Yearly
+ * `unknown` - Unknown
+ providerOrganization:
+ type: string
+ description: Organization which is responsible for publication of the object
+ type
+ maxLength: 200
+ documentationUrl:
+ type: string
+ format: uri
+ description: Link to the documentation for the object type
+ maxLength: 200
+ labels:
+ type: object
+ additionalProperties:
+ type: string
+ description: Key-value pairs of keywords related for the object type
+ createdAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Date when the object type was created
+ modifiedAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Last date when the object type was modified
+ allowGeometry:
+ type: boolean
+ description: 'Shows whether the related objects can have geographic coordinates.
+ If the value is ''false'' the related objects are not allowed to have
+ coordinates and the creation/update of objects with `geometry` property
+ will raise an error '
+ versions:
+ type: array
+ items:
+ type: string
+ format: uri
+ minLength: 1
+ maxLength: 1000
+ readOnly: true
+ description: list of URLs for the OBJECTTYPE versions
+ PatchedObjectTypeVersion:
+ type: object
+ description: |-
+ A type of `ModelSerializer` that uses hyperlinked relationships with compound keys instead
+ of primary key relationships. Specifically:
+
+ * A 'url' field is included instead of the 'id' field.
+ * Relationships to other instances are hyperlinks, instead of primary keys.
+
+ NOTE: this only works with DRF 3.1.0 and above.
+ properties:
+ url:
+ type: string
+ format: uri
+ minLength: 1
+ maxLength: 1000
+ description: URL reference to this object. This is the unique identification
+ and location of this object.
+ readOnly: true
+ version:
+ type: integer
+ readOnly: true
+ description: Integer version of the OBJECTTYPE
+ objectType:
+ type: string
+ format: uri
+ minLength: 1
+ maxLength: 1000
+ description: URL reference to this object. This is the unique identification
+ and location of this object.
+ readOnly: true
+ status:
+ allOf:
+ - $ref: '#/components/schemas/StatusEnum'
+ description: |-
+ Status of the object type version
+
+ * `published` - Published
+ * `draft` - Draft
+ * `deprecated` - Deprecated
+ jsonSchema:
+ type: object
+ additionalProperties: true
+ title: JSON schema
+ description: JSON schema for Object validation
+ createdAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Date when the version was created
+ modifiedAt:
+ type: string
+ format: date
+ readOnly: true
+ description: Last date when the version was modified
+ publishedAt:
+ type: string
+ format: date
+ readOnly: true
+ nullable: true
+ title: Published_at
+ description: Date when the version was published
Permission:
type: object
properties:
@@ -1285,7 +2160,7 @@ components:
format: uri
minLength: 1
maxLength: 1000
- description: Url reference to OBJECTTYPE in Objecttypes API
+ description: Url reference to OBJECTTYPE
mode:
allOf:
- $ref: '#/components/schemas/ModeEnum'
@@ -1362,6 +2237,34 @@ components:
- zaak
type: string
description: '* `zaak` - Zaak'
+ StatusEnum:
+ enum:
+ - published
+ - draft
+ - deprecated
+ type: string
+ description: |-
+ * `published` - Published
+ * `draft` - Draft
+ * `deprecated` - Deprecated
+ UpdateFrequencyEnum:
+ enum:
+ - real_time
+ - hourly
+ - daily
+ - weekly
+ - monthly
+ - yearly
+ - unknown
+ type: string
+ description: |-
+ * `real_time` - Real-time
+ * `hourly` - Hourly
+ * `daily` - Daily
+ * `weekly` - Weekly
+ * `monthly` - Monthly
+ * `yearly` - Yearly
+ * `unknown` - Unknown
securitySchemes:
tokenAuth:
type: apiKey
@@ -1372,6 +2275,7 @@ servers:
- url: /api/v2
tags:
- name: objects
+- name: objecttypes
- name: permissions
externalDocs:
url: https://objects-and-objecttypes-api.readthedocs.io/
diff --git a/src/objects/api/v2/urls.py b/src/objects/api/v2/urls.py
index d618a549..84e73267 100644
--- a/src/objects/api/v2/urls.py
+++ b/src/objects/api/v2/urls.py
@@ -3,7 +3,7 @@
from drf_spectacular.views import (
SpectacularRedocView,
)
-from rest_framework import routers
+from vng_api_common import routers
from objects.utils.oas_extensions.views import (
DeprecationRedirectView,
@@ -11,9 +11,21 @@
SpectacularYAMLAPIView,
)
-from .views import ObjectViewSet, PermissionViewSet
+from .views import (
+ ObjectTypeVersionViewSet,
+ ObjectTypeViewSet,
+ ObjectViewSet,
+ PermissionViewSet,
+)
router = routers.DefaultRouter(trailing_slash=False)
+router.register(
+ r"objecttypes",
+ ObjectTypeViewSet,
+ [routers.Nested("versions", ObjectTypeVersionViewSet)],
+ basename="objecttype",
+)
+
router.register(r"objects", ObjectViewSet, basename="object")
router.register(r"permissions", PermissionViewSet)
diff --git a/src/objects/api/v2/views.py b/src/objects/api/v2/views.py
index 3ae11fa5..46448083 100644
--- a/src/objects/api/v2/views.py
+++ b/src/objects/api/v2/views.py
@@ -4,6 +4,7 @@
from django.db import models, transaction
from django.urls import reverse
from django.utils.dateparse import parse_date
+from django.utils.translation import gettext_lazy as _
import structlog
from drf_spectacular.utils import (
@@ -17,18 +18,25 @@
from notifications_api_common.cloudevents import process_cloudevent
from rest_framework import mixins, serializers, status, viewsets
from rest_framework.decorators import action
+from rest_framework.exceptions import ValidationError
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
+from rest_framework.settings import api_settings
from vng_api_common.filters_backend import Backend as FilterBackend
from vng_api_common.pagination import DynamicPageSizePagination
from vng_api_common.search import SearchMixin
+from objects.api.metrics import (
+ objecttype_create_counter,
+ objecttype_delete_counter,
+ objecttype_update_counter,
+)
from objects.cloud_events.constants import ZAAK_ONTKOPPELD
from objects.cloud_events.tasks import send_zaak_events
-from objects.core.constants import ReferenceType
-from objects.core.models import Object, ObjectRecord
-from objects.token.models import Permission
-from objects.token.permissions import ObjectTypeBasedPermission
+from objects.core.constants import ObjectTypeVersionStatus, ReferenceType
+from objects.core.models import Object, ObjectRecord, ObjectType, ObjectTypeVersion
+from objects.token.models import Permission, TokenAuth
+from objects.token.permissions import IsTokenAuthenticated, ObjectTypeBasedPermission
from ..filter_backends import OrderingBackend
from ..kanalen import KANAAL_OBJECTEN
@@ -37,15 +45,24 @@
objects_delete_counter,
objects_update_counter,
)
-from ..mixins import GeoMixin, ObjectNotificationMixin
+from ..mixins import GeoMixin, NestedViewSetMixin, ObjectNotificationMixin
from ..serializers import (
HistoryRecordSerializer,
ObjectSearchSerializer,
ObjectSerializer,
+ ObjectTypeSerializer,
+ ObjectTypeVersionSerializer,
PermissionSerializer,
)
from ..utils import is_date
-from .filters import DATA_ATTR_HELP_TEXT, DATA_ATTRS_HELP_TEXT, ObjectRecordFilterSet
+from .filters import (
+ DATA_ATTR_HELP_TEXT,
+ DATA_ATTRS_HELP_TEXT,
+ ObjectRecordFilterSet,
+ ObjectTypeFilterSet,
+)
+
+logger = structlog.stdlib.get_logger(__name__)
# manually override OAS because of "deprecated" attribute
data_attrs_parameter = OpenApiParameter(
@@ -68,6 +85,148 @@
logger = structlog.stdlib.get_logger(__name__)
+@extend_schema_view(
+ retrieve=extend_schema(operation_id="objecttype_read"),
+ destroy=extend_schema(operation_id="objecttype_delete"),
+)
+class ObjectTypeViewSet(viewsets.ModelViewSet):
+ queryset = ObjectType.objects.prefetch_related("versions").order_by("-pk")
+ serializer_class = ObjectTypeSerializer
+ lookup_field = "uuid"
+ filterset_class = ObjectTypeFilterSet
+ pagination_class = DynamicPageSizePagination
+ permission_classes = [IsTokenAuthenticated]
+
+ def perform_create(self, serializer):
+ super().perform_create(serializer)
+ obj = serializer.instance
+ token_auth: TokenAuth = self.request.auth
+ logger.info(
+ "objecttype_created",
+ uuid=str(obj.uuid),
+ name=obj.name,
+ token_identifier=token_auth.identifier,
+ token_application=token_auth.application,
+ )
+ objecttype_create_counter.add(1)
+
+ def perform_update(self, serializer):
+ super().perform_update(serializer)
+ obj = serializer.instance
+ token_auth: TokenAuth = self.request.auth
+ logger.info(
+ "objecttype_updated",
+ uuid=str(obj.uuid),
+ name=obj.name,
+ token_identifier=token_auth.identifier,
+ token_application=token_auth.application,
+ )
+ objecttype_update_counter.add(1)
+
+ def perform_destroy(self, instance):
+ if instance.versions.exists():
+ raise ValidationError(
+ {
+ api_settings.NON_FIELD_ERRORS_KEY: [
+ _(
+ "All related versions should be destroyed before destroying the objecttype"
+ )
+ ]
+ },
+ code="pending-versions",
+ )
+
+ super().perform_destroy(instance)
+ token_auth: TokenAuth = self.request.auth
+ logger.info(
+ "objecttype_deleted",
+ uuid=str(instance.uuid),
+ name=instance.name,
+ token_identifier=token_auth.identifier,
+ token_application=token_auth.application,
+ )
+ objecttype_delete_counter.add(1)
+
+
+@extend_schema_view(
+ retrieve=extend_schema(
+ operation_id="objecttypeversion_read",
+ description=_("Retrieve an OBJECTTYPE with the given version."),
+ ),
+ list=extend_schema(
+ operation_id="objecttypeversion_list",
+ description=_("Retrieve all versions of an OBJECTTYPE"),
+ ),
+ create=extend_schema(
+ operation_id="objecttypeversion_create",
+ description=_("Create an OBJECTTYPE with the given version."),
+ ),
+ destroy=extend_schema(
+ operation_id="objecttypeversion_delete",
+ description=_("Destroy the given OBJECTTYPE."),
+ ),
+ update=extend_schema(
+ operation_id="objecttypeversion_update",
+ description=_("Update an OBJECTTYPE with the given version."),
+ ),
+ partial_update=extend_schema(
+ operation_id="objecttypeversion_partial_update",
+ description=_("Partially update an OBJECTTYPE with the given version."),
+ ),
+)
+class ObjectTypeVersionViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
+ queryset = ObjectTypeVersion.objects.order_by("object_type", "-version")
+ serializer_class = ObjectTypeVersionSerializer
+ lookup_field = "version"
+ pagination_class = DynamicPageSizePagination
+ permission_classes = [IsTokenAuthenticated]
+
+ def perform_create(self, serializer):
+ super().perform_create(serializer)
+ obj = serializer.instance
+ token_auth = self.request.auth
+ logger.info(
+ "object_version_created",
+ version=str(obj.version),
+ objecttype_uuid=str(obj.object_type.uuid),
+ token_identifier=token_auth.identifier,
+ token_application=token_auth.application,
+ )
+
+ def perform_update(self, serializer):
+ super().perform_update(serializer)
+ obj = serializer.instance
+ token_auth = self.request.auth
+ logger.info(
+ "object_version_updated",
+ version=str(obj.version),
+ objecttype_uuid=str(obj.object_type.uuid),
+ token_identifier=token_auth.identifier,
+ token_application=token_auth.application,
+ )
+
+ def perform_destroy(self, instance):
+ if instance.status != ObjectTypeVersionStatus.draft:
+ raise ValidationError(
+ {
+ api_settings.NON_FIELD_ERRORS_KEY: [
+ _("Only draft versions can be destroyed")
+ ]
+ },
+ code="non-draft-version-destroy",
+ )
+
+ super().perform_destroy(instance)
+ token_auth = self.request.auth
+ logger.info(
+ "object_version_deleted",
+ version=str(instance.version),
+ objecttype_uuid=str(instance.object_type.uuid),
+ token_identifier=token_auth.identifier,
+ token_application=token_auth.application,
+ )
+
+
@extend_schema_view(
list=extend_schema(
description="Retrieve a list of OBJECTs and their actual RECORD. "
@@ -125,7 +284,6 @@ class ObjectViewSet(
queryset = (
ObjectRecord.objects.select_related(
"_object_type",
- "_object_type__service",
"correct",
"corrected",
)
diff --git a/src/objects/api/validators.py b/src/objects/api/validators.py
index 69f394a1..6d7d5c02 100644
--- a/src/objects/api/validators.py
+++ b/src/objects/api/validators.py
@@ -1,19 +1,42 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
-import requests
from rest_framework import serializers
from rest_framework.fields import get_attribute
-from objects.core.utils import check_objecttype_cached
-from objects.utils.client import get_objecttypes_client
+from objects.core.utils import check_json_schema, check_objecttype
+from ..core.constants import ObjectTypeVersionStatus
from .constants import Operators
from .utils import merge_patch, string_to_value
+class VersionUpdateValidator:
+ message = _("Only draft versions can be changed")
+ code = "non-draft-version-update"
+ requires_context = True
+
+ def __call__(self, attrs, serializer):
+ instance = getattr(serializer, "instance", None)
+ if not instance:
+ return
+
+ if instance.status != ObjectTypeVersionStatus.draft:
+ raise serializers.ValidationError(self.message, code=self.code)
+
+
class JsonSchemaValidator:
code = "invalid-json-schema"
+
+ def __call__(self, value):
+ try:
+ check_json_schema(value)
+ except ValidationError as exc:
+ raise serializers.ValidationError(exc.args[0], code=self.code) from exc
+
+
+class ObjectTypeSchemaValidator:
+ code = "invalid-json-schema"
requires_context = True
def __call__(self, attrs, serializer):
@@ -41,7 +64,7 @@ def __call__(self, attrs, serializer):
if not object_type or not version:
return
try:
- check_objecttype_cached(object_type, version, data)
+ check_objecttype(object_type, version, data)
except ValidationError as exc:
raise serializers.ValidationError(exc.args[0], code=self.code) from exc
@@ -130,14 +153,5 @@ def __call__(self, attrs, serializer):
if not geometry:
return
- with get_objecttypes_client(object_type.service) as client:
- try:
- response_data = client.get_objecttype(object_type.uuid)
- except requests.RequestException as exc:
- msg = f"Object type can not be retrieved: {exc.args[0]}"
- raise ValidationError(msg)
-
- allow_geometry = response_data.get("allowGeometry", True)
-
- if geometry and not allow_geometry:
+ if geometry and not object_type.allow_geometry:
raise serializers.ValidationError(self.message, code=self.code)
diff --git a/src/objects/conf/api.py b/src/objects/conf/api.py
index b7a7e09b..cd4c6920 100644
--- a/src/objects/conf/api.py
+++ b/src/objects/conf/api.py
@@ -108,7 +108,11 @@
"drf_spectacular.hooks.postprocess_schema_enums",
"maykin_common.drf_spectacular.hooks.remove_invalid_url_defaults",
],
- "TAGS": [{"name": "objects"}, {"name": "permissions"}],
+ "TAGS": [
+ {"name": "objects"},
+ {"name": "objecttypes"},
+ {"name": "permissions"},
+ ], # TODO
"SERVERS": [{"url": "/api/v2"}],
}
diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py
index 7a82fc62..45a44a07 100644
--- a/src/objects/conf/base.py
+++ b/src/objects/conf/base.py
@@ -1,5 +1,12 @@
import os
+from upgrade_check.constraints import (
+ CommandCheck,
+ UpgradeCheck,
+ UpgradePaths,
+ VersionRange,
+)
+
os.environ["_USE_STRUCTLOG"] = "True"
from django.core.exceptions import ImproperlyConfigured
@@ -37,6 +44,7 @@
"django.contrib.sites",
# External applications.
"rest_framework_gis",
+ "jsonsuit.apps.JSONSuitConfig",
# Project applications.
"objects.accounts",
"objects.setup_configuration",
@@ -57,17 +65,6 @@
# FIXME should this be UTC?
TIME_ZONE = "Europe/Amsterdam"
-#
-# Caches
-#
-
-OBJECTTYPE_VERSION_CACHE_TIMEOUT = config(
- "OBJECTTYPE_VERSION_CACHE_TIMEOUT",
- default=5 * 60, # 300 seconds
- help_text="Timeout in seconds for cache when retrieving objecttype versions.",
- group="Cache",
-)
-
#
# Additional Django settings
#
@@ -182,3 +179,11 @@
# Note: the LOGIN_URL Django setting is not used because you could have
# multiple login urls defined.
LOGIN_URLS = [reverse_lazy("admin:login")]
+
+
+UPGRADE_CHECK_PATHS: UpgradePaths = {
+ "4.0.0": UpgradeCheck(
+ VersionRange(minimum="3.6.0"),
+ code_checks=[CommandCheck("check_for_external_objecttypes")],
+ ),
+}
diff --git a/src/objects/conf/ci.py b/src/objects/conf/ci.py
index b6944643..da397a9e 100644
--- a/src/objects/conf/ci.py
+++ b/src/objects/conf/ci.py
@@ -12,6 +12,9 @@
os.environ.setdefault("OTEL_SDK_DISABLED", "true")
os.environ.setdefault("OTEL_SERVICE_NAME", "objects-ci")
+os.environ.setdefault("DB_USER", "postgres")
+os.environ.setdefault("DB_PASSWORD", "postgres")
+
from .base import * # noqa isort:skip
CACHES = {
diff --git a/src/objects/core/admin.py b/src/objects/core/admin.py
index 9fd84cef..95c7b35c 100644
--- a/src/objects/core/admin.py
+++ b/src/objects/core/admin.py
@@ -1,54 +1,187 @@
+import json
from typing import Sequence
from django import forms
from django.conf import settings
-from django.contrib import admin
-from django.contrib.admin import SimpleListFilter
+from django.contrib import admin, messages
from django.contrib.gis.db.models import GeometryField
-from django.http import HttpRequest, JsonResponse
-from django.urls import path
+from django.db import models
+from django.http import HttpRequest, HttpResponseRedirect
+from django.shortcuts import redirect, render
+from django.urls import path, reverse
+from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
-import requests
import structlog
+from jsonsuit.widgets import READONLY_WIDGET_MEDIA_CSS, READONLY_WIDGET_MEDIA_JS
from vng_api_common.utils import get_help_text
from objects.api.v2.filters import filter_queryset_by_data_attr
-from objects.utils.client import get_objecttypes_client
-from .models import Object, ObjectRecord, ObjectType
+from .constants import ObjectTypeVersionStatus
+from .forms import ObjectTypeVersionForm, UrlImportForm
+from .models import Object, ObjectRecord, ObjectType, ObjectTypeVersion
+from .widgets import JSONSuit
logger = structlog.stdlib.get_logger(__name__)
+def can_change(obj) -> bool:
+ if not obj:
+ return True
+
+ if not obj.last_version:
+ return True
+
+ if obj.last_version.status == ObjectTypeVersionStatus.draft:
+ return True
+
+ return False
+
+
+class ObjectTypeVersionInline(admin.StackedInline):
+ verbose_name_plural = _("last version")
+ model = ObjectTypeVersion
+ form = ObjectTypeVersionForm
+ extra = 0
+ max_num = 1
+ min_num = 1
+ readonly_fields = ("version", "status", "published_at")
+ formfield_overrides = {
+ models.JSONField: {
+ "widget": JSONSuit,
+ "error_messages": {
+ "invalid": _("'%(value)s' value must be valid JSON"),
+ },
+ }
+ }
+
+ def get_queryset(self, request):
+ queryset = super().get_queryset(request)
+ parent_id = request.resolver_match.kwargs.get("object_id")
+ if not parent_id:
+ return queryset
+
+ last_version = (
+ queryset.filter(object_type_id=parent_id).order_by("-version").first()
+ )
+ if not last_version:
+ return queryset.none()
+ return queryset.filter(id=last_version.id)
+
+ def has_delete_permission(self, request, obj=None):
+ return False
+
+ # work around to prettify readonly JSON field
+ def get_exclude(self, request, obj=None):
+ if not can_change(obj):
+ return ("json_schema",)
+ return super().get_exclude(request, obj)
+
+ def get_readonly_fields(self, request, obj=None):
+ if not can_change(obj):
+ local_fields = [field.name for field in self.opts.local_fields]
+ # work around to prettify readonly JSON field
+ local_fields.remove("json_schema")
+ local_fields.append("json_schema_readonly")
+ return local_fields
+
+ return super().get_readonly_fields(request, obj)
+
+ def json_schema_readonly(self, obj):
+ return format_html(
+ '
',
+ json.dumps(obj.json_schema, indent=2),
+ )
+
+ json_schema_readonly.short_description = "JSON schema"
+
+ class Media:
+ js = READONLY_WIDGET_MEDIA_JS
+ css = READONLY_WIDGET_MEDIA_CSS
+
+
@admin.register(ObjectType)
class ObjectTypeAdmin(admin.ModelAdmin):
- list_display = (
- "_name",
- "uuid",
- )
- readonly_fields = ("_name",)
+ list_display = ("name", "name_plural", "allow_geometry")
+ search_fields = ("name", "name_plural", "uuid")
+ inlines = [ObjectTypeVersionInline]
+
+ change_list_template = "admin/core/objecttype/object_list.html"
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(
- "/_versions/",
- self.admin_site.admin_view(self.versions_view),
- name="objecttype_versions",
- )
+ "import-from-url/",
+ self.admin_site.admin_view(self.import_from_url_view),
+ name="import_from_url",
+ ),
]
return my_urls + urls
- def versions_view(self, request, objecttype_id):
- versions = []
- if objecttype := self.get_object(request, objecttype_id):
- with get_objecttypes_client(objecttype.service) as client:
- try:
- versions = client.list_objecttype_versions(objecttype.uuid)
- except (requests.RequestException, requests.JSONDecodeError) as exc:
- logger.exception("objecttypes_api_request_failure", exc_info=exc)
- return JsonResponse(versions, safe=False)
+ def get_readonly_fields(self, request, obj=None):
+ readonly_fields = super().get_readonly_fields(request, obj)
+
+ if obj:
+ readonly_fields = ("uuid",) + readonly_fields
+
+ return readonly_fields
+
+ def publish(self, request, obj):
+ last_version = obj.last_version
+ last_version.status = ObjectTypeVersionStatus.published
+ last_version.save()
+
+ msg = format_html(
+ _("The object type {version} has been published successfully!"),
+ version=obj.last_version,
+ )
+ self.message_user(request, msg, level=messages.SUCCESS)
+
+ return HttpResponseRedirect(request.path)
+
+ def add_new_version(self, request, obj):
+ new_version = obj.last_version
+ new_version.pk = None
+ new_version.version = new_version.version + 1
+ new_version.status = ObjectTypeVersionStatus.draft
+ new_version.save()
+
+ msg = format_html(
+ _("The new version {version} has been created successfully!"),
+ version=new_version,
+ )
+ self.message_user(request, msg, level=messages.SUCCESS)
+
+ return HttpResponseRedirect(request.path)
+
+ def response_change(self, request, obj):
+ if "_publish" in request.POST:
+ return self.publish(request, obj)
+
+ if "_newversion" in request.POST:
+ return self.add_new_version(request, obj)
+
+ return super().response_change(request, obj)
+
+ def import_from_url_view(self, request):
+ if request.method == "POST":
+ form = UrlImportForm(request.POST)
+ if form.is_valid():
+ form_json = form.cleaned_data.get("json")
+
+ ObjectType.objects.create_from_schema(
+ json_schema=form_json,
+ name_plural=form.data.get("name_plural", "").title(),
+ )
+ return redirect(reverse("admin:core_objecttype_changelist"))
+ else:
+ form = UrlImportForm()
+
+ return render(
+ request, "admin/core/objecttype/object_import_form.html", {"form": form}
+ )
class ObjectRecordForm(forms.ModelForm):
@@ -112,26 +245,6 @@ def get_corrected_by(self, obj):
get_corrected_by.short_description = "corrected by"
-class ObjectTypeFilter(SimpleListFilter):
- """
- List filters do not use `ModelAdmin.list_select_related` unfortunately, so to avoid
- additional queries for each ObjectType.service, the filter's queryset is explicitly
- overridden
- """
-
- title = "object type"
- parameter_name = "object_type__id__exact"
-
- def lookups(self, request, model_admin):
- qs = ObjectType.objects.select_related("service")
- return [(ot.pk, str(ot)) for ot in qs]
-
- def queryset(self, request, queryset):
- if self.value():
- return queryset.filter(object_type__id=self.value())
- return queryset
-
-
@admin.register(Object)
class ObjectAdmin(admin.ModelAdmin):
list_display = (
@@ -145,7 +258,7 @@ class ObjectAdmin(admin.ModelAdmin):
)
search_fields = ("uuid",)
inlines = (ObjectRecordInline,)
- list_filter = (ObjectTypeFilter, "created_on", "modified_on")
+ list_filter = ("object_type", "created_on", "modified_on")
def get_search_fields(self, request: HttpRequest) -> Sequence[str]:
if settings.OBJECTS_ADMIN_SEARCH_DISABLED:
diff --git a/src/objects/core/constants.py b/src/objects/core/constants.py
index da95e602..6160d339 100644
--- a/src/objects/core/constants.py
+++ b/src/objects/core/constants.py
@@ -2,7 +2,7 @@
from django.utils.translation import gettext_lazy as _
-class ObjectVersionStatus(models.TextChoices):
+class ObjectTypeVersionStatus(models.TextChoices):
published = "published", _("Published")
draft = "draft", _("Draft")
deprecated = "deprecated", _("Deprecated")
diff --git a/src/objects/core/forms.py b/src/objects/core/forms.py
new file mode 100644
index 00000000..a8b05fdb
--- /dev/null
+++ b/src/objects/core/forms.py
@@ -0,0 +1,72 @@
+from json.decoder import JSONDecodeError
+
+from django import forms
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext_lazy as _
+
+import requests
+from rest_framework import exceptions
+
+from ..api.validators import JsonSchemaValidator
+from .models import ObjectTypeVersion
+
+
+class UrlImportForm(forms.Form):
+ objecttype_url = forms.URLField(
+ label="Objecttype URL",
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": "https://example.com/boom.json",
+ "size": 100,
+ }
+ ),
+ required=True,
+ help_text=_("The direct URL for a given objecttype file (JSON)."),
+ )
+ name_plural = forms.CharField(
+ label=_("Plural name"),
+ max_length=100,
+ required=True,
+ help_text=_("The plural name variant of the objecttype."),
+ )
+
+ def clean_objecttype_url(self):
+ url = self.cleaned_data["objecttype_url"]
+
+ try:
+ response = requests.get(url)
+ except requests.exceptions.RequestException:
+ raise ValidationError("The Objecttype URL does not exist.")
+
+ if response.status_code != requests.codes.ok:
+ raise ValidationError("Objecttype URL returned non OK status.")
+
+ try:
+ response_json = response.json()
+ except JSONDecodeError:
+ raise ValidationError("Could not parse JSON from Objecttype URL.")
+
+ json_schema_validator = JsonSchemaValidator()
+
+ try:
+ json_schema_validator(response_json)
+ except exceptions.ValidationError as e:
+ raise ValidationError(
+ f"Invalid JSON schema. {e.detail[0]}.", code=e.detail[0].code
+ )
+
+ self.cleaned_data["json"] = response_json
+
+
+class ObjectTypeVersionForm(forms.ModelForm):
+ class Meta:
+ model = ObjectTypeVersion
+ fields = "__all__"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Pass the initial value to the widget, this value is used in case
+ # the new value is invalid JSON which causes the widget to break
+ if "json_schema" in self.initial:
+ self.fields["json_schema"].widget.initial = self.initial["json_schema"]
diff --git a/src/objects/core/management/commands/check_for_external_objecttypes.py b/src/objects/core/management/commands/check_for_external_objecttypes.py
new file mode 100644
index 00000000..cedc8967
--- /dev/null
+++ b/src/objects/core/management/commands/check_for_external_objecttypes.py
@@ -0,0 +1,30 @@
+from django.core.management import BaseCommand, CommandError
+
+from objects.core.models import ObjectType
+
+
+class Command(BaseCommand):
+ help = "Checks if external objecttypes exist"
+
+ def _get_objecttype(self):
+ """
+ Separated for easier mocking
+ """
+ return ObjectType
+
+ def handle(self, *args, **options):
+ ObjectType = self._get_objecttype()
+
+ external_object_count = 0
+ external_uuids = set()
+ for objecttype in ObjectType.objects.iterator():
+ if not objecttype.is_imported:
+ external_object_count += 1
+ external_uuids.add(objecttype.uuid)
+
+ msg = f"{external_object_count} objectype(s) have not been imported: {external_uuids}"
+
+ self.stdout.write(self.style.ERROR(msg))
+
+ if external_object_count > 0:
+ raise CommandError(msg)
diff --git a/src/objects/core/management/commands/import_objecttypes.py b/src/objects/core/management/commands/import_objecttypes.py
index f12b4dba..c1e62d0c 100644
--- a/src/objects/core/management/commands/import_objecttypes.py
+++ b/src/objects/core/management/commands/import_objecttypes.py
@@ -14,7 +14,7 @@
# Minimum Objecttypes application version is 3.4.0, because that version added the
# version header to the responses
-MIN_OBJECTTYPES_VERSION = "2.2.2"
+MIN_OBJECTTYPES_API_VERSION = "2.2.2"
class Command(BaseCommand):
@@ -36,7 +36,7 @@ def handle(self, *args, **options):
self._check_objecttypes_api_version(client)
objecttypes = client.list_objecttypes()
- data = self._parse_objecttype_data(objecttypes, service)
+ data = self._parse_objecttype_data(objecttypes)
self._bulk_create_or_update_objecttypes(data)
self.stdout.write("Successfully imported %s objecttypes" % len(data))
@@ -44,7 +44,7 @@ def handle(self, *args, **options):
objecttype_versions = client.list_objecttype_versions(
objecttype.uuid
)
- data = self._parse_objectversion_data(
+ data = self._parse_objecttypeversion_data(
objecttype_versions, objecttype
)
self._bulk_create_or_update_objecttype_versions(data)
@@ -70,10 +70,10 @@ def _check_objecttypes_api_version(self, client):
api_version = client.get_objecttypes_api_version()
if api_version is None or Version(
client.get_objecttypes_api_version()
- ) < Version(MIN_OBJECTTYPES_VERSION):
+ ) < Version(MIN_OBJECTTYPES_API_VERSION):
raise CommandError(
_("Object types API version must be {} or higher.").format(
- MIN_OBJECTTYPES_VERSION
+ MIN_OBJECTTYPES_API_VERSION
)
)
@@ -83,11 +83,8 @@ def _bulk_create_or_update_objecttypes(self, data):
update_conflicts=True, # Updates existing Objecttypes based on unique_fields
unique_fields=[
"uuid",
- "service",
- ], # TODO remove service from unique_fields after objecttype migration since it will no longer be part of the ObjectType model.
+ ],
update_fields=[
- "is_imported",
- "_name",
"name",
"name_plural",
"description",
@@ -125,21 +122,18 @@ def _bulk_create_or_update_objecttype_versions(self, data):
)
def _parse_objecttype_data(
- self, objecttypes: list[dict[str, Any]], service: Service
+ self, objecttypes: list[dict[str, Any]]
) -> list[ObjectType]:
data = []
for objecttype in objecttypes:
- objecttype.pop("versions")
- objecttype.pop("url")
# This attribute was added in 3.4.0 but removed in 3.4.1
objecttype.pop("linkableToZaken", None)
- objecttype["service"] = service
- objecttype["is_imported"] = True
- objecttype["_name"] = objecttype["name"]
+ objecttype.pop("versions")
+ objecttype.pop("url")
data.append(ObjectType(**underscoreize(objecttype)))
return data
- def _parse_objectversion_data(
+ def _parse_objecttypeversion_data(
self, objecttype_versions: list[dict[str, Any]], objecttype
) -> list[ObjectTypeVersion]:
data = []
diff --git a/src/objects/core/migrations/0039_alter_objecttype_unique_together_and_more.py b/src/objects/core/migrations/0039_alter_objecttype_unique_together_and_more.py
new file mode 100644
index 00000000..6a5910cd
--- /dev/null
+++ b/src/objects/core/migrations/0039_alter_objecttype_unique_together_and_more.py
@@ -0,0 +1,77 @@
+# Generated by Django 5.2.8 on 2026-01-27 14:55
+
+import django.db.models.deletion
+import django.utils.timezone
+import uuid
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0038_remove_objecttype_linkable_to_zaken'),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='objecttype',
+ unique_together=set(),
+ ),
+ migrations.AlterField(
+ model_name='object',
+ name='object_type',
+ field=models.ForeignKey(help_text='OBJECTTYPE', on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'),
+ ),
+ migrations.AlterField(
+ model_name='objectrecord',
+ name='_object_type',
+ field=models.ForeignKey(help_text='OBJECTTYPE', on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'),
+ ),
+ migrations.AlterField(
+ model_name='objecttype',
+ name='created_at',
+ field=models.DateField(auto_now_add=True, default=django.utils.timezone.now, help_text='Date when the object type was created', verbose_name='created at'),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='objecttype',
+ name='modified_at',
+ field=models.DateField(auto_now=True, default=django.utils.timezone.now, help_text='Last date when the object type was modified', verbose_name='modified at'),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='objecttype',
+ name='name',
+ field=models.CharField(help_text='Name of the object type', max_length=100, verbose_name='name'),
+ ),
+ migrations.AlterField(
+ model_name='objecttype',
+ name='name_plural',
+ field=models.CharField(help_text='Plural name of the object type', max_length=100, verbose_name='name plural'),
+ ),
+ migrations.AlterField(
+ model_name='objecttype',
+ name='uuid',
+ field=models.UUIDField(default=uuid.uuid4, help_text='Unique identifier (UUID4)', unique=True),
+ ),
+ migrations.AlterField(
+ model_name='objecttypeversion',
+ name='created_at',
+ field=models.DateField(auto_now_add=True, default=django.utils.timezone.now, help_text='Date when the version was created', verbose_name='created at'),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='objecttypeversion',
+ name='modified_at',
+ field=models.DateField(auto_now=True, default=django.utils.timezone.now, help_text='Last date when the version was modified', verbose_name='modified at'),
+ preserve_default=False,
+ ),
+ migrations.RemoveField(
+ model_name='objecttype',
+ name='_name',
+ ),
+ migrations.RemoveField(
+ model_name='objecttype',
+ name='service',
+ ),
+ ]
diff --git a/src/objects/core/models.py b/src/objects/core/models.py
index d550e960..168e44a7 100644
--- a/src/objects/core/models.py
+++ b/src/objects/core/models.py
@@ -2,61 +2,47 @@
import datetime
import uuid
-from typing import ClassVar, Iterable
+from typing import ClassVar
from django.contrib.gis.db.models import GeometryField
from django.contrib.postgres.indexes import GinIndex
-from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.translation import gettext_lazy as _
-import requests
-from requests.exceptions import ConnectionError
-from zgw_consumers.models import Service
-
-from objects.utils.client import get_objecttypes_client
-
from .constants import (
DataClassificationChoices,
- ObjectVersionStatus,
+ ObjectTypeVersionStatus,
ReferenceType,
UpdateFrequencyChoices,
)
from .query import ObjectQuerySet, ObjectRecordQuerySet, ObjectTypeQuerySet
-from .utils import check_json_schema, check_objecttype_cached
+from .utils import check_json_schema, check_objecttype
class ObjectType(models.Model):
- service = models.ForeignKey(
- Service, on_delete=models.PROTECT, related_name="object_types"
- )
uuid = models.UUIDField(
- help_text=_("Unique identifier (UUID4) of the OBJECTTYPE in Objecttypes API")
+ help_text=_("Unique identifier (UUID4)"),
+ unique=True,
+ default=uuid.uuid4,
)
- _name = models.CharField(
- max_length=100,
- help_text=_("Cached name of the objecttype retrieved from the Objecttype API"),
- ) # TODO can be removed after objecttype migration
is_imported = models.BooleanField(
_("Is imported"),
default=False,
editable=False,
- ) # TODO temp field to track if object was imported, can be removed after objecttype migration
+ ) # TODO temp field to track if object was imported, can be removed after objecttype migration (in 4.1.0)
name = models.CharField(
_("name"),
max_length=100,
help_text=_("Name of the object type"),
- blank=True, # TODO blank=False after objecttype migration
)
name_plural = models.CharField(
_("name plural"),
max_length=100,
help_text=_("Plural name of the object type"),
- blank=True, # TODO blank=False after objecttype migration
)
description = models.CharField(
_("description"),
@@ -133,16 +119,12 @@ class ObjectType(models.Model):
)
created_at = models.DateField(
_("created at"),
- auto_now_add=False, # TODO auto_now_add=True after migration
- blank=True, # TODO blank=False after migration
- null=True, # TODO null=False after migration
+ auto_now_add=True,
help_text=_("Date when the object type was created"),
)
modified_at = models.DateField(
_("modified at"),
- auto_now=False, # TODO auto_now=True after migration
- blank=True, # TODO blank=False after migration
- null=True, # TODO null=False after migration
+ auto_now=True,
help_text=_("Last date when the object type was modified"),
)
allow_geometry = models.BooleanField(
@@ -158,37 +140,19 @@ class ObjectType(models.Model):
objects = ObjectTypeQuerySet.as_manager()
- class Meta:
- unique_together = ("service", "uuid")
-
def __str__(self):
- return f"{self.service.label}: {self.name or self._name}"
+ return f"{self.name}"
@property
- 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 last_version(self):
+ if not self.versions:
+ return None
- @property
- def versions_url(self):
- return f"{self.url}/versions"
-
- def clean_fields(self, exclude: Iterable[str] | None = None) -> None:
- super().clean_fields(exclude=exclude)
-
- if exclude and "service" in exclude:
- return
+ return self.versions.order_by("-version").first()
- with get_objecttypes_client(self.service) as client:
- try:
- object_type_data = client.get_objecttype(self.uuid)
- except (requests.RequestException, ConnectionError, ValueError) as exc:
- raise ValidationError(f"Objecttype can't be requested: {exc}")
- except requests.exceptions.JSONDecodeError:
- raise ValidationError("Object type version didn't have any data")
-
- if not self._name:
- self._name = object_type_data["name"]
+ @property
+ def ordered_versions(self):
+ return self.versions.order_by("-version")
class ObjectTypeVersion(models.Model):
@@ -200,16 +164,12 @@ class ObjectTypeVersion(models.Model):
)
created_at = models.DateField(
_("created at"),
- auto_now_add=False, # TODO auto_now_add=True after migration
- blank=True, # TODO blank=False after migration
- null=True, # TODO null=False after migration
+ auto_now_add=True,
help_text=_("Date when the version was created"),
)
modified_at = models.DateField(
_("modified at"),
- auto_now=False, # TODO auto_now=True after migration
- blank=True, # TODO blank=False after migration
- null=True, # TODO null=False after migration
+ auto_now=True,
help_text=_("Last date when the version was modified"),
)
published_at = models.DateField(
@@ -224,8 +184,8 @@ class ObjectTypeVersion(models.Model):
status = models.CharField(
_("status"),
max_length=20,
- choices=ObjectVersionStatus.choices,
- default=ObjectVersionStatus.draft,
+ choices=ObjectTypeVersionStatus.choices,
+ default=ObjectTypeVersionStatus.draft,
help_text=_("Status of the object type version"),
)
@@ -249,7 +209,7 @@ def save(self, *args, **kwargs):
ObjectTypeVersion.objects.get(id=self.id).status if self.id else None
)
if (
- self.status == ObjectVersionStatus.published
+ self.status == ObjectTypeVersionStatus.published
and previous_status != self.status
):
self.published_at = datetime.date.today()
@@ -278,7 +238,7 @@ class Object(models.Model):
object_type = models.ForeignKey(
ObjectType,
on_delete=models.PROTECT,
- help_text=_("OBJECTTYPE in Objecttypes API"),
+ help_text=_("OBJECTTYPE"),
)
created_on = models.DateTimeField(auto_now_add=True, help_text=_("Creation date"))
@@ -364,7 +324,7 @@ class ObjectRecord(models.Model):
_object_type = models.ForeignKey(
ObjectType,
on_delete=models.PROTECT,
- help_text=_("OBJECTTYPE in Objecttypes API"),
+ help_text=_("OBJECTTYPE"),
null=False,
blank=False,
db_index=True,
@@ -398,7 +358,7 @@ def clean(self):
super().clean()
if hasattr(self.object, "object_type") and self.version and self.data:
- check_objecttype_cached(self.object.object_type, self.version, self.data)
+ check_objecttype(self.object.object_type, self.version, self.data)
def save(self, *args, **kwargs):
if not self.id and self.object.last_record:
diff --git a/src/objects/core/query.py b/src/objects/core/query.py
index 2a5005e5..ad4e2582 100644
--- a/src/objects/core/query.py
+++ b/src/objects/core/query.py
@@ -1,14 +1,24 @@
from django.db import models
from vng_api_common.utils import get_uuid_from_path
-from zgw_consumers.models import Service
class ObjectTypeQuerySet(models.QuerySet):
def get_by_url(self, url):
- service = Service.get_service(url)
uuid = get_uuid_from_path(url)
- return self.get(service=service, uuid=uuid)
+ return self.get(uuid=uuid)
+
+ def create_from_schema(self, json_schema: dict, **kwargs):
+ object_type_data = {
+ "name": json_schema.get("title", "").title(),
+ "description": json_schema.get("description", ""),
+ }
+ object_type_data.update(kwargs)
+ objecttype = self.create(**object_type_data)
+
+ objecttype.versions.create(json_schema=json_schema)
+
+ return objecttype
class ObjectQuerySet(models.QuerySet):
diff --git a/src/objects/core/tests/factories.py b/src/objects/core/tests/factories.py
index 0dff91a4..0ea5614f 100644
--- a/src/objects/core/tests/factories.py
+++ b/src/objects/core/tests/factories.py
@@ -1,12 +1,10 @@
import random
-import uuid
from datetime import date, timedelta
from django.contrib.gis.geos import Point
import factory
from factory.fuzzy import BaseFuzzyAttribute
-from zgw_consumers.test.factories import ServiceFactory
from objects.core.constants import ReferenceType
@@ -14,11 +12,7 @@
class ObjectTypeFactory(factory.django.DjangoModelFactory[ObjectType]):
- service = factory.SubFactory(ServiceFactory)
- uuid = factory.LazyFunction(uuid.uuid4)
-
name = factory.Faker("word")
- _name = factory.LazyAttribute(lambda x: x.name)
name_plural = factory.LazyAttribute(lambda x: f"{x.name}s")
description = factory.Faker("bs")
@@ -28,13 +22,22 @@ class Meta:
class ObjectTypeVersionFactory(factory.django.DjangoModelFactory[ObjectTypeVersion]):
object_type = factory.SubFactory(ObjectTypeFactory)
- json_schema = {
- "type": "object",
- "title": "Tree",
- "$schema": "http://json-schema.org/draft-07/schema#",
- "required": ["diameter"],
- "properties": {"diameter": {"type": "integer", "description": "size in cm."}},
- }
+ json_schema = factory.Dict(
+ {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": ["diameter"],
+ "properties": {
+ "diameter": {"type": "integer", "description": "size in cm."},
+ "plantDate": {
+ "type": "string",
+ "format": "date",
+ "description": "Date the tree was planted.",
+ },
+ },
+ }
+ )
class Meta:
model = ObjectTypeVersion
diff --git a/src/objects/core/tests/files/objecttypes_empty_database.yaml b/src/objects/core/tests/files/objecttypes_empty_database.yaml
index b969949e..6f079f5c 100644
--- a/src/objects/core/tests/files/objecttypes_empty_database.yaml
+++ b/src/objects/core/tests/files/objecttypes_empty_database.yaml
@@ -3,8 +3,6 @@ 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
index f93e005f..397cd055 100644
--- a/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml
+++ b/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml
@@ -3,8 +3,6 @@ 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
index b969949e..6f079f5c 100644
--- a/src/objects/core/tests/files/objecttypes_idempotent.yaml
+++ b/src/objects/core/tests/files/objecttypes_idempotent.yaml
@@ -3,8 +3,6 @@ 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
index 2a360c8e..e461792c 100644
--- a/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml
+++ b/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml
@@ -3,8 +3,6 @@ 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
deleted file mode 100644
index 8348427c..00000000
--- a/src/objects/core/tests/files/objecttypes_unknown_service.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-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_admin.py b/src/objects/core/tests/test_admin.py
index 70aaec31..3b198aae 100644
--- a/src/objects/core/tests/test_admin.py
+++ b/src/objects/core/tests/test_admin.py
@@ -3,19 +3,16 @@
from django.test import override_settings, tag
from django.urls import reverse
-import requests_mock
from django_webtest import WebTest
from maykin_2fa.test import disable_admin_mfa
-from zgw_consumers.constants import AuthTypes
-from zgw_consumers.test.factories import ServiceFactory
from objects.accounts.tests.factories import UserFactory
from objects.core.tests.factories import (
ObjectFactory,
ObjectRecordFactory,
ObjectTypeFactory,
+ ObjectTypeVersionFactory,
)
-from objects.tests.utils import mock_objecttype_version
@disable_admin_mfa()
@@ -27,16 +24,10 @@ def setUp(self):
@tag("gh-615")
def test_object_changelist_filter_by_objecttype(self):
- service = ServiceFactory.create(
- api_root="http://objecttypes.local/api/v1/",
- auth_type=AuthTypes.api_key,
- header_key="Authorization",
- header_value="Token 5cebbb33ffa725b6ed5e9e98300061218ba98d71",
- )
object_type = ObjectTypeFactory.create(
- service=service, uuid="71a2452a-66c3-4030-b5ec-a06035102e9e"
+ uuid="71a2452a-66c3-4030-b5ec-a06035102e9e"
)
- # Create 100 unused ObjectTypes, which creates 100 Services as well
+ # Create 100 unused ObjectTypes
ObjectTypeFactory.create_batch(100)
object1 = ObjectFactory.create(object_type=object_type)
object2 = ObjectFactory.create()
@@ -94,19 +85,10 @@ def get_num_results(response) -> int:
@tag("gh-677")
def test_add_new_objectrecord(self):
- service = ServiceFactory.create(
- api_root="http://objecttypes.local/api/v1/",
- auth_type=AuthTypes.api_key,
- header_key="Authorization",
- header_value="Token 5cebbb33ffa725b6ed5e9e98300061218ba98d71",
- )
object_type = ObjectTypeFactory.create(
- service=service, uuid="71a2452a-66c3-4030-b5ec-a06035102e9e"
- )
- object_type_url = (
- "http://objecttypes.local/api/v1/"
- "objecttypes/71a2452a-66c3-4030-b5ec-a06035102e9e/versions/1"
+ uuid="71a2452a-66c3-4030-b5ec-a06035102e9e"
)
+ ObjectTypeVersionFactory.create(object_type=object_type)
object = ObjectFactory.create(object_type=object_type)
self.assertEqual(object.records.count(), 0)
@@ -125,10 +107,7 @@ def test_add_new_objectrecord(self):
form["records-0-version"] = 1
form["records-0-start_at"] = "2025-01-01"
- with requests_mock.Mocker() as m:
- m.get(object_type_url, json=mock_objecttype_version(object_type_url))
- response = form.submit()
-
+ form.submit()
self.assertEqual(object.records.count(), 1)
@tag("gh-621")
diff --git a/src/objects/core/tests/test_import_objecttypes.py b/src/objects/core/tests/test_import_objecttypes.py
index faf741a7..49dcdbe7 100644
--- a/src/objects/core/tests/test_import_objecttypes.py
+++ b/src/objects/core/tests/test_import_objecttypes.py
@@ -4,6 +4,7 @@
from django.test import TestCase
import requests_mock
+from freezegun import freeze_time
from zgw_consumers.models import Service
from objects.core.models import ObjectType, ObjectTypeVersion
@@ -14,6 +15,7 @@
)
+@freeze_time("2020-12-01")
class TestImportObjectTypesCommand(TestCase):
def setUp(self):
super().setUp()
@@ -23,7 +25,7 @@ def setUp(self):
self.m.start()
self.service = Service.objects.create(api_root=self.url, slug="objecttypes-api")
- self.m.head(self.url, status_code=200, headers={"api-version": "3.4.0"})
+ self.m.head(self.url, status_code=200, headers={"api-version": "2.2.2"})
def tearDown(self):
self.m.stop()
@@ -40,7 +42,7 @@ def test_api_version_is_required(self):
self._call_command()
def test_api_version_must_be_greater_than_constant(self):
- self.m.head(self.url, status_code=200, headers={"api-version": "2.1.0"})
+ self.m.head(self.url, status_code=200, headers={"api-version": "2.2.1"})
with self.assertRaisesMessage(
CommandError, "API version must be 2.2.2 or higher"
@@ -74,9 +76,7 @@ def test_new_objecttypes_are_created(self):
self.assertEqual(ObjectType.objects.count(), 2)
objecttype = ObjectType.objects.get(uuid=uuid1)
- self.assertEqual(objecttype.is_imported, True)
self.assertEqual(objecttype.name, "Melding")
- self.assertEqual(objecttype._name, "Melding")
self.assertEqual(objecttype.name_plural, "Meldingen")
self.assertEqual(objecttype.description, "")
self.assertEqual(objecttype.data_classification, "intern")
@@ -117,8 +117,8 @@ def test_new_objecttypes_are_created(self):
self.assertEqual(str(version.status), "published")
def test_existing_objecttypes_are_updated(self):
- objecttype1 = ObjectTypeFactory.create(service=self.service)
- objecttype2 = ObjectTypeFactory.create(service=self.service)
+ objecttype1 = ObjectTypeFactory.create()
+ objecttype2 = ObjectTypeFactory.create()
self.m.get(
f"{self.url}objecttypes",
@@ -138,9 +138,7 @@ def test_existing_objecttypes_are_updated(self):
self.assertEqual(ObjectTypeVersion.objects.count(), 4)
objecttype = ObjectType.objects.get(uuid=objecttype1.uuid)
- self.assertEqual(objecttype.is_imported, True)
self.assertEqual(objecttype.name, "Melding")
- self.assertEqual(objecttype._name, "Melding")
version = ObjectTypeVersion.objects.get(object_type=objecttype, version=1)
self.assertEqual(
diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py
index 5588f1d5..28587fe8 100644
--- a/src/objects/core/tests/test_objecttype_config.py
+++ b/src/objects/core/tests/test_objecttype_config.py
@@ -5,8 +5,6 @@
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
@@ -17,44 +15,34 @@
class ObjectTypesConfigurationStepTests(TestCase):
def test_empty_database(self):
- service_1 = ServiceFactory.create(slug="service-1")
- service_2 = ServiceFactory.create(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")
+ 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)
+ self.assertEqual(objecttype_1.name, "Object Type 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)
+ self.assertEqual(objecttype_2.name, "Object Type 2")
def test_existing_objecttype(self):
test_file_path = str(TEST_FILES / "objecttypes_existing_objecttype.yaml")
- service_1: Service = ServiceFactory.create(slug="service-1")
- service_2: Service = ServiceFactory.create(slug="service-2")
-
objecttype_1: ObjectType = ObjectTypeFactory.create(
- service=service_1,
uuid="b427ef84-189d-43aa-9efd-7bb2c459e281",
- _name="Object Type 001",
+ name="Object Type 001",
)
objecttype_2: ObjectType = ObjectTypeFactory.create(
- service=service_2,
uuid="b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2",
- _name="Object Type 002",
+ name="Object Type 002",
)
execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path)
@@ -64,56 +52,26 @@ def test_existing_objecttype(self):
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)
+ self.assertEqual(objecttype_1.name, "Object Type 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)
+ self.assertEqual(objecttype_2.name, "Object Type 002")
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.create(slug="service-1")
-
- objecttype: ObjectType = ObjectTypeFactory.create(
- 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)
+ self.assertEqual(objecttype_3.name, "Object Type 3")
def test_invalid_uuid(self):
test_file_path = str(TEST_FILES / "objecttypes_invalid_uuid.yaml")
- service: Service = ServiceFactory.create(slug="service-1")
-
objecttype: ObjectType = ObjectTypeFactory.create(
- service=service,
uuid="b427ef84-189d-43aa-9efd-7bb2c459e281",
- _name="Object Type 001",
+ name="Object Type 001",
)
with self.assertRaises(ConfigurationRunFailed):
@@ -127,32 +85,26 @@ def test_invalid_uuid(self):
self.assertEqual(str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281")
# Name should not be changed, because the error causes a rollback
- self.assertEqual(objecttype._name, "Object Type 001")
- self.assertEqual(objecttype.service, service)
+ self.assertEqual(objecttype.name, "Object Type 001")
def test_idempotent_step(self):
- service_1 = ServiceFactory.create(slug="service-1")
- service_2 = ServiceFactory.create(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")
+ 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)
+ self.assertEqual(objecttype_1.name, "Object Type 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)
+ self.assertEqual(objecttype_2.name, "Object Type 2")
# Rerun
execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path)
@@ -164,10 +116,8 @@ def test_idempotent_step(self):
# 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)
+ self.assertEqual(objecttype_1.name, "Object Type 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)
+ self.assertEqual(objecttype_2.name, "Object Type 2")
diff --git a/src/objects/core/tests/test_upgrade_check.py b/src/objects/core/tests/test_upgrade_check.py
new file mode 100644
index 00000000..0b87e635
--- /dev/null
+++ b/src/objects/core/tests/test_upgrade_check.py
@@ -0,0 +1,121 @@
+import uuid
+from unittest.mock import patch
+
+from django.core.management import call_command
+from django.core.management.base import SystemCheckError
+from django.test import TestCase, override_settings
+
+from upgrade_check.models import Version
+
+from objects.core.tests.factories import ObjectTypeFactory
+from objects.token.tests.test_migrations import BaseMigrationTest
+
+
+class TestUpgradeCheckBefore40(BaseMigrationTest):
+ app = "core"
+ migrate_from = "0036_objecttype_is_imported"
+ migrate_to = "0037_alter_objecttype_unique_together_and_more"
+
+ def setUp(self):
+ super().setUp()
+
+ self.ObjectType = self.old_app_state.get_model("core", "ObjectType")
+ Service = self.old_app_state.get_model("zgw_consumers", "Service")
+
+ self.service = Service.objects.create()
+
+ # patch get_model in command to return the model with is_imported
+ self.patch = patch(
+ "objects.core.management.commands.check_for_external_objecttypes.Command._get_objecttype",
+ return_value=self.ObjectType,
+ ).start()
+
+ def tearDown(self):
+ self.patch.stop()
+
+ @override_settings(RELEASE="4.0.0")
+ def test_upgrade_from_30_to_40(self):
+ """
+ from 3.0.0 directly to 4.0.0 is not allowed, 3.6.0 is the minimum version
+
+ """
+ Version.objects.create(version="3.0.0", git_sha="test")
+
+ with self.assertRaises(SystemCheckError):
+ call_command("check")
+
+ @override_settings(RELEASE="4.0.0")
+ def test_upgrade_from_36_to_40_with_non_imported_objecttypes(self):
+ """
+ import should fail because non-imported objecttypes exist
+ """
+ Version.objects.create(version="3.6.0", git_sha="test")
+ self.ObjectType.objects.create(
+ is_imported=False, uuid=uuid.uuid4(), service=self.service
+ )
+
+ with self.assertRaises(SystemCheckError):
+ call_command("check")
+
+ @override_settings(RELEASE="4.0.0")
+ def test_upgrade_from_36_to_40_with_all_imported(self):
+ """
+ import should succeed because all objecttypes are imported
+ """
+ Version.objects.create(version="3.6.0", git_sha="test")
+ self.ObjectType.objects.create(
+ is_imported=True, uuid=uuid.uuid4(), service=self.service
+ )
+
+ call_command("check")
+
+ @override_settings(RELEASE="4.1.0")
+ def test_upgrade_from_37_to_41_with_non_imported_objecttypes(self):
+ """
+ import should fail because non-imported objecttypes exist
+ """
+ Version.objects.create(version="3.7.0", git_sha="test")
+ self.ObjectType.objects.create(
+ is_imported=False, uuid=uuid.uuid4(), service=self.service
+ )
+
+ with self.assertRaises(SystemCheckError):
+ call_command("check")
+
+ @override_settings(RELEASE="4.1.0")
+ def test_upgrade_from_37_to_41_with_all_imported(self):
+ """
+ import should succeed because all objecttypes are imported
+ """
+ Version.objects.create(version="3.7.0", git_sha="test")
+ self.ObjectType.objects.create(
+ is_imported=True, uuid=uuid.uuid4(), service=self.service
+ )
+
+ call_command("check")
+
+
+class TestUpgradeCheckAfter40(TestCase):
+ @override_settings(RELEASE="4.0.0")
+ def test_upgrade_from_36_to_40_with_non_imported(self):
+ Version.objects.create(version="3.6.0", git_sha="test")
+ ObjectTypeFactory.create(is_imported=False)
+
+ with self.assertRaises(SystemCheckError):
+ call_command("check")
+
+ @override_settings(RELEASE="4.0.0")
+ def test_upgrade_from_36_to_40_with_imported(self):
+ Version.objects.create(version="3.6.0", git_sha="test")
+ ObjectTypeFactory.create(is_imported=True)
+
+ call_command("check")
+
+ @override_settings(RELEASE="4.1.0")
+ def test_upgrade_from_40_to_41_with_all_imported(self):
+ """
+ import should succeed because version is already 4.0.0
+ """
+ Version.objects.create(version="4.0.0", git_sha="test")
+ ObjectTypeFactory.create()
+ call_command("check")
diff --git a/src/objects/core/utils.py b/src/objects/core/utils.py
index 3879d711..0b6965a5 100644
--- a/src/objects/core/utils.py
+++ b/src/objects/core/utils.py
@@ -1,58 +1,27 @@
-from django.conf import settings
from django.core.exceptions import ValidationError
import jsonschema
-import requests
from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for
from objects.core import models
-from objects.utils.cache import cache
-from objects.utils.client import get_objecttypes_client
-def check_objecttype_cached(
+def check_objecttype(
object_type: "models.ObjectType", version: int, data: dict
) -> None:
- @cache(
- f"objecttypen-{object_type.uuid}:versions-{version}",
- timeout=settings.OBJECTTYPE_VERSION_CACHE_TIMEOUT,
- )
- def get_objecttype_version_response():
- with get_objecttypes_client(object_type.service) as client:
- try:
- return client.get_objecttype_version(object_type.uuid, version)
- except (requests.RequestException, requests.JSONDecodeError):
- raise ValidationError(
- "Object type version can not be retrieved.",
- code="invalid",
- )
-
try:
- version_data = get_objecttype_version_response()
- jsonschema.validate(data, version_data["jsonSchema"])
- except KeyError:
+ version_data = object_type.versions.get(version=version)
+ jsonschema.validate(data, version_data.json_schema)
+ except models.ObjectTypeVersion.DoesNotExist:
raise ValidationError(
- f"{object_type.versions_url} does not appear to be a valid objecttype.",
+ f"{object_type} version: {version} does not appear to exist.",
code="invalid_key",
)
except jsonschema.exceptions.ValidationError as exc:
raise ValidationError(exc.args[0], code="invalid_jsonschema")
-def can_connect_to_objecttypes() -> bool:
- """
- check that all services of objecttypes are available
- """
- from zgw_consumers.models import Service
-
- for service in Service.objects.filter(object_types__isnull=False).distinct():
- with get_objecttypes_client(service) as client:
- if not client.can_connect:
- return False
- return True
-
-
def check_json_schema(json_schema: dict):
schema_validator = validator_for(json_schema)
try:
diff --git a/src/objects/core/widgets.py b/src/objects/core/widgets.py
new file mode 100644
index 00000000..400048d0
--- /dev/null
+++ b/src/objects/core/widgets.py
@@ -0,0 +1,18 @@
+import json
+
+from jsonsuit.widgets import JSONSuit as _JSONSuit
+
+
+class JSONSuit(_JSONSuit):
+ initial = dict()
+
+ def render(self, name, value, attrs=None, renderer=None):
+ if attrs is None:
+ attrs = {}
+ try:
+ json.loads(value)
+ except ValueError:
+ # The supplied value is not valid JSON, use the original value as
+ # a fallback
+ value = json.dumps(self.initial)
+ return super().render(name, value, attrs)
diff --git a/src/objects/fixtures/demodata.json b/src/objects/fixtures/demodata.json
index 4ebd82c8..63d5434e 100644
--- a/src/objects/fixtures/demodata.json
+++ b/src/objects/fixtures/demodata.json
@@ -900,33 +900,1711 @@
"model": "core.objecttype",
"pk": 1,
"fields": {
- "service": [
- "objecttypen-api"
- ],
"uuid": "feeaa795-d212-4fa2-bb38-2c34996e5702",
- "_name": "Boom"
+ "name": "Boom",
+ "name_plural": "Bomen",
+ "description": "",
+ "data_classification": "open",
+ "maintainer_organization": "Gemeente Delft",
+ "maintainer_department": "",
+ "contact_person": "Jan Eik",
+ "contact_email": "",
+ "source": "",
+ "update_frequency": "unknown",
+ "provider_organization": "",
+ "documentation_url": "",
+ "labels": {},
+ "created_at": "2020-12-01",
+ "modified_at": "2020-12-01"
}
},
{
"model": "core.objecttype",
"pk": 2,
"fields": {
- "service": [
- "objecttypen-api"
- ],
"uuid": "3a82fb7f-fc9b-4104-9804-993f639d6d0d",
- "_name": "Straatverlichting"
+ "name": "Straatverlichting",
+ "name_plural": "Straatverlichting",
+ "description": "",
+ "data_classification": "open",
+ "maintainer_organization": "Maykin Media",
+ "maintainer_department": "",
+ "contact_person": "Desiree Lumen",
+ "contact_email": "",
+ "source": "",
+ "update_frequency": "unknown",
+ "provider_organization": "",
+ "documentation_url": "",
+ "labels": {},
+ "created_at": "2020-12-01",
+ "modified_at": "2020-12-01"
}
},
{
"model": "core.objecttype",
"pk": 3,
"fields": {
- "service": [
- "objecttypen-api"
- ],
"uuid": "ca754b52-3f37-4c49-837c-130e8149e337",
- "_name": "Melding"
+ "name": "Melding",
+ "name_plural": "Meldingen",
+ "description": "",
+ "data_classification": "intern",
+ "maintainer_organization": "Dimpact",
+ "maintainer_department": "",
+ "contact_person": "Ad Alarm",
+ "contact_email": "",
+ "source": "",
+ "update_frequency": "unknown",
+ "provider_organization": "",
+ "documentation_url": "",
+ "labels": {},
+ "created_at": "2020-12-01",
+ "modified_at": "2020-12-01"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 1,
+ "fields": {
+ "object_type": 1,
+ "version": 1,
+ "created_at": "2020-11-14",
+ "modified_at": "2020-11-16",
+ "published_at": "2020-11-16",
+ "json_schema": {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": [
+ "diameter"
+ ],
+ "properties": {
+ "diameter": {
+ "type": "integer",
+ "description": "Size in cm."
+ },
+ "plantDate": {
+ "type": "string",
+ "format": "date",
+ "description": "Date the tree was planted."
+ }
+ }
+ },
+ "status": "published"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 2,
+ "fields": {
+ "object_type": 1,
+ "version": 2,
+ "created_at": "2020-12-01",
+ "modified_at": "2020-12-01",
+ "published_at": "2020-12-01",
+ "json_schema": {
+ "$id": "https://objecttypes.vng.cloud/schema.json",
+ "type": "object",
+ "title": "Boom",
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "default": {},
+ "examples": [],
+ "required": [],
+ "properties": {
+ "type": {
+ "$id": "#/properties/type",
+ "enum": [
+ "Haag",
+ "Fruitteelt",
+ "Buitenfitness",
+ "Betonverharding",
+ "Dilatatievoegovergang",
+ "Beheervak - brug",
+ "Dienstgang",
+ "Vacumpompstation",
+ "Putschacht",
+ "Beregeningspomp",
+ "Behendigheidstoestel",
+ "Laadperron",
+ "Schroefpomp",
+ "Erfafscheidingsput",
+ "Waaierdeur",
+ "Kast",
+ "Trottoirband",
+ "Hoogspanningskabel",
+ "Dubbelkerende afsluiter",
+ "Spijlenhek",
+ "Met instroomvoorziening",
+ "Palen met draad",
+ "Doelwand",
+ "Faunatunnel groot",
+ "Hondenpoepbak",
+ "Waterloop",
+ "Aansluitleiding",
+ "Werk in uitvoering-bord",
+ "Palen met planken",
+ "Schutsluis",
+ "Muur met hek",
+ "Natuurlijke elementen",
+ "Informatief verkeersbord",
+ "Speciale bank",
+ "Sinusvormige verkeersdrempel",
+ "Verzamelput",
+ "Gegraven tunnel",
+ "Stobbe",
+ "Boomteelt",
+ "Verkeersplateau",
+ "Terreindeel",
+ "Heesters",
+ "Afsluiter rioolleiding",
+ "Sportcombinatietoestel",
+ "Overgangsconstructie voor integraal kunstwerk",
+ "Klimtoestel",
+ "Volleybalset",
+ "Geleidebarrier",
+ "Boomrooster",
+ "Familiegraf",
+ "Sandwichconstructie",
+ "Spoor",
+ "Enkele bak",
+ "Schommel",
+ "Eenvoudige picknicktafel",
+ "Overdekte bank",
+ "Laagspanningskabel",
+ "Trottoirkolk",
+ "Hefdeur",
+ "Gecombineerde straat-trottoirkolk",
+ "Spiraal gegolfd stalen duikerbuizen",
+ "Toldeur",
+ "Beheervak - tunnel",
+ "Planken beschoeiing",
+ "Lozingspunt",
+ "Faunatunnel klein",
+ "Atletiekbaan",
+ "Basketbalbord",
+ "Boom niet vrij uitgroeiend",
+ "Drinkwatermeter",
+ "Struikrozen",
+ "Boomkratten",
+ "Tunnelobject",
+ "Middenspanningskabel",
+ "Infiltratiekolk",
+ "Veerooster",
+ "Nestkast voor zoogdieren",
+ "Bebakeningselement",
+ "Blauwe spiegel",
+ "Fruitboom",
+ "Heide",
+ "Stedenbandbord",
+ "Drukrioleringspomp",
+ "Solitair gras",
+ "Thematische picknicktafel",
+ "Rijrichtingbord",
+ "Drijvende mat",
+ "Veldafscheiding",
+ "Verlichtingsobject",
+ "Picknicktafel zeshoekig",
+ "Sensor",
+ "Septictank",
+ "Zuigerpomp",
+ "Planten",
+ "Bedekt",
+ "Eigen bouw",
+ "Grote sproeier",
+ "Ontstoppingsput",
+ "Parcours",
+ "Basket",
+ "Technische gang",
+ "Houten beschoeiing",
+ "Afsluiter beregeningsleiding",
+ "Centrifugaalpomp",
+ "Damtafel",
+ "Fietsbeugel",
+ "Onderbord",
+ "Sluiswachterskantoor",
+ "Toegangspoort",
+ "Standaard reflector",
+ "Fitnesstoestel",
+ "Winterverblijf amfibien",
+ "Avontuurlijke speelplek",
+ "Hoog raster",
+ "Reinigende put",
+ "Opruimplicht hondenpoep",
+ "Beheervak - gemaal",
+ "Signaleringsband",
+ "Onverhard",
+ "Balspelterrein",
+ "Kwelder",
+ "Wervelput",
+ "Hoekblok",
+ "Lamellenvoegovergang",
+ "Stootband",
+ "Gemaal in natte opstelling",
+ "Waterinrichtingsobject",
+ "Lijnmarkering",
+ "Zinloos geweld tegel",
+ "Keermuur met bank",
+ "Meubilair",
+ "Bouwspeelplaats",
+ "JongerenOntmoetingsPlek",
+ "Greppel",
+ "Waterspeeltoestel",
+ "Lozingsput",
+ "Vrijverval rioolleiding",
+ "Overbruggingsobject",
+ "Beheervak - verkeersregelinstallatie",
+ "Duikelrek fitness",
+ "Vingervoegovergang",
+ "Grasland agrarisch",
+ "Scheiding",
+ "Rijstrook",
+ "Puntmarkering",
+ "Voorrangsbord",
+ "Puntdeur",
+ "Steilwand",
+ "Dubbele bak",
+ "Skateboardbaan",
+ "Externe overstortconstructie",
+ "Schampkant",
+ "Vluchtgang",
+ "Matten",
+ "Jeu de Boules",
+ "Dwarsgang",
+ "Natte pompkelder",
+ "Basketbalpaal",
+ "Meervoudige voegovergang",
+ "Onderwaterbeschoeiing",
+ "Schaaktafel",
+ "Tafeltennistafel",
+ "Persluchtpomp",
+ "Graft",
+ "Boombank",
+ "Mechanische transportleiding",
+ "Oorlogsgraf",
+ "Boom vrij uitgroeiend",
+ "Oeverzwaluwenwand",
+ "Leidingelement",
+ "V-polder",
+ "Wildspiegel op voet",
+ "Drijvende bak",
+ "Overnamepunt",
+ "Straatkolk",
+ "Kabelbaan",
+ "Zandvlakte",
+ "Zandspeeltoestel",
+ "Kunststofverharding",
+ "Leiboom",
+ "Duikelrek",
+ "Hondenrooster",
+ "Midgetgolfbaan",
+ "Bebouwde kombord",
+ "Doorspuitput",
+ "Knotboom",
+ "Halfverharding",
+ "Zinkerput",
+ "Schuine trottoirband",
+ "Turntoestel",
+ "Educatietoestel",
+ "Verbodsbord",
+ "Verholen goot",
+ "Buishek",
+ "Huisvuilcontainerplaats",
+ "Geleidehek",
+ "Schotbalk",
+ "Pompput",
+ "Roldeur",
+ "Klimplant",
+ "Skatevoorziening",
+ "Uitneembare brug",
+ "Put",
+ "Kruisingsput",
+ "Dynamische snelheidsindicator",
+ "Glijbaan",
+ "Fietscrossbaan",
+ "Vlakmarkering",
+ "Waterspeelplaats",
+ "Hek Verre Veld",
+ "Keersluis",
+ "Piramideblok",
+ "Vegetatieobject",
+ "Fietsabri",
+ "Trapeziumvormige verkeersdrempel",
+ "Bijzondere putconstructie",
+ "Gras- en kruidachtigen",
+ "GVC beschoeiing",
+ "GRIP",
+ "Rioolput met geleiding",
+ "Buispaal",
+ "Mattenvoegovergang",
+ "Bomengranulaat",
+ "Fietsenrek",
+ "Beweegbare brug",
+ "Bord",
+ "Installatie",
+ "Flexibele voegovergang",
+ "IJsvogelwand",
+ "Kleine sproeier",
+ "Wanden dak methode tunnel",
+ "Groenobject",
+ "IBA",
+ "Mast",
+ "Onbedekt",
+ "Voorwaarschuwingsbord",
+ "Rimob",
+ "Backstop",
+ "Leiplant",
+ "Wand",
+ "Standaard",
+ "Beluchtingsrooster",
+ "Buffer",
+ "Elementenverharding",
+ "Toegangshekinstallatie",
+ "Rietland",
+ "Struiken",
+ "Interne overstortconstructie",
+ "Uitlaatpunt",
+ "Beheervak - sluis",
+ "Begroeid",
+ "Overstortput",
+ "Verkeerstegel",
+ "Afsluiter gasleiding",
+ "Stuwput",
+ "Schampblok",
+ "Grondwatermeter",
+ "Doorspoelput",
+ "Staafmathek",
+ "Poef",
+ "Bouwwerk",
+ "Combinatietoestel",
+ "Visoverwinteringsplek",
+ "Skateterrein",
+ "Voetbalveld",
+ "Roostergoot",
+ "Duiventil",
+ "Atletiekvoorziening",
+ "Perceelaansluitpunt",
+ "Zandspeelplaats",
+ "Boomkorf",
+ "Gaashek",
+ "Bouwland",
+ "Boom",
+ "Tennisbaan",
+ "Flespaal",
+ "Infiltratiebassin",
+ "Bomenzand",
+ "Reddingsboei",
+ "Asbaktegel",
+ "Kunstwerk",
+ "Eenzijdig kerende afsluiter",
+ "Gemengd bos",
+ "Winterverblijf algemene amfibien en kamsalamder",
+ "Net",
+ "Vrijverval transportleiding",
+ "Weginrichtingsobject",
+ "FunctioneelGebied",
+ "Watervogels",
+ "Basaltblokken",
+ "Nooduitlaat",
+ "Opsluitband",
+ "Straatbank",
+ "Gemaal in droge opstelling",
+ "DRIP",
+ "Duin",
+ "Rijbaan",
+ "Ijsbaan",
+ "Reddingshaak",
+ "Biofilter",
+ "Doel",
+ "Klimklauterparcours",
+ "Brugwachterskantoor",
+ "Grondwatermeetpunt",
+ "Wiptoestel",
+ "Bodembedekkers",
+ "Laag raster",
+ "Geleiderail",
+ "Boombumper",
+ "Solitaire heester",
+ "Gierzwaluwtil",
+ "Bomengrond",
+ "Moeras",
+ "Bushalteband",
+ "Water over weg",
+ "Slik",
+ "Adoptiebak",
+ "Verdekte put",
+ "Afsluiter waterleiding",
+ "Fietsklem",
+ "Vijzelgemaal",
+ "Afgezonken tunnel",
+ "Boomjuk",
+ "Tennisbaanafrastering",
+ "Berging",
+ "Leiding",
+ "Gekandelaberde boom",
+ "Boostergemaal",
+ "Gazonband",
+ "Fietssleuf",
+ "Trimbaan",
+ "Nestkast voor vogels",
+ "Busvriendelijke verkeersdrempel",
+ "Draaitoestel",
+ "Paal",
+ "Velddrain",
+ "Verbindingsstuk",
+ "Binnenterrein",
+ "Microtunneling,",
+ "Perkoen",
+ "Fietssteun",
+ "Rij-ijzer",
+ "Motorfietsdrempel",
+ "Zeecontainer",
+ "Knikkertegel",
+ "Wildrooster",
+ "Schijnvoeg",
+ "Schroefcentrifugaalpomp",
+ "Paaltje (Amsterdammertje)",
+ "Winterverblijf rugstreeppad",
+ "Bromfietsdrempel",
+ "Korfbalpaal",
+ "Opbouwputtunnel",
+ "Water over water",
+ "Zinkvoeg",
+ "Weg",
+ "Waterobject",
+ "Fietsenkluis",
+ "Rood wit paaltje (toegangsbeperking)",
+ "Vaste brug",
+ "Houtwal",
+ "Geleideband",
+ "Monsternamepunt",
+ "Wegobject",
+ "Schoolspeelplaats",
+ "Groot wild",
+ "Overkapping",
+ "Loofbos",
+ "Thematische bank",
+ "Handbediende slagboom",
+ "Vijzelpomp",
+ "Riooleindgemaal",
+ "Zitmuur",
+ "Keermuur met plantenbak",
+ "Verwijsbord",
+ "Mechanische rioolleiding",
+ "Waarschuwingsbord",
+ "Vormboom",
+ "Geboorde tunnel",
+ "Speeltuin",
+ "Sociaal spel",
+ "Speelkuil",
+ "Watervlakte",
+ "Draaiende reflector",
+ "Naaldbos",
+ "Bak",
+ "Omleidingsbord",
+ "Spuisluis",
+ "Vluchtdeur",
+ "Boombunker",
+ "Speelplek",
+ "Verborgen voegovergang",
+ "Grasland overig",
+ "Asfaltverharding",
+ "Voegovergang met of zonder balken en randprofielen met afdichtingrubbers",
+ "Elektrische slagboom",
+ "Bosplantsoen",
+ "Verzameldrain",
+ "Haltetegel",
+ "Inspectieput",
+ "Boomkrans",
+ "Parkeerbord",
+ "Winterverblijf slangen",
+ "Sierhek",
+ "Snelheidsbord",
+ "Geallieerdengraf",
+ "Fietstrommel"
+ ],
+ "type": "string",
+ "title": "Type",
+ "examples": [
+ "Haag"
+ ],
+ "description": "Typering van het beheerobject."
+ },
+ "kiemjaar": {
+ "$id": "#/properties/kiemjaar",
+ "type": "integer",
+ "title": "Kiemjaar",
+ "examples": [],
+ "description": "Kiemjaar van de boom.\nEenheid: Jaartal"
+ },
+ "leeftijd": {
+ "$id": "#/properties/leeftijd",
+ "type": "integer",
+ "title": "Leeftijd",
+ "examples": [],
+ "description": "Leeftijd van het beheerobject in jaren.\nEenheid: Aantal"
+ },
+ "typeplus": {
+ "$id": "#/properties/typeplus",
+ "enum": [
+ "Grind Rail",
+ "Overhead Ladder",
+ "Draaiende stoeltjes (type A)",
+ "Klimladder",
+ "Steeple-chase waterbak",
+ "Dubbele draaibrug",
+ "Neststeen voor gierzwaluw",
+ "Drievoudig duikelrek",
+ "Spoelleiding",
+ "Waterrad",
+ "Natuursteen",
+ "Hoogspringbak",
+ "Twist en Wobble",
+ "Squat en Shoulder Press en Lat Pull Down",
+ "Vacumpompstation",
+ "Minidoel",
+ "Roll-In Ramp",
+ "Ramp",
+ "Zandbak",
+ "Mini Box",
+ "Step block",
+ "Combinatie - Peuter",
+ "Type 4 - Meer assen - 1 richting",
+ "Bootcamp Box en Gear",
+ "Evenwichtsplateau op veren",
+ "Pull up bars",
+ "Sierbestrating",
+ "Driekhoeksmarkering",
+ "Infiltratiegreppel",
+ "Balance beam",
+ "Speelhuis",
+ "Nestkast bosuil",
+ "Nestkast koolmees",
+ "Type 2 - Rotatie op meerdere assen",
+ "Triple Bars",
+ "Betonelement",
+ "Rietvegetatie",
+ "Zandtransporttoestel",
+ "Suspension trainer",
+ "Cross Trainer",
+ "Fijne sierheester",
+ "Botanische rozen",
+ "Street Workout, Parkour",
+ "Parkeervak",
+ "Ruw gras",
+ "Hellingklimmer",
+ "Log hop",
+ "Nestkast boomklever",
+ "Kliedertafel",
+ "Chest Press en Horizontal Row",
+ "Kunstgras",
+ "Verspringbak",
+ "Heesters",
+ "Split Quarter Pipe",
+ "Ophaalbrug",
+ "Hurdles",
+ "Loopbrug",
+ "Opdrukken",
+ "Tafeltennistafel rond",
+ "Lo-Box",
+ "Hobbelbrug",
+ "Klaphek",
+ "Pontje",
+ "Klimpaal",
+ "Speelschip",
+ "Speeltrein",
+ "Bodembedekkende heesters",
+ "Overgangsstuk",
+ "Perceelaansluitleiding",
+ "Rek- en strekbrug",
+ "Onderbroken brede streep",
+ "Weide",
+ "Body Flexer",
+ "Ven",
+ "Gebogen evenwichtsbalk",
+ "Ballentrechter",
+ "Behendigheidsparcours",
+ "Midi Ramp",
+ "Vertikale ladder",
+ "Leunhek",
+ "Wateremmer",
+ "Nestkast grote bonte specht",
+ "Bandenloop",
+ "Gewone normale vaste brug",
+ "Kogelstotenbak",
+ "Cultuurrozen",
+ "Start Box",
+ "Multi net",
+ "Zandspeelhuis",
+ "Loopton",
+ "Upperbody Trainer - Free Runner - Body Flexer",
+ "Grasveld",
+ "Angle Box",
+ "Struikvormers",
+ "Type 3B - Meerpunts - Meerdere richtingen",
+ "Toroveld",
+ "Verbeterde overstortput",
+ "Bergingsleiding",
+ "Wobble en Step",
+ "Y-stuk",
+ "Core twist",
+ "Vast",
+ "Dubbele taludkabelbaan",
+ "Wijngaarden",
+ "Vollegrondsteelt",
+ "Zonemarkering",
+ "Hangelduo",
+ "Instructiebord",
+ "Big Wedge",
+ "Drijvende brug",
+ "Getalmarkering",
+ "Duo ab - bench en ladder",
+ "Free Runner - Cross Trainer - Power bike",
+ "Doorgetrokken en dubbele smalle strepen",
+ "Nestkast pimpelmees",
+ "Liggerbrug",
+ "Step",
+ "Enterladder",
+ "Tafeltennistafel vierkant",
+ "Nestkast boomkruiper",
+ "Zesvoudig duikelrek",
+ "Klauterparcours",
+ "Doorgetrokken brede streep",
+ "Square Pull Up Station",
+ "Planten",
+ "Enkel duikelrek",
+ "Stapstenen",
+ "Nestkast zwarte roodstaart",
+ "Tafelvoetbaltafel",
+ "Taludglijbaan - type 2",
+ "Buik- en rugsteun",
+ "Zand",
+ "Schuifhek",
+ "Jump pod",
+ "Mobile bar",
+ "Fitnesstoestel",
+ "Hefbrug",
+ "Persleiding",
+ "Parkeerplaats",
+ "Infiltratieriool",
+ "Beek",
+ "Box Ramp",
+ "Over Under",
+ "Upright Row en Press Down",
+ "Natte heide",
+ "Afgekruist vlak",
+ "Nestkast roodborst",
+ "Zandkraan",
+ "Hexagon Pull Up Station",
+ "Verkeersdruppel",
+ "Bootcamp & Circuit Training",
+ "Zandspeeltafel",
+ "Klimrek",
+ "Type 2B - Enkelpunts - Meerdere richtingen",
+ "Ruimtenet",
+ "Frame",
+ "Brug",
+ "Monkey bar",
+ "Trapschot",
+ "Stappalen",
+ "Type 5 - Zweefwip",
+ "Verplaatsbaar",
+ "Vleermuiskast rosse vleermuis",
+ "Watertappunt",
+ "Wobble en Swing en Step en Twist",
+ "Zigzag-markering",
+ "Kunststof vloer",
+ "Rolverbinding",
+ "Dubbelstaafmathek",
+ "Griend en hakhout",
+ "Verloopstuk",
+ "Zwarte grond",
+ "Tractorband",
+ "Klapbrug",
+ "Mini Ramp",
+ "Jurytrap",
+ "Handrail Box",
+ "Kooi",
+ "Push up bars",
+ "Pijlmarkering",
+ "Kanaal",
+ "Bloemrijk gras",
+ "Coping",
+ "Kruipbuis",
+ "Gecombineerde glijbaan - type 1",
+ "Ster klim-duikelrek combinatie",
+ "Double Chest Press",
+ "Speelauto",
+ "Grondwaterpomp",
+ "Fietsparkeervak",
+ "Spine",
+ "Type 3 - Rotatie om 1 punt",
+ "Pannaveld",
+ "Twist en Swing",
+ "Nestkast Marter",
+ "Corner Ramp",
+ "Netbrug oversteek",
+ "Pants Driveway",
+ "Verstopschotten",
+ "Zandverstuiving",
+ "Curved Grind Rail",
+ "Dip Bench",
+ "Hangtouwen",
+ "Vakwerkbrug",
+ "Vijver",
+ "Beachvolleybalveld",
+ "Nestkast gierzwaluw",
+ "Decline Bench",
+ "Duo pull up bar en ladder",
+ "Dakboom",
+ "Enterrek",
+ "Betonstraatstenen",
+ "Molens op spoor met voet of hand aangedreven (type D)",
+ "Gaybrapad",
+ "Nestkast steenuil",
+ "Triple Ramp Grinder",
+ "Vleermuiskast gewone dwergvleermuis",
+ "Wall with Net",
+ "Klimgordijn",
+ "Sit up bench - Power Bike",
+ "Grove sierheester",
+ "Open duinvegetatie",
+ "Externe overstortput",
+ "Laagstam boomgaarden",
+ "Crank",
+ "Droge heide",
+ "Hoogstam",
+ "Wiebelbrug",
+ "Evenwichtsbalk",
+ "Dichte deklagen",
+ "Plasberm",
+ "Stammentrap",
+ "Voetbaldoelnet",
+ "Thematische basketbalpaal",
+ "Klimmuur/ladder combi",
+ "Trekvaste koppeling",
+ "Klassieke draaimolen met meedraaiende vloer (type B)",
+ "Hinkelbaan",
+ "Quad Box",
+ "Dubbele basculebrug",
+ "Verkeersbord",
+ "Hemelwaterriool",
+ "Side Panel",
+ "Turnbrug",
+ "Lijnvormige haag",
+ "Fietssymbool",
+ "Fun box",
+ "Upperbody Trainer",
+ "Bochtstuk",
+ "Enkelvoudige kabelbaan",
+ "Fly box",
+ "Bench",
+ "Platform",
+ "Pirouette",
+ "Suspension Trainer, Parallel Bars & Magnetic Bells Link",
+ "Vogelvide",
+ "Los",
+ "Jump Box",
+ "Speelpaneel",
+ "Speelplatform",
+ "Basketbalterrein",
+ "Valdempend gras",
+ "Rolbrug",
+ "Volleybalveld",
+ "Discuskooi",
+ "Flensverbinding",
+ "Double Turbo Challenge",
+ "Gras- en kruidachtigen",
+ "Wobble en Swing",
+ "Ollie Jump",
+ "Parallel bar",
+ "Type 2A - Enkelpunts - 1 richting",
+ "Combi Step",
+ "Rondobollen",
+ "Cross Training, Street Workout 130m_",
+ "Balanceernet",
+ "Heesterrozen",
+ "Poel",
+ "Metaal",
+ "Praatpaal",
+ "Bomen en struikvormers",
+ "Rodeo stier",
+ "Vrijstaande glijbaan - type 2",
+ "Fitness Bike",
+ "Verdrijfstrepen",
+ "Ballenpaal",
+ "Basket en doel",
+ "Meerdelig duikelrek",
+ "Hoogstam boomgaarden",
+ "Small Wedge",
+ "Natuurlijke oeverzwaluwwand",
+ "Rivier",
+ "Chill schijf",
+ "Telefoonpaal",
+ "Thematisch evenwichtsplateau op veren",
+ "Bodembedekkende rozen",
+ "Crosstrainer",
+ "Waterwip",
+ "Bokspringpaal",
+ "Flex Wheel - Body Flexer",
+ "Magnetic bells, suspension trainer en multi net link",
+ "Blokboom",
+ "Workout combination",
+ "Boomstam",
+ "Nestkast spreeuw",
+ "Hand Bike",
+ "Stretch Bar",
+ "Circuit Training",
+ "Steunbrug",
+ "Magnetic bells",
+ "Nestkast Eekhoorn",
+ "Tuibrug",
+ "Sit up bench",
+ "Trapoefenwand",
+ "Plas",
+ "Loopvlonder",
+ "Open grond",
+ "Lasverbinding",
+ "Drievoudig duikelrek gebogen",
+ "Enkelvoudige taludkabelbaan",
+ "Boogbrug",
+ "Cross Training, Circuit Training, Bootcamp, Street Workout 256m_",
+ "Gesloten duinvegetatie",
+ "Type 1 - Wip - 1 richting",
+ "Vaste planten",
+ "Bedrijfsaansluitleiding",
+ "Ruigte",
+ "Twist en Step",
+ "Grind Bench",
+ "Type 6 - Schommelwip met enkelvoudige hoge as",
+ "Geknipte boom",
+ "Klimnet met duikelrekken",
+ "Enkelstaafmathek",
+ "Nestkast bonte vliegenvanger",
+ "Kolkaansluitleiding",
+ "Gazon",
+ "Pompunit",
+ "Flat Bank",
+ "Drukleiding",
+ "Overige markering",
+ "Klimwand",
+ "T-stuk",
+ "Vouwhek",
+ "Double Overhead Ladder",
+ "Ongewapend verdeuveld beton",
+ "Bolboom",
+ "Zandgraver",
+ "Pleinplakker",
+ "Grind Box",
+ "Draaihek",
+ "Transportrioolleiding",
+ "Hout",
+ "Trainingsdoeltje",
+ "Pull up bars, parallel bars & multi net link",
+ "Voethek",
+ "Kunstmatige oeverzwaluwwand",
+ "Taludglijbaan - type 1",
+ "Roll-off Ramp",
+ "Glas",
+ "Draaiende evenwichtsbalk",
+ "Schudzeef",
+ "Strip",
+ "Boomvormers",
+ "Flat Bank with Platform",
+ "Braakliggend",
+ "Bron",
+ "Voetbaldoel",
+ "Ongewapend nietverdeuveld beton",
+ "Haven",
+ "Tuinbouwgrond",
+ "Supernova",
+ "Puzzelbord",
+ "Zoab en open deklagen",
+ "Ruig gras",
+ "Zandstransportband",
+ "Archimedesspiraal",
+ "Container",
+ "Zinker",
+ "Net",
+ "Type 4 - Contactschommel",
+ "Glijverbinding",
+ "Combinatie van een smalle doorgetrokken en een smalle onderbroken streep",
+ "High rotator",
+ "Horden",
+ "Doel P-model",
+ "Schraalgrasland",
+ "Ingemetselde nestkast",
+ "Turnparcours",
+ "JOP",
+ "Plaatbrug",
+ "Flex Wheel",
+ "Bollenteelt",
+ "Chin up",
+ "Ringenrek met balanceertouw",
+ "Pendelwaag",
+ "Hang- en zweefmolens (type C)",
+ "Street Spine",
+ "Balansvorm",
+ "Straatbaksteen",
+ "Hellende enterladder",
+ "Cladding",
+ "Enkelvoudige platformkabelbaan",
+ "Evenwichtsparcours",
+ "Tegels",
+ "Vacumleiding",
+ "Bodembedekkende vaste planten",
+ "Ollie Hurdle",
+ "Volcano",
+ "Bodembedekkers",
+ "Touwbalans",
+ "Speeltafel",
+ "Touwbrug",
+ "Talud verkeersdrempel",
+ "Touwduikelrek",
+ "Gracht",
+ "Combi",
+ "Gecombineerde glijbaan - type 2",
+ "Quarterpipe",
+ "Lijmverbinding",
+ "Zee",
+ "Sloot",
+ "Tuinachtige grond",
+ "Vuilwaterriool",
+ "Kogelslingerkooi",
+ "Half Pipe",
+ "Draaibrug",
+ "Woordmarkering",
+ "Dubbele platformkabelbaan",
+ "Nestkast winterkoning",
+ "Wiebelplaat",
+ "Akkerbouw",
+ "Frame klimtoestel",
+ "Body Flexer - Upperbody Trainer",
+ "Looptouw",
+ "Helmgras",
+ "Hink-stapspringbak",
+ "Speelboot",
+ "Springkussen",
+ "Geschoren boom",
+ "Stuwrioolleiding",
+ "Nestkast torenvalk",
+ "Dip - bar",
+ "Trampoline",
+ "Strand en strandwal",
+ "Duikerbrug",
+ "Interne overstortput",
+ "Ladder",
+ "Frame & net",
+ "Voorwaarschuwingsdriehoek",
+ "Drievoudig duikelrek zigzag",
+ "Free Runner - Cross Trainer",
+ "Gewapend beton",
+ "Bootcamp Base",
+ "Samenhangend",
+ "Zwevende evenwichtsbalk",
+ "Oppervlakbehandelingen",
+ "Bergbezinkleiding",
+ "Rioolstreng",
+ "Flat Ramp",
+ "Take Off Ramp",
+ "Goot",
+ "Onderbroken smalle streep",
+ "Basculebrug",
+ "Optrekken",
+ "Panel",
+ "Steps",
+ "Pull up Station",
+ "Combinatie - Kleuter",
+ "Monkey bar extended",
+ "Dubbele ophaalbrug",
+ "Shaped Grind Rail",
+ "Parallel bars",
+ "Push up bars met paal",
+ "Kindertafel",
+ "Laagstam",
+ "Verspring- en hinkstapspringbak",
+ "Draaischijf (type E)",
+ "Piramidevorm",
+ "Touw tornado",
+ "ZinloosGeweldMarkering",
+ "Step en Swing",
+ "Vrijstaande glijbaan - type 1",
+ "Jongerenbank",
+ "Slinger-klim-entercombi",
+ "Vormhaag",
+ "Hangbrug",
+ "Speelspoor",
+ "Speelstoel en tafel",
+ "Nestkast huismus",
+ "Springplank",
+ "Moerasvegetatie",
+ "Type 1 - Rotatie om 1 as",
+ "Polsstokhoogspringbak",
+ "Draaimolen",
+ "Overstortleiding",
+ "Incline Press",
+ "Boter-kaas-eieren",
+ "Palenwoud",
+ "Waterpomp",
+ "Klimschans",
+ "Vlot",
+ "Dubbele kabelbaan",
+ "Rear Panel",
+ "Gemengd riool",
+ "Hockeydoel",
+ "Free Runner",
+ "Planter for Steps",
+ "Waterglijbaan",
+ "Jump Ramp",
+ "Pyramid",
+ "Combinatie - Kind",
+ "Doorgetrokken smalle streep",
+ "Wisselperken",
+ "Natuurlijke grasvegetatie",
+ "Wall Ride",
+ "Blokhaag",
+ "Meer",
+ "Draadcircus",
+ "Puntstukken en witte vlakken",
+ "Klein fruit",
+ "Power Bike",
+ "Type 3A - Meerpunts - 1 richting",
+ "Stammenstapel",
+ "Steunsprong",
+ "Wiebelloop",
+ "Zitpaal",
+ "Cross & Circuit Training"
+ ],
+ "type": "string",
+ "title": "TypePlus",
+ "examples": [
+ "Grind Rail"
+ ],
+ "description": "Nadere typering van het type beheerobject."
+ },
+ "verplant": {
+ "$id": "#/properties/verplant",
+ "type": "boolean",
+ "title": "Verplant",
+ "examples": [],
+ "description": "Aanduidig of het groen- of vegetatieobject verplant is."
+ },
+ "boombeeld": {
+ "$id": "#/properties/boombeeld",
+ "enum": [
+ "Niet van toepassing",
+ "Verwaarloosd boombeeld",
+ "Aanvaard boombeeld",
+ "Achterstallig boombeeld",
+ "Boombeeld regulier (HB)",
+ "Niet te beoordelen"
+ ],
+ "type": "string",
+ "title": "Boombeeld",
+ "examples": [
+ "Niet van toepassing"
+ ],
+ "description": "Onderhoudssituatie van de boom."
+ },
+ "boomgroep": {
+ "$id": "#/properties/boomgroep",
+ "enum": [
+ "Laanboom",
+ "Boomweide",
+ "Solitaire boom"
+ ],
+ "type": "string",
+ "title": "Boomgroep",
+ "examples": [
+ "Laanboom"
+ ],
+ "description": "Aanduiding of de boom onderdeel is van een boomgroep."
+ },
+ "groeifase": {
+ "$id": "#/properties/groeifase",
+ "enum": [
+ "Jeugdfase",
+ "Volwassen fase",
+ "Eindfase",
+ "Aanlegfase",
+ "Onbekend",
+ "Niet te beoordelen"
+ ],
+ "type": "string",
+ "title": "Groeifase",
+ "examples": [
+ "Jeugdfase"
+ ],
+ "description": "Aanduiding van de groeifase van een boom.\nToelichting: Er is geen volledige eenduidigheid over de indeling, maar over het algemeen worden bij een boom zon 4 groeifasen onderscheiden. Het onderscheid is gebaseerd op de verschillen in beheermaatregelen."
+ },
+ "snoeifase": {
+ "$id": "#/properties/snoeifase",
+ "enum": [
+ "Begeleidingssnoeifase",
+ "Onbekend",
+ "Niet van toepassing",
+ "Onderhoudssnoeifase"
+ ],
+ "type": "string",
+ "title": "Snoeifase",
+ "examples": [
+ "Begeleidingssnoeifase"
+ ],
+ "description": "Aanduiding van de snoeifase van de boom."
+ },
+ "boomspiegel": {
+ "$id": "#/properties/boomspiegel",
+ "type": "string",
+ "title": "Boomspiegel",
+ "examples": [],
+ "description": "Wanneer een boomspiegel aanwezig is, wordt het GUID van het beheerobject Boomspiegel gekoppeld aan het object Boom.\nToelichting: Boomspiegel: afgebakend oppervlak rondom de stam van een boom, dat niet is ingeplant."
+ },
+ "kroonvolume": {
+ "$id": "#/properties/kroonvolume",
+ "type": "integer",
+ "title": "Kroonvolume",
+ "examples": [],
+ "description": "Volume van de boomkroon in kubieke meters\nEenheid: m3"
+ },
+ "meerstammig": {
+ "$id": "#/properties/meerstammig",
+ "type": "boolean",
+ "title": "Meerstammig",
+ "examples": [],
+ "description": "Aanduiding voor meerstammigheid bij een Boom"
+ },
+ "transponder": {
+ "$id": "#/properties/transponder",
+ "type": "string",
+ "title": "Transponder",
+ "examples": [],
+ "description": "Nummer of identificatie van een transponder op een beheerobject."
+ },
+ "vrijetakval": {
+ "$id": "#/properties/vrijetakval",
+ "enum": [
+ "Onbekend",
+ "Geen vrije takval mogelijk",
+ "Vrije takval mogelijk"
+ ],
+ "type": "string",
+ "title": "VrijeTakval",
+ "examples": [
+ "Onbekend"
+ ],
+ "description": "Aanduiding of vrije takval is toegestaan."
+ },
+ "stamdiameter": {
+ "$id": "#/properties/stamdiameter",
+ "type": "integer",
+ "title": "Stamdiameter",
+ "examples": [],
+ "description": "Aanduiding voor de diameter van de stam.\nEenheid: cm"
+ },
+ "takvrijestam": {
+ "$id": "#/properties/takvrijestam",
+ "enum": [
+ "0 m.",
+ "Anders, namelijk",
+ "4 m.",
+ "2 m.",
+ "8 m.",
+ "6 m.",
+ "Onbekend",
+ "Niet te beoordelen"
+ ],
+ "type": "string",
+ "title": "TakvrijeStam",
+ "examples": [
+ "0 m."
+ ],
+ "description": "De benodigde takvrije stam in het eindbeeld, gemeten vanaf maaiveld tot aan de onderste gesteltak.\nEenheid: m\nToelichting: Takvrije stam: gedeelte van de stam, gemeten vanaf maaiveld tot aan eerste gesteltak. Eindbeeld: vorm van een boom in volgroeide staat, omschreven door middel van takvrije zone en/of takvrije stam. Als synoniem wordt vaak gebruikt de opkroonhoogte: de verticaal gemeten vrije hoogte tussen maaiveld en kroon van de boom."
+ },
+ "verplantbaar": {
+ "$id": "#/properties/verplantbaar",
+ "type": "boolean",
+ "title": "Verplantbaar",
+ "examples": [],
+ "description": "Aanduiding of de boom verplant kan worden."
+ },
+ "beleidsstatus": {
+ "$id": "#/properties/beleidsstatus",
+ "enum": [
+ "Structuurbepalend/hoofd(groen/bomen)structuur",
+ "Geen specifieke status-verkorte omloop (tot ca. 20 jaar) en bomen 3e grootte",
+ "Beschermwaardig/monumentaal",
+ "Geen specifieke status-functionele laan- en parkbomen"
+ ],
+ "type": "string",
+ "title": "Beleidsstatus",
+ "examples": [
+ "Structuurbepalend/hoofd(groen/bomen)structuur"
+ ],
+ "description": "Beleidsstatus is een functiecategorie bomen conform de richtlijnen NVTB, t.b.v. de bepaling van de monetaire waarde."
+ },
+ "boombeschermer": {
+ "$id": "#/properties/boombeschermer",
+ "type": "string",
+ "title": "Boombeschermer",
+ "examples": [],
+ "description": "Wanneer een boombeschermer aanwezig is, wordt het GUID van het beheerobject Boombeschermer gekoppeld aan het object Boom.\nToelichting: Constructie, meestal van metaal, rondom het onderste gedeelte van de stam, bedoeld ter bescherming van de stam."
+ },
+ "herplantplicht": {
+ "$id": "#/properties/herplantplicht",
+ "type": "boolean",
+ "title": "Herplantplicht",
+ "examples": [],
+ "description": "Aanduiding of er in het kader van de Wet Natuurbescherming sprake is van een herplantplicht."
+ },
+ "feestverlichting": {
+ "$id": "#/properties/feestverlichting",
+ "type": "string",
+ "title": "Feestverlichting",
+ "examples": [],
+ "description": "Wanneer Feestverlichting aanwezig is, wordt het GUID van het beheerobject Feestverlichting gekoppeld aan het gekoppelde beheerobject."
+ },
+ "beoogdeomlooptijd": {
+ "$id": "#/properties/beoogdeomlooptijd",
+ "enum": [
+ "75-100 jaar",
+ "30-50 jaar",
+ ">200 jaar",
+ "50-75 jaar",
+ "20-30 jaar",
+ "100-150 jaar",
+ "< 10 jaar",
+ "Onbekend",
+ "10-20 jaar",
+ "150-200 jaar"
+ ],
+ "type": "string",
+ "title": "BeoogdeOmlooptijd",
+ "examples": [
+ "75-100 jaar"
+ ],
+ "description": "De potentieel haalbare omlooptijd, in relatie tot de standplaats (schatting)."
+ },
+ "boomhoogteactueel": {
+ "$id": "#/properties/boomhoogteactueel",
+ "type": "integer",
+ "title": "BoomhoogteActueel",
+ "examples": [],
+ "description": "Hoogte van de boom in meters.\nEenheid: m"
+ },
+ "controlefrequentie": {
+ "$id": "#/properties/controlefrequentie",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "Controlefrequentie",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Aanduiding van de frequentie van de controle van het beheerobject.\nToelichting: \"De frequentie van de boomveiligheidscontrole. Dit is de periodieke visuele controle van een boom in het kader van de zorgplicht (voortkomend uit artikel 6:162 van het BW) ten behoeve van het vaststellen van een (potentieel toekomstig) risico dat de boom vormt voor zijn omgeving. \nDe term boomveiligheidscontrole heeft betrekking op het gehele proces om in het veld de benodigde gegevens te verkrijgen voor het logboek, zoals beschreven in de CROW Richtlijn Boomveiligheidsregistratie.\n\""
+ },
+ "stamdiameterklasse": {
+ "$id": "#/properties/stamdiameterklasse",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "Stamdiameterklasse",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Aanduiding van de diameter van de stam in diameterklassen."
+ },
+ "vrijedoorrijhoogte": {
+ "$id": "#/properties/vrijedoorrijhoogte",
+ "enum": [
+ "4,5 m. en groter",
+ "6,5 m. en groter",
+ "0 m.",
+ "2,5 m. en groter",
+ "Onbekend"
+ ],
+ "type": "string",
+ "title": "VrijeDoorrijhoogte",
+ "examples": [
+ "4,5 m. en groter"
+ ],
+ "description": "De benodigde vrije ruimte tussen de weg of het fietspad, en de onderkant van de boomkroon of van een bouwwerk boven de weg, zoals een viaduct of tunnel.\nEenheid: m\nToelichting: Takvrije zone boven het wegdek en onder de kroon waar bestuurders van voertuigen vrije doorgang genieten tot een hoogte zoals is bepaald door de wegbeheerder."
+ },
+ "boomboomvoorziening": {
+ "$id": "#/properties/boomboomvoorziening",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "BoomBoomvoorziening",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Mogelijkheid om 1 of meerdere boomvoorzieningen bij een boom te registreren."
+ },
+ "monetaireboomwaarde": {
+ "$id": "#/properties/monetaireboomwaarde",
+ "type": "number",
+ "title": "MonetaireBoomwaarde",
+ "examples": [],
+ "description": "Monetaire waarde volgens richtlijnen NVTB.\nEenheid: "
+ },
+ "takvrijezoneprimair": {
+ "$id": "#/properties/takvrijezoneprimair",
+ "type": "integer",
+ "title": "TakvrijeZonePrimair",
+ "examples": [],
+ "description": "De benodigde takvrije ruimte tussen de weg of het fietspad en de onderkant van de boomkroon (eindbeeld van de boom). Als aan beide zijden van de boom een weg en een fietspad ligt, wordt de takvrije ruimte boven de weg aangeduid met primair, en de takvrijke ruimte boven het fietspad met secundair.\nEenheid: m\nToelichting: Takvrije zone: vrije ruimte ten behoeve van verkeer of andere omgevingsfactoren."
+ },
+ "boomveiligheidsklasse": {
+ "$id": "#/properties/boomveiligheidsklasse",
+ "enum": [
+ "Niet te beoordelen",
+ "Boom zonder gebreken",
+ "Onbekend",
+ "Attentieboom",
+ "Risicoboom"
+ ],
+ "type": "string",
+ "title": "Boomveiligheidsklasse",
+ "examples": [
+ "Niet te beoordelen"
+ ],
+ "description": "Aanduiding van de veiligheid van de boom, ingedeeld in vaste klassen.\nToelichting: Voor bosplantsoen met boomvormers is de mate van veiligheid van het beheerobject voor de omgeving relevant. Hiervoor is nog geen landelijk vastgestelde classificatie. Boomveiligheid: Aanduiding voor de veiligheid van personen, dieren, objecten en goederen in de nabijheid van een boom."
+ },
+ "groeiplaatsinrichting": {
+ "$id": "#/properties/groeiplaatsinrichting",
+ "type": "string",
+ "title": "Groeiplaatsinrichting",
+ "examples": [],
+ "description": "Wanneer een groeiplaatsinrichting aanwezig is, wordt het GUID van het beheerobject Groeiplaatsinrichting gekoppeld aan het object Boom"
+ },
+ "takvrijezonesecundair": {
+ "$id": "#/properties/takvrijezonesecundair",
+ "type": "integer",
+ "title": "TakvrijeZoneSecundair",
+ "examples": [],
+ "description": "De benodigde takvrije ruimte tussen het fietspad en de onderkant van de boomkroon (eindbeeld van de boom). Als aan beide zijden van de boom een weg en een fietspad ligt, wordt de takvrije ruimte boven de weg aangeduid met primair, en de takvrijke ruimte boven het fietspad met secundair.\nEenheid: m\nToelichting: Takvrije zone: vrije ruimte ten behoeve van verkeer of andere omgevingsfactoren."
+ },
+ "typebeschermingsstatus": {
+ "$id": "#/properties/typebeschermingsstatus",
+ "enum": [
+ "Monumentale boom",
+ "Geen beschermingsstatus",
+ "Rust - en of verblijfplaats fauna"
+ ],
+ "type": "string",
+ "title": "TypeBeschermingsstatus",
+ "examples": [
+ "Monumentale boom"
+ ],
+ "description": "Aanduiding voor de speciale status van de boom."
+ },
+ "typevermeerderingsvorm": {
+ "$id": "#/properties/typevermeerderingsvorm",
+ "enum": [
+ "Veredeld",
+ "Eigen wortel",
+ "Onbekend",
+ "Gent",
+ "Gezaaid"
+ ],
+ "type": "string",
+ "title": "TypeVermeerderingsvorm",
+ "examples": [
+ "Veredeld"
+ ],
+ "description": "Wijze waarop de plant of boom is vermeerderd."
+ },
+ "boomhoogteklasseactueel": {
+ "$id": "#/properties/boomhoogteklasseactueel",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "BoomhoogteklasseActueel",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Aanduiding van de boomhoogte in meters ingedeeld in vaste klassen.\nToelichting: Boomhoogte in meters, ingedeeld in vaste klassen, gemeten vanaf het maaiveld tot de top van de boom, bij vormbomen gemeten tot de hoogste knot of de gewenste hoogte (Bron: RAW)"
+ },
+ "takvrijeruimtetotgebouw": {
+ "$id": "#/properties/takvrijeruimtetotgebouw",
+ "type": "integer",
+ "title": "TakvrijeRuimteTotGebouw",
+ "examples": [],
+ "description": "De benodigde takvrije ruimte tussen het gebouw en de zijkant van de boom.\nEenheid: m"
+ },
+ "boomhoogteklasseeindbeeld": {
+ "$id": "#/properties/boomhoogteklasseeindbeeld",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "BoomhoogteklasseEindbeeld",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Aanduiding van de boomhoogte van het eindbeeld, in meters ingedeeld in vaste klassen.\nToelichting: Boomhoogte in meters, ingedeeld in vaste klassen, gemeten vanaf het maaiveld tot de top van de boom, bij vormbomen gemeten tot de hoogste knot of de gewenste hoogte (Bron: RAW)"
+ },
+ "typeomgevingsrisicoklasse": {
+ "$id": "#/properties/typeomgevingsrisicoklasse",
+ "enum": [
+ "Hoog",
+ "Gemiddeld",
+ "Laag",
+ "Onbekend",
+ "Geen"
+ ],
+ "type": "string",
+ "title": "TypeOmgevingsrisicoklasse",
+ "examples": [
+ "Hoog"
+ ],
+ "description": "Aanduiding van het omgevingsrisico van het beheerobject.\nToelichting: Classificering voor de intensiteit van het gebruik van de omgeving van een boom en daarmee de mate waarin de omgeving van de boom risicoverhogend is voor eventuele schade bij stambreuk, takbreuk of instabiliteit. Aanvullende informatie: vast te stellen door de boomeigenaar."
+ },
+ "vrijedoorrijhoogteprimair": {
+ "$id": "#/properties/vrijedoorrijhoogteprimair",
+ "enum": [
+ "4,5 m. en groter",
+ "6,5 m. en groter",
+ "0 m.",
+ "2,5 m. en groter",
+ "Onbekend"
+ ],
+ "type": "string",
+ "title": "VrijeDoorrijhoogtePrimair",
+ "examples": [
+ "4,5 m. en groter"
+ ],
+ "description": "De benodigde vrije ruimte tussen de weg of het fietspad, en de onderkant van de boomkroon of van een bouwwerk boven de weg, zoals een viaduct. Als aan beide zijden van de boom een weg en een fietspad ligt, wordt de vrije doorrijhoogte boven de weg aangeduid met primair, en de takvrije ruimte boven het fietspad met secundair.\nEenheid: m"
+ },
+ "kroondiameterklasseactueel": {
+ "$id": "#/properties/kroondiameterklasseactueel",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "KroondiameterklasseActueel",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Diameter van de kroon van de boom in meters ingedeeld in vaste klassen."
+ },
+ "vrijedoorrijhoogtesecundair": {
+ "$id": "#/properties/vrijedoorrijhoogtesecundair",
+ "enum": [
+ "4,5 m. en groter",
+ "6,5 m. en groter",
+ "0 m.",
+ "2,5 m. en groter",
+ "Onbekend"
+ ],
+ "type": "string",
+ "title": "VrijeDoorrijhoogteSecundair",
+ "examples": [
+ "4,5 m. en groter"
+ ],
+ "description": "De benodigde vrije ruimte tussen de weg of het fietspad, en de onderkant van de boomkroon of van een bouwwerk boven de weg, zoals een viaduct. Als aan beide zijden van de boom een weg en een fietspad ligt, wordt de vrije doorrijhoogte boven de weg aangeduid met primair, en de takvrije ruimte boven het fietspad met secundair.\nEenheid: m"
+ },
+ "kroondiameterklasseeindbeeld": {
+ "$id": "#/properties/kroondiameterklasseeindbeeld",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "KroondiameterklasseEindbeeld",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Diameter van de kroon van het eindbeeld van de boom in meters ingedeeld in vaste klassen."
+ },
+ "boomtypebeschermingsstatusplus": {
+ "$id": "#/properties/boomtypebeschermingsstatusplus",
+ "enum": [
+ "TODO"
+ ],
+ "type": "string",
+ "title": "BoomTypeBeschermingsstatusPlus",
+ "examples": [
+ "TODO"
+ ],
+ "description": "Nadere aanduiding voor de speciale status van de boom."
+ }
+ },
+ "description": "Een houtachtig gewas (loofboom of conifeer) met een wortelgestel en een enkele, stevige, houtige stam, die zich boven de grond vertakt.\nToelichting: Een houtachtig gewas (loofboom of conifeer) met een wortelgestel en een enkele, stevige, houtige stam, die zich boven de grond vertakt.",
+ "additionalProperties": false
+ },
+ "status": "published"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 3,
+ "fields": {
+ "object_type": 2,
+ "version": 1,
+ "created_at": "2020-12-01",
+ "modified_at": "2020-12-01",
+ "published_at": "2020-09-28",
+ "json_schema": {
+ "type": "object",
+ "title": "Straatverlichting",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": [
+ "id_nummer"
+ ],
+ "properties": {
+ "id_nummer": {
+ "type": "integer",
+ "title": "ID-nummer",
+ "description": "Identificatienummer"
+ }
+ }
+ },
+ "status": "published"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 4,
+ "fields": {
+ "object_type": 3,
+ "version": 1,
+ "created_at": "2020-12-01",
+ "modified_at": "2020-12-01",
+ "published_at": "2020-10-02",
+ "json_schema": {
+ "type": "object",
+ "title": "Melding",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": [
+ "description"
+ ],
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Explanation what happened"
+ }
+ }
+ },
+ "status": "published"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 5,
+ "fields": {
+ "object_type": 3,
+ "version": 2,
+ "created_at": "2020-11-12",
+ "modified_at": "2020-11-27",
+ "published_at": "2020-11-27",
+ "json_schema": {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": [
+ "description"
+ ],
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Explanation what happened"
+ }
+ }
+ },
+ "status": "published"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 6,
+ "fields": {
+ "object_type": 2,
+ "version": 2,
+ "created_at": "2020-11-13",
+ "modified_at": "2020-11-27",
+ "published_at": null,
+ "json_schema": {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "height": {
+ "type": "integer",
+ "description": "height in meters"
+ }
+ }
+ },
+ "status": "draft"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 9,
+ "fields": {
+ "object_type": 3,
+ "version": 3,
+ "created_at": "2020-11-27",
+ "modified_at": "2020-11-27",
+ "published_at": "2020-11-27",
+ "json_schema": {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": [
+ "description"
+ ],
+ "properties": {
+ "description": {
+ "type": "string",
+ "description": "Explanation what happened"
+ }
+ }
+ },
+ "status": "draft"
+ }
+},
+{
+ "model": "core.objecttypeversion",
+ "pk": 10,
+ "fields": {
+ "object_type": 1,
+ "version": 3,
+ "created_at": "2021-01-12",
+ "modified_at": "2021-01-12",
+ "published_at": null,
+ "json_schema": {
+ "type": "object",
+ "title": "Monument",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": [
+ "naam",
+ "kunstenaarsNaam"
+ ],
+ "properties": {
+ "bijschrift": {
+ "type": "string",
+ "description": "Bijschrift die de context van het monument verduidelijkt."
+ },
+ "kunstenaar": {
+ "type": "string",
+ "format": "url",
+ "description": "URL naar de Natuurlijk Persoon in de BRP."
+ },
+ "opleverdatum": {
+ "type": "string",
+ "format": "date",
+ "description": "Datum waarop het monument is onthuld."
+ },
+ "kunstenaarsNaam": {
+ "type": "string",
+ "description": "Voor en achternaam van de kunstenaar."
+ }
+ }
+ },
+ "status": "draft"
}
}
]
diff --git a/src/objects/setup_configuration/models/objecttypes.py b/src/objects/setup_configuration/models/objecttypes.py
index 4125848a..0d42c86b 100644
--- a/src/objects/setup_configuration/models/objecttypes.py
+++ b/src/objects/setup_configuration/models/objecttypes.py
@@ -1,13 +1,11 @@
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")
+ name: str = DjangoModelRef(ObjectType, "name")
class Meta:
django_model_refs = {ObjectType: ("uuid",)}
diff --git a/src/objects/setup_configuration/models/token_auth.py b/src/objects/setup_configuration/models/token_auth.py
index 7ae4ff6b..007079c6 100644
--- a/src/objects/setup_configuration/models/token_auth.py
+++ b/src/objects/setup_configuration/models/token_auth.py
@@ -1,4 +1,3 @@
-from django_setup_configuration.fields import DjangoModelRef
from django_setup_configuration.models import ConfigurationModel
from pydantic import UUID4, Field
@@ -9,29 +8,10 @@ class TokenAuthPermissionConfigurationModel(ConfigurationModel):
object_type: UUID4 = Field(
description="The UUID of the objecttype for which permission will be configured for this token."
)
- fields: dict[str, list[str]] | None = DjangoModelRef(
- Permission,
- "fields",
- default=None,
- examples=[{"1": ["record__data__leeftijd", "record__data__kiemjaar"]}],
- description=(
- "The fields to which this token has access (field-based authorization). "
- "Note that this can only be specified if the permission mode is `read_only` "
- "and use_fields is set to `true`."
- ),
- )
class Meta:
django_model_refs = {
- Permission: (
- "mode",
- "use_fields",
- ),
- }
- extra_kwargs = {
- "use_fields": {
- "examples": [True],
- },
+ Permission: ("mode",),
}
diff --git a/src/objects/setup_configuration/steps/objecttypes.py b/src/objects/setup_configuration/steps/objecttypes.py
index 75ffddf4..d2bc2b6b 100644
--- a/src/objects/setup_configuration/steps/objecttypes.py
+++ b/src/objects/setup_configuration/steps/objecttypes.py
@@ -3,7 +3,6 @@
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
@@ -26,25 +25,16 @@ class ObjectTypesConfigurationStep(BaseConfigurationStep):
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,
+ name=item.name,
+ name_plural=f"{item.name}s",
)
objecttype_instance = ObjectType(**objecttype_kwargs)
try:
- objecttype_instance.full_clean(
- exclude=("id", "service"), validate_unique=False
- )
+ objecttype_instance.full_clean(exclude=("id"), validate_unique=False)
except ValidationError as exception:
exception_message = (
f"Validation error(s) occured for objecttype {item.uuid}."
diff --git a/src/objects/setup_configuration/steps/token_auth.py b/src/objects/setup_configuration/steps/token_auth.py
index 1b313296..45b0e447 100644
--- a/src/objects/setup_configuration/steps/token_auth.py
+++ b/src/objects/setup_configuration/steps/token_auth.py
@@ -51,8 +51,6 @@ def _configure_permissions(self, token: TokenAuth, permissions: list) -> None:
"token_auth": token,
"object_type": ObjectType.objects.get(uuid=permission.object_type),
"mode": permission.mode,
- "use_fields": permission.use_fields,
- "fields": permission.fields,
}
except ObjectDoesNotExist as exception:
raise ConfigurationRunFailed(
@@ -68,8 +66,6 @@ def _configure_permissions(self, token: TokenAuth, permissions: list) -> None:
object_type=permission_kwargs["object_type"],
defaults={
"mode": permission_kwargs["mode"],
- "use_fields": permission_kwargs["use_fields"],
- "fields": permission_kwargs["fields"],
},
)
except IntegrityError as exception:
diff --git a/src/objects/setup_configuration/tests/files/token_auth/valid_setup_complete.yaml b/src/objects/setup_configuration/tests/files/token_auth/valid_setup_complete.yaml
index 67557d7e..af9f864c 100644
--- a/src/objects/setup_configuration/tests/files/token_auth/valid_setup_complete.yaml
+++ b/src/objects/setup_configuration/tests/files/token_auth/valid_setup_complete.yaml
@@ -11,11 +11,6 @@ tokenauth:
permissions:
- object_type: 3a82fb7f-fc9b-4104-9804-993f639d6d0d
mode: read_only
- use_fields: true
- fields:
- '1':
- - record__data__leeftijd
- - record__data__kiemjaar
- object_type: ca754b52-3f37-4c49-837c-130e8149e337
mode: read_and_write
diff --git a/src/objects/setup_configuration/tests/test_token_auth_config.py b/src/objects/setup_configuration/tests/test_token_auth_config.py
index 83d3c599..5f3cca9f 100644
--- a/src/objects/setup_configuration/tests/test_token_auth_config.py
+++ b/src/objects/setup_configuration/tests/test_token_auth_config.py
@@ -7,8 +7,6 @@
PrerequisiteFailed,
)
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
@@ -21,21 +19,17 @@
class TokenTestCase(TestCase):
def setUp(self):
- self.service = ServiceFactory.create(slug="service")
ObjectTypeFactory.create(
- service=self.service,
uuid="3a82fb7f-fc9b-4104-9804-993f639d6d0d",
- _name="Object Type 001",
+ name="Object Type 001",
)
ObjectTypeFactory.create(
- service=self.service,
uuid="ca754b52-3f37-4c49-837c-130e8149e337",
- _name="Object Type 002",
+ name="Object Type 002",
)
ObjectTypeFactory.create(
- service=self.service,
uuid="feeaa795-d212-4fa2-bb38-2c34996e5702",
- _name="Object Type 003",
+ name="Object Type 003",
)
@@ -435,7 +429,6 @@ class TokenAuthConfigurationStepWithPermissionsTests(TokenTestCase):
def test_valid_setup_default_without_permissions(self):
self.assertEqual(TokenAuth.objects.count(), 0)
self.assertEqual(Permission.objects.count(), 0)
- self.assertEqual(Service.objects.count(), 1)
self.assertEqual(ObjectType.objects.count(), 3)
execute_single_step(
@@ -470,7 +463,6 @@ def test_valid_setup_default_without_permissions(self):
def test_valid_setup_complete(self):
self.assertEqual(TokenAuth.objects.count(), 0)
self.assertEqual(Permission.objects.count(), 0)
- self.assertEqual(Service.objects.count(), 1)
self.assertEqual(ObjectType.objects.count(), 3)
execute_single_step(
@@ -493,28 +485,19 @@ def test_valid_setup_complete(self):
self.assertEqual(token.object_types.count(), 2)
self.assertEqual(token_permissions.count(), 2)
object_type = ObjectType.objects.get(
- uuid="3a82fb7f-fc9b-4104-9804-993f639d6d0d", service=self.service
+ uuid="3a82fb7f-fc9b-4104-9804-993f639d6d0d"
)
permission = token_permissions.get(object_type=object_type)
self.assertTrue(object_type in token.object_types.all())
self.assertTrue(permission in token.permissions.all())
self.assertEqual(permission.mode, "read_only")
- self.assertTrue(permission.use_fields)
- self.assertTrue(isinstance(permission.fields, dict))
- self.assertTrue(isinstance(permission.fields["1"], list))
- self.assertEqual(len(permission.fields.keys()), 1)
- self.assertTrue("1" in permission.fields)
- self.assertTrue("record__data__leeftijd" in permission.fields["1"])
- self.assertTrue("record__data__kiemjaar" in permission.fields["1"])
object_type = ObjectType.objects.get(
- uuid="ca754b52-3f37-4c49-837c-130e8149e337", service=self.service
+ uuid="ca754b52-3f37-4c49-837c-130e8149e337"
)
permission = token_permissions.get(object_type=object_type)
self.assertTrue(object_type in token.object_types.all())
self.assertTrue(permission in token.permissions.all())
self.assertEqual(permission.mode, "read_and_write")
- self.assertFalse(permission.use_fields)
- self.assertIsNone(permission.fields)
token = tokens.get(identifier="token-2")
token_permissions = token.permissions.all()
@@ -528,14 +511,12 @@ def test_valid_setup_complete(self):
self.assertEqual(token.permissions.count(), 1)
self.assertEqual(token.object_types.count(), 1)
object_type = ObjectType.objects.get(
- uuid="feeaa795-d212-4fa2-bb38-2c34996e5702", service=self.service
+ uuid="feeaa795-d212-4fa2-bb38-2c34996e5702"
)
permission = token_permissions.get(object_type=object_type)
self.assertTrue(object_type in token.object_types.all())
self.assertTrue(permission in token.permissions.all())
self.assertEqual(permission.mode, "read_only")
- self.assertFalse(permission.use_fields)
- self.assertIsNone(permission.fields)
token = tokens.get(identifier="token-3")
self.assertEqual(token.contact_person, "Person 3")
@@ -584,14 +565,12 @@ def test_valid_update_permissions(self):
self.assertEqual(token.permissions.count(), 1)
self.assertEqual(token.object_types.count(), 1)
object_type = ObjectType.objects.get(
- uuid="3a82fb7f-fc9b-4104-9804-993f639d6d0d", service=self.service
+ uuid="3a82fb7f-fc9b-4104-9804-993f639d6d0d"
)
permission = token.permissions.get(object_type=object_type)
self.assertTrue(object_type in token.object_types.all())
self.assertTrue(permission in token.permissions.all())
self.assertEqual(permission.mode, "read_and_write")
- self.assertFalse(permission.use_fields)
- self.assertIsNone(permission.fields)
# Update token permissions
execute_single_step(
@@ -612,18 +591,10 @@ def test_valid_update_permissions(self):
self.assertTrue(object_type in token.object_types.all())
self.assertTrue(permission in token.permissions.all())
self.assertEqual(permission.mode, "read_only")
- self.assertTrue(permission.use_fields)
- self.assertTrue(isinstance(permission.fields, dict))
- self.assertTrue(isinstance(permission.fields["1"], list))
- self.assertEqual(len(permission.fields.keys()), 1)
- self.assertTrue("1" in permission.fields)
- self.assertTrue("record__data__leeftijd" in permission.fields["1"])
- self.assertTrue("record__data__kiemjaar" in permission.fields["1"])
def test_valid_idempotent_step(self):
self.assertEqual(TokenAuth.objects.count(), 0)
self.assertEqual(Permission.objects.count(), 0)
- self.assertEqual(Service.objects.count(), 1)
self.assertEqual(ObjectType.objects.count(), 3)
execute_single_step(
@@ -647,19 +618,12 @@ def test_valid_idempotent_step(self):
self.assertEqual(old_token.object_types.count(), 2)
self.assertEqual(old_token_permissions.count(), 2)
object_type = ObjectType.objects.get(
- uuid="3a82fb7f-fc9b-4104-9804-993f639d6d0d", service=self.service
+ uuid="3a82fb7f-fc9b-4104-9804-993f639d6d0d"
)
old_permission = old_token_permissions.get(object_type=object_type)
self.assertTrue(object_type in old_token.object_types.all())
self.assertTrue(old_permission in old_token.permissions.all())
self.assertEqual(old_permission.mode, "read_only")
- self.assertTrue(old_permission.use_fields)
- self.assertTrue(isinstance(old_permission.fields, dict))
- self.assertTrue(isinstance(old_permission.fields["1"], list))
- self.assertEqual(len(old_permission.fields.keys()), 1)
- self.assertTrue("1" in old_permission.fields)
- self.assertTrue("record__data__leeftijd" in old_permission.fields["1"])
- self.assertTrue("record__data__kiemjaar" in old_permission.fields["1"])
execute_single_step(
TokenAuthConfigurationStep,
@@ -684,18 +648,11 @@ def test_valid_idempotent_step(self):
self.assertTrue(object_type in new_token.object_types.all())
self.assertTrue(new_permission in new_token.permissions.all())
self.assertEqual(new_permission.mode, "read_only")
- self.assertTrue(new_permission.use_fields)
- self.assertTrue(isinstance(new_permission.fields, dict))
- self.assertTrue(isinstance(new_permission.fields["1"], list))
- self.assertEqual(len(new_permission.fields.keys()), 1)
- self.assertTrue("1" in new_permission.fields)
- self.assertTrue("record__data__leeftijd" in new_permission.fields["1"])
- self.assertTrue("record__data__kiemjaar" in new_permission.fields["1"])
def test_invalid_permissions_object_type_does_not_exist(self):
self.assertFalse(
ObjectType.objects.filter(
- uuid="69feca90-6c3d-4628-ace8-19e4b0ae4065", service=self.service
+ uuid="69feca90-6c3d-4628-ace8-19e4b0ae4065"
).exists()
)
object_source = {
@@ -714,13 +671,6 @@ def test_invalid_permissions_object_type_does_not_exist(self):
{
"object_type": "69feca90-6c3d-4628-ace8-19e4b0ae4065",
"mode": "read_only",
- "use_fields": True,
- "fields": {
- "1": [
- "record__data__leeftijd",
- "record__data__kiemjaar",
- ]
- },
},
],
},
@@ -755,13 +705,6 @@ def test_invalid_permissions_mode_not_valid(self):
{
"object_type": "3a82fb7f-fc9b-4104-9804-993f639d6d0d",
"mode": "test",
- "use_fields": True,
- "fields": {
- "1": [
- "record__data__leeftijd",
- "record__data__kiemjaar",
- ]
- },
},
],
},
@@ -776,41 +719,3 @@ def test_invalid_permissions_mode_not_valid(self):
)
self.assertEqual(TokenAuth.objects.count(), 0)
self.assertEqual(Permission.objects.count(), 0)
-
- def test_invalid_permissions_field_based_authorization(self):
- object_source = {
- "tokenauth_config_enable": True,
- "tokenauth": {
- "items": [
- {
- "identifier": "token-1",
- "token": "ba9d233e95e04c4a8a661a27daffe7c9bd019067",
- "contact_person": "Person 1",
- "email": "person-1@example.com",
- "organization": "Organization 1",
- "application": "Application 1",
- "administration": "Administration 1",
- "permissions": [
- {
- "object_type": "3a82fb7f-fc9b-4104-9804-993f639d6d0d",
- "mode": "read_and_write",
- "use_fields": True,
- "fields": {
- "1": [
- "record__data__leeftijd",
- "record__data__kiemjaar",
- ]
- },
- },
- ],
- },
- ],
- },
- }
- with self.assertRaises(ConfigurationRunFailed) as command_error:
- execute_single_step(TokenAuthConfigurationStep, object_source=object_source)
-
- self.assertTrue(
- "Validation error(s) during instance cleaning"
- in str(command_error.exception)
- )
diff --git a/src/objects/templates/admin/core/objecttype/object_history.html b/src/objects/templates/admin/core/objecttype/object_history.html
new file mode 100644
index 00000000..bee875f1
--- /dev/null
+++ b/src/objects/templates/admin/core/objecttype/object_history.html
@@ -0,0 +1,35 @@
+{% extends "admin/object_history.html" %}
+{% load i18n admin_urls %}
+
+
+{% block content %}
+
+
+
+
+
+ | {% trans 'Version' %} |
+ {% trans 'Status' %} |
+ {% trans 'Created at' %} |
+ {% trans 'Modified at' %} |
+ {% trans 'Published at' %} |
+ {% trans 'JSON schema' %} |
+
+
+
+ {% for version in object.ordered_versions %}
+
+ | {{ version.version }} |
+ {{ version.get_status_display }} |
+ {{ version.created_at }} |
+ {{ version.modified_at }} |
+ {{ version.published_at }} |
+ {{ version.json_schema }} |
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
diff --git a/src/objects/templates/admin/core/objecttype/object_import_form.html b/src/objects/templates/admin/core/objecttype/object_import_form.html
new file mode 100644
index 00000000..709b9170
--- /dev/null
+++ b/src/objects/templates/admin/core/objecttype/object_import_form.html
@@ -0,0 +1,37 @@
+{% extends 'admin/change_form.html' %}
+{% load i18n admin_urls static %}
+
+{% block title %} {% trans "Import objecttype" %} {{ block.super }} {% endblock %}
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+{% block content %}
+ {% trans 'Import from URL' %}
+
+
+{% endblock %}
diff --git a/src/objects/templates/admin/core/objecttype/object_list.html b/src/objects/templates/admin/core/objecttype/object_list.html
new file mode 100644
index 00000000..86ff2d1b
--- /dev/null
+++ b/src/objects/templates/admin/core/objecttype/object_list.html
@@ -0,0 +1,11 @@
+{% extends "admin/change_list.html" %}
+{% load i18n admin_urls %}
+
+{% block object-tools-items %}
+
+
+ {% trans 'Import from URL' %}
+
+
+ {{ block.super }}
+{% endblock %}
diff --git a/src/objects/templates/admin/core/objecttype/submit_line.html b/src/objects/templates/admin/core/objecttype/submit_line.html
new file mode 100644
index 00000000..dec5bd47
--- /dev/null
+++ b/src/objects/templates/admin/core/objecttype/submit_line.html
@@ -0,0 +1,14 @@
+{% extends 'admin/submit_line.html' %}
+{% load i18n %}
+
+{% block submit-row %}
+ {{ block.super }}
+
+ {% if original.last_version.status == 'draft' %}
+
+
+ {% elif original.last_version.status == 'published' %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/src/objects/tests/admin/test_core_views.py b/src/objects/tests/admin/test_core_views.py
deleted file mode 100644
index ea8ad676..00000000
--- a/src/objects/tests/admin/test_core_views.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from django.urls import reverse
-
-import requests_mock
-from django_webtest import WebTest
-from maykin_2fa.test import disable_admin_mfa
-from requests.exceptions import HTTPError
-
-from objects.accounts.tests.factories import UserFactory
-from objects.token.tests.factories import ObjectTypeFactory
-
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
-
-
-@disable_admin_mfa()
-@requests_mock.Mocker()
-class ObjectTypeAdminVersionsTests(WebTest):
- def test_valid_response_view(self, m):
- objecttypes_api = "https://example.com/objecttypes/v1/"
- object_type = ObjectTypeFactory.create(service__api_root=objecttypes_api)
- mock_service_oas_get(m, objecttypes_api, "objecttypes")
- m.get(f"{objecttypes_api}objecttypes", json=[])
- m.get(object_type.url, json=mock_objecttype(object_type.url))
- version = mock_objecttype_version(object_type.url, attrs={"jsonSchema": {}})
- m.get(
- object_type.versions_url,
- json={
- "count": 1,
- "next": None,
- "previous": None,
- "results": [version],
- },
- )
-
- user = UserFactory.create(is_staff=True, is_superuser=True)
-
- # object_type exist
- url = reverse("admin:objecttype_versions", args=[object_type.pk])
- response = self.app.get(url, user=user)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(len(response.json), 1)
-
- # object_type does not exist
- url = reverse("admin:objecttype_versions", args=[object_type.pk + 1])
- response = self.app.get(url, user=user)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json, [])
-
- def test_endpoint_unreachable(self, m):
- user = UserFactory.create(is_staff=True, is_superuser=True)
- object_type = ObjectTypeFactory.create()
- m.get(object_type.versions_url, exc=HTTPError)
-
- url = reverse("admin:objecttype_versions", args=[object_type.pk])
- response = self.app.get(url, user=user)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json, [])
-
- def test_invalid_authentication_view(self, m):
- url = reverse("admin:objecttype_versions", args=[1])
- response = self.client.get(url)
- redirect_url = f"{reverse('admin:login')}?next={url}"
- self.assertRedirects(response, redirect_url, status_code=302)
-
- def test_invalid_permission_view(self, m):
- user = UserFactory.create(is_staff=False, is_superuser=False)
- url = reverse("admin:objecttype_versions", args=[1])
- response = self.app.get(url, user=user, auto_follow=True)
- self.assertContains(
- response,
- f"You are authenticated as {user.username}, but are not authorized to access this page",
- )
diff --git a/src/objects/tests/admin/test_objecttype_admin.py b/src/objects/tests/admin/test_objecttype_admin.py
new file mode 100644
index 00000000..c9bc5ad1
--- /dev/null
+++ b/src/objects/tests/admin/test_objecttype_admin.py
@@ -0,0 +1,465 @@
+import json
+from datetime import date
+
+from django.urls import reverse, reverse_lazy
+
+import requests_mock
+from django_webtest import WebTest
+from freezegun import freeze_time
+from maykin_2fa.test import disable_admin_mfa
+
+from objects.accounts.tests.factories import SuperUserFactory
+from objects.core.constants import (
+ DataClassificationChoices,
+ ObjectTypeVersionStatus,
+ UpdateFrequencyChoices,
+)
+from objects.core.models import ObjectType
+from objects.core.tests.factories import ObjectTypeFactory, ObjectTypeVersionFactory
+
+JSON_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "title": "Tree",
+ "description": (
+ "A woody plant (deciduous or coniferous) with a root system and a "
+ "single, sturdy, woody stem, branching above the ground."
+ ),
+ "required": ["diameter"],
+ "properties": {"diameter": {"type": "integer", "description": "size in cm."}},
+}
+
+
+@freeze_time("2020-01-01")
+@disable_admin_mfa()
+class AdminAddTests(WebTest):
+ url = reverse_lazy("admin:core_objecttype_add")
+ import_from_url = reverse_lazy("admin:import_from_url")
+
+ @classmethod
+ def setUpTestData(cls):
+ super().setUpTestData()
+
+ cls.user = SuperUserFactory.create()
+
+ def setUp(self) -> None:
+ super().setUp()
+
+ self.app.set_user(self.user)
+
+ def test_create_objecttype_success(self):
+ get_response = self.app.get(self.url)
+
+ form = get_response.forms[1]
+ form["name"] = "boom"
+ form["name_plural"] = "bomen"
+ form["description"] = "some object type description"
+ form["data_classification"] = DataClassificationChoices.intern
+ form["maintainer_organization"] = "tree municipality"
+ form["maintainer_department"] = "object types department"
+ form["contact_person"] = "John Smith"
+ form["contact_email"] = "John.Smith@objecttypes.nl"
+ form["source"] = "tree system"
+ form["update_frequency"] = UpdateFrequencyChoices.monthly
+ form["provider_organization"] = "tree provider"
+ form["documentation_url"] = "http://example.com/doc/trees"
+ form["labels"] = json.dumps({"key1": "value1"})
+ form["versions-0-json_schema"] = json.dumps(JSON_SCHEMA)
+
+ response = form.submit()
+
+ # redirect on successful create, 200 on validation errors, 500 on db errors
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(ObjectType.objects.count(), 1)
+
+ object_type = ObjectType.objects.get()
+
+ self.assertEqual(object_type.name, "boom")
+ self.assertEqual(object_type.name_plural, "bomen")
+ self.assertEqual(object_type.description, "some object type description")
+ self.assertEqual(
+ object_type.data_classification, DataClassificationChoices.intern
+ )
+ self.assertEqual(object_type.maintainer_organization, "tree municipality")
+ self.assertEqual(object_type.maintainer_department, "object types department")
+ self.assertEqual(object_type.contact_person, "John Smith")
+ self.assertEqual(object_type.contact_email, "John.Smith@objecttypes.nl")
+ self.assertEqual(object_type.source, "tree system")
+ self.assertEqual(object_type.update_frequency, UpdateFrequencyChoices.monthly)
+ self.assertEqual(object_type.provider_organization, "tree provider")
+ self.assertEqual(object_type.documentation_url, "http://example.com/doc/trees")
+ self.assertEqual(object_type.labels, {"key1": "value1"})
+ self.assertEqual(object_type.created_at, date(2020, 1, 1))
+ self.assertEqual(object_type.modified_at, date(2020, 1, 1))
+ self.assertEqual(object_type.versions.count(), 1)
+
+ object_version = object_type.last_version
+
+ self.assertEqual(object_version.version, 1)
+ self.assertEqual(object_version.json_schema, JSON_SCHEMA)
+ self.assertEqual(object_version.status, ObjectTypeVersionStatus.draft)
+ self.assertEqual(object_version.created_at, date(2020, 1, 1))
+ self.assertEqual(object_version.modified_at, date(2020, 1, 1))
+ self.assertIsNone(object_version.published_at)
+
+ def test_create_objecttype_invalid_json_schema(self):
+ get_response = self.app.get(self.url)
+
+ form = get_response.forms[1]
+ form["name"] = "boom"
+ form["name_plural"] = "bomen"
+ form["description"] = "some object type description"
+ form["data_classification"] = DataClassificationChoices.intern
+ form["maintainer_organization"] = "tree municipality"
+ form["maintainer_department"] = "object types department"
+ form["contact_person"] = "John Smith"
+ form["contact_email"] = "John.Smith@objecttypes.nl"
+ form["source"] = "tree system"
+ form["update_frequency"] = UpdateFrequencyChoices.monthly
+ form["provider_organization"] = "tree provider"
+ form["documentation_url"] = "http://example.com/doc/trees"
+ form["labels"] = json.dumps({"key1": "value1"})
+ form["versions-0-json_schema"] = "{}{"
+
+ response = form.submit()
+
+ # redirect on successful create, 200 on validation errors, 500 on db errors
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(ObjectType.objects.count(), 0)
+
+ error_list = response.html.find("ul", {"class": "errorlist"})
+
+ # The submitted value is shown in the error
+ self.assertIn("{}{", error_list.text)
+
+ json_schema_field = response.forms[1].fields["versions-0-json_schema"][0]
+
+ # Verify that the value of the JSON schema field is the fallback value
+ self.assertEqual(json_schema_field.value, "{}")
+
+ def test_create_objecttype_without_version_fail(self):
+ get_response = self.app.get(self.url)
+
+ form = get_response.forms[1]
+ form["name"] = "boom"
+ form["name_plural"] = "bomen"
+ form["description"] = "some object type description"
+ form["maintainer_organization"] = "tree municipality"
+
+ response = form.submit()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(ObjectType.objects.count(), 0)
+
+ def test_create_objecttype_with_invalid_json_schema(self):
+ get_response = self.app.get(self.url)
+
+ form = get_response.forms[1]
+ form["name"] = "boom"
+ form["name_plural"] = "bomen"
+ form["description"] = "some object type description"
+ form["maintainer_organization"] = "tree municipality"
+ form["versions-0-json_schema"] = json.dumps(
+ {
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "any",
+ }
+ )
+
+ response = form.submit()
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(ObjectType.objects.count(), 0)
+
+ @requests_mock.Mocker()
+ def test_create_objecttype_from_url(self, m):
+ get_response = self.app.get(self.import_from_url)
+
+ m.get("https://example.com/tree.json", json=JSON_SCHEMA)
+
+ form = get_response.form
+ form["objecttype_url"] = "https://example.com/tree.json"
+ form["name_plural"] = "Trees"
+
+ response = form.submit()
+
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(ObjectType.objects.count(), 1)
+
+ object_type = ObjectType.objects.get()
+
+ self.assertEqual(object_type.name, "Tree")
+ self.assertEqual(object_type.name_plural, "Trees")
+ self.assertEqual(
+ object_type.description,
+ "A woody plant (deciduous or coniferous) with a root system and a single, sturdy, woody stem, branching "
+ "above the ground.",
+ )
+ self.assertEqual(
+ object_type.data_classification, DataClassificationChoices.open
+ )
+ self.assertEqual(object_type.created_at, date(2020, 1, 1))
+ self.assertEqual(object_type.modified_at, date(2020, 1, 1))
+ self.assertEqual(object_type.versions.count(), 1)
+
+ object_version = object_type.last_version
+
+ self.assertEqual(object_version.version, 1)
+ self.assertEqual(object_version.status, ObjectTypeVersionStatus.draft)
+ self.assertEqual(object_version.created_at, date(2020, 1, 1))
+ self.assertEqual(object_version.modified_at, date(2020, 1, 1))
+ self.assertIsNone(object_version.published_at)
+ self.assertEqual(object_version.json_schema, JSON_SCHEMA)
+
+ @requests_mock.Mocker()
+ def test_create_objecttype_from_url_with_invalid_schema(self, m):
+ get_response = self.app.get(self.import_from_url)
+
+ m.get(
+ "https://example.com/tree.json",
+ json={
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "any",
+ },
+ )
+
+ form = get_response.form
+ form["objecttype_url"] = "https://example.com/tree.json"
+ form["name_plural"] = "bomen"
+
+ response = form.submit()
+
+ self.assertIn("Invalid JSON schema.", response.text)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(ObjectType.objects.count(), 0)
+
+ def test_create_objecttype_from_url_with_nonexistent_url(self):
+ get_response = self.app.get(self.import_from_url)
+
+ form = get_response.form
+ form["objecttype_url"] = "https://random-url123.com"
+ form["name_plural"] = "bomen"
+
+ response = form.submit()
+
+ self.assertIn("The Objecttype URL does not exist.", response.text)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(ObjectType.objects.count(), 0)
+
+
+@disable_admin_mfa()
+class AdminDetailTests(WebTest):
+ @classmethod
+ def setUpTestData(cls):
+ super().setUpTestData()
+
+ cls.user = SuperUserFactory.create()
+
+ def setUp(self) -> None:
+ super().setUp()
+
+ self.app.set_user(self.user)
+
+ def test_display_successfully_without_versions(self):
+ object_type = ObjectTypeFactory.create()
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ self.assertEqual(get_response.status_code, 200)
+
+ form = get_response.forms[1]
+
+ self.assertEqual(form["versions-0-id"].value, "")
+
+ def test_display_only_last_version(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create_batch(3, object_type=object_type)
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+ form = get_response.forms[1]
+
+ self.assertEqual(int(form["versions-TOTAL_FORMS"].value), 1)
+ self.assertEqual(int(form["versions-0-id"].value), object_type.last_version.id)
+
+ def test_update_draft(self):
+ old_schema = {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "diameter": {"type": "integer", "description": "size in cm."}
+ },
+ }
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(
+ object_type=object_type, json_schema=old_schema
+ )
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ save_button = get_response.html.find("input", {"name": "_save"})
+ self.assertIsNotNone(save_button)
+
+ form = get_response.forms[1]
+ form["versions-0-json_schema"] = json.dumps(JSON_SCHEMA)
+ response = form.submit()
+
+ self.assertEqual(response.status_code, 302)
+
+ object_version.refresh_from_db()
+ self.assertEqual(object_version.json_schema, JSON_SCHEMA)
+
+ def test_update_draft_invalid_json_schema(self):
+ old_schema = {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "diameter": {"type": "integer", "description": "size in cm."}
+ },
+ }
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(
+ object_type=object_type, json_schema=old_schema
+ )
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ save_button = get_response.html.find("input", {"name": "_save"})
+ self.assertIsNotNone(save_button)
+
+ form = get_response.forms[1]
+ form["versions-0-json_schema"] = "{}{"
+ response = form.submit()
+
+ self.assertEqual(response.status_code, 200)
+
+ object_version.refresh_from_db()
+ self.assertEqual(object_version.json_schema, old_schema)
+
+ error_list = response.html.find("ul", {"class": "errorlist"})
+
+ # The submitted value is shown in the error
+ self.assertIn("{}{", error_list.text)
+
+ json_schema_field = response.forms[1].fields["versions-0-json_schema"][0]
+
+ # Verify that the value of the JSON schema field is the fallback value
+ self.assertEqual(json_schema_field.value, json.dumps(old_schema))
+
+ def test_update_draft_save_button(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(
+ object_type=object_type, status=ObjectTypeVersionStatus.draft
+ )
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ save_button = get_response.html.find("input", {"name": "_save"})
+ self.assertIsNotNone(save_button)
+
+ def test_update_published_save_button(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(
+ object_type=object_type, status=ObjectTypeVersionStatus.published
+ )
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ save_button = get_response.html.find("input", {"name": "_save"})
+ self.assertIsNotNone(save_button)
+
+ def test_publish_draft(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(object_type=object_type)
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ publish_button = get_response.html.find("input", {"name": "_publish"})
+ self.assertIsNotNone(publish_button)
+
+ form = get_response.forms[1]
+ response = form.submit("_publish")
+
+ self.assertEqual(response.status_code, 302)
+
+ object_version.refresh_from_db()
+ self.assertEqual(object_version.status, ObjectTypeVersionStatus.published)
+ self.assertEqual(object_version.published_at, date.today())
+
+ def test_publish_published_no_button(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(
+ object_type=object_type, status=ObjectTypeVersionStatus.published
+ )
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ publish_button = get_response.html.find("input", {"name": "_publish"})
+ self.assertIsNone(publish_button)
+
+ def test_new_version_draft_no_button(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(object_type=object_type)
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ new_version_button = get_response.html.find("input", {"name": "_newversion"})
+ self.assertIsNone(new_version_button)
+
+ @freeze_time("2020-02-02")
+ def test_new_version_published(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(
+ object_type=object_type, status=ObjectTypeVersionStatus.published
+ )
+ url = reverse("admin:core_objecttype_change", args=[object_type.id])
+
+ get_response = self.app.get(url)
+
+ new_version_button = get_response.html.find("input", {"name": "_newversion"})
+ self.assertIsNotNone(new_version_button)
+
+ form = get_response.forms[1]
+ response = form.submit("_newversion")
+
+ self.assertEqual(response.status_code, 302)
+
+ object_type.refresh_from_db()
+ self.assertEqual(object_type.versions.count(), 2)
+
+ last_version = object_type.last_version
+ self.assertNotEqual(last_version, object_version)
+ self.assertEqual(last_version.version, object_version.version + 1)
+ self.assertEqual(last_version.json_schema, object_version.json_schema)
+ self.assertEqual(last_version.status, ObjectTypeVersionStatus.draft)
+
+ def test_display_all_versions_in_history(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create_batch(3, object_type=object_type)
+ url = reverse("admin:core_objecttype_history", args=[object_type.id])
+
+ response = self.app.get(url)
+ object_type = response.context["object"]
+
+ self.assertEqual(response.status_code, 200)
+
+ table = response.html.find(id="change-history")
+ table_rows = table.tbody.find_all("tr")
+
+ self.assertEqual(len(table_rows), 3)
+
+ for object_version, row in zip(object_type.ordered_versions, table_rows):
+ row_version = row.find("td")
+ self.assertEqual(int(row_version.text), object_version.version)
diff --git a/src/objects/tests/admin/test_token_permissions.py b/src/objects/tests/admin/test_token_permissions.py
index 169c804d..049103ef 100644
--- a/src/objects/tests/admin/test_token_permissions.py
+++ b/src/objects/tests/admin/test_token_permissions.py
@@ -2,21 +2,16 @@
from django.urls import reverse_lazy
-import requests_mock
from django_webtest import WebTest
from maykin_2fa.test import disable_admin_mfa
-from requests.exceptions import ConnectionError
from objects.accounts.tests.factories import UserFactory
from objects.token.tests.factories import ObjectTypeFactory, TokenAuthFactory
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
-
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
+from ...core.tests.factories import ObjectTypeVersionFactory
@disable_admin_mfa()
-@requests_mock.Mocker()
class AddPermissionTests(WebTest):
url = reverse_lazy("admin:token_permission_add")
@@ -24,44 +19,27 @@ def setUp(self):
user = UserFactory.create(is_superuser=True, is_staff=True)
self.app.set_user(user)
- def test_add_permission_choices_without_properties(self, m):
- object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ def test_add_permission_choices_without_properties(self):
+ object_type = ObjectTypeFactory.create()
TokenAuthFactory.create()
- # mock objecttypes api
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(f"{OBJECT_TYPES_API}objecttypes", json=[])
- m.get(object_type.url, json=mock_objecttype(object_type.url))
- version1 = mock_objecttype_version(object_type.url, attrs={"jsonSchema": {}})
- version2 = mock_objecttype_version(object_type.url, attrs={"version": 2})
- m.get(f"{object_type.url}/versions", json=[version1, version2])
+ version1 = ObjectTypeVersionFactory.create(
+ object_type=object_type, json_schema={}
+ )
+ version2 = ObjectTypeVersionFactory.create(object_type=object_type, version=2)
response = self.app.get(self.url)
self.assertEqual(response.status_code, 200)
- self.assertEqual(version1["jsonSchema"], {})
- self.assertTrue("diameter", version2["jsonSchema"]["properties"].keys())
- self.assertTrue("plantDate", version2["jsonSchema"]["properties"].keys())
+ self.assertEqual(version1.json_schema, {})
+ self.assertTrue("diameter", version2.json_schema["properties"].keys())
+ self.assertTrue("plantDate", version2.json_schema["properties"].keys())
self.assertFalse("record__data__diameter" in str(response.content))
self.assertFalse("record__data__plantDate" in str(response.content))
- def test_get_permission_with_unavailable_objecttypes(self, m):
- """
- regression test for https://github.com/maykinmedia/objects-api/issues/373
- """
- object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
- # mock objecttypes api
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(f"{OBJECT_TYPES_API}objecttypes", exc=ConnectionError)
- m.get(f"{object_type.url}/versions", exc=ConnectionError)
-
- response = self.app.get(self.url)
-
- self.assertEqual(response.status_code, 200)
-
- def test_token_auth_is_preselected_in_select(self, m):
+ def test_token_auth_is_preselected_in_select(self):
token = TokenAuthFactory.create()
url = f"{self.url}?token_auth={token.pk}"
page = self.app.get(url)
diff --git a/src/objects/tests/test_objectversion_generate.py b/src/objects/tests/test_objectversion_generate.py
new file mode 100644
index 00000000..49a7c52f
--- /dev/null
+++ b/src/objects/tests/test_objectversion_generate.py
@@ -0,0 +1,33 @@
+from django.test import TestCase
+
+from objects.core.models import ObjectTypeVersion
+from objects.core.tests.factories import ObjectTypeFactory, ObjectTypeVersionFactory
+
+JSON_SCHEMA = {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": ["diameter"],
+ "properties": {"diameter": {"type": "integer", "description": "size in cm."}},
+}
+
+
+class GenerateVersionTests(TestCase):
+ def test_generate_version_for_new_objecttype(self):
+ object_type = ObjectTypeFactory.create()
+
+ object_version = ObjectTypeVersion.objects.create(
+ json_schema=JSON_SCHEMA, object_type=object_type
+ )
+
+ self.assertEqual(object_version.version, 1)
+
+ def test_generate_version_for_objecttype_with_existed_version(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(object_type=object_type, version=1)
+
+ object_version = ObjectTypeVersion.objects.create(
+ json_schema=JSON_SCHEMA, object_type=object_type
+ )
+
+ self.assertEqual(object_version.version, 2)
diff --git a/src/objects/tests/test_widgets.py b/src/objects/tests/test_widgets.py
new file mode 100644
index 00000000..ca5f7915
--- /dev/null
+++ b/src/objects/tests/test_widgets.py
@@ -0,0 +1,44 @@
+from django.test import TestCase
+
+from bs4 import BeautifulSoup
+
+from objects.core.widgets import JSONSuit
+
+
+class JSONSuitTestCase(TestCase):
+ def test_render_valid_json_schema(self):
+ widget = JSONSuit()
+
+ widget.initial = {"foo": "bar"}
+
+ rendered = widget.render("field_name", '{"bar": "foo"}')
+ soup = BeautifulSoup(rendered, "html.parser")
+
+ textarea = soup.find("textarea")
+ self.assertEqual(
+ textarea.text.strip(), soup.find("code").attrs["data-raw"], '{"bar": "foo"}'
+ )
+
+ def test_render_invalid_json_schema_fallback(self):
+ widget = JSONSuit()
+
+ rendered = widget.render("field_name", "{}{")
+ soup = BeautifulSoup(rendered, "html.parser")
+
+ textarea = soup.find("textarea")
+ self.assertEqual(
+ textarea.text.strip(), soup.find("code").attrs["data-raw"], "{}"
+ )
+
+ def test_render_invalid_json_schema_initial(self):
+ widget = JSONSuit()
+
+ widget.initial = {"foo": "bar"}
+
+ rendered = widget.render("field_name", "{}{")
+ soup = BeautifulSoup(rendered, "html.parser")
+
+ textarea = soup.find("textarea")
+ self.assertEqual(
+ textarea.text.strip(), soup.find("code").attrs["data-raw"], '{"foo": "bar"}'
+ )
diff --git a/src/objects/tests/v2/test_auth.py b/src/objects/tests/v2/test_auth.py
index 089172c6..ad6057b2 100644
--- a/src/objects/tests/v2/test_auth.py
+++ b/src/objects/tests/v2/test_auth.py
@@ -1,26 +1,22 @@
from django.contrib.gis.geos import Point
-import requests_mock
from rest_framework import status
from rest_framework.test import APITestCase
-from objects.core.models import ObjectType
from objects.core.tests.factories import (
ObjectFactory,
ObjectRecordFactory,
ObjectTypeFactory,
- ServiceFactory,
+ ObjectTypeVersionFactory,
)
from objects.token.constants import PermissionModes
from objects.token.tests.factories import PermissionFactory, TokenAuthFactory
from objects.utils.test import TokenAuthMixin
+from ...token.models import TokenAuth
from ..constants import GEO_WRITE_KWARGS, POLYGON_AMSTERDAM_CENTRUM
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
from .utils import reverse, reverse_lazy
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class TokenAuthTests(APITestCase):
def setUp(self) -> None:
@@ -49,7 +45,7 @@ class PermissionTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
def test_retrieve_no_object_permission(self):
object = ObjectFactory.create()
@@ -170,25 +166,10 @@ def test_create_with_invalid_objecttype(self):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- def test_create_with_unknown_objecttype_service(self):
- url = reverse("object-list")
- data = {
- "type": "https://other-api.nl/v1/objecttypes/8be76be2-6567-4f5c-a17b-05217ab6d7b2",
- "record": {
- "typeVersion": 1,
- "data": {"plantDate": "2020-04-12"},
- "startDate": "2020-01-01",
- },
- }
-
- response = self.client.post(url, data, **GEO_WRITE_KWARGS)
-
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
def test_create_with_unknown_objecttype_uuid(self):
url = reverse("object-list")
data = {
- "type": f"{OBJECT_TYPES_API}objecttypes/8be76be2-6567-4f5c-a17b-05217ab6d7b2",
+ "type": "objecttypes/8be76be2-6567-4f5c-a17b-05217ab6d7b2",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12"},
@@ -206,7 +187,7 @@ class FilterAuthTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
def test_list_objects_without_object_permissions(self):
ObjectFactory.create_batch(2)
@@ -253,7 +234,7 @@ def test_search_objects_without_object_permissions(self):
"coordinates": [POLYGON_AMSTERDAM_CENTRUM],
}
},
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
**GEO_WRITE_KWARGS,
)
@@ -282,7 +263,7 @@ def test_search_objects_limited_to_object_permission(self):
"coordinates": [POLYGON_AMSTERDAM_CENTRUM],
}
},
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
**GEO_WRITE_KWARGS,
)
@@ -354,112 +335,48 @@ def test_history_superuser(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
- @requests_mock.Mocker()
- def test_create_superuser(self, m):
- object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ def test_create_superuser(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(object_type=object_type)
url = reverse("object-list")
data = {
- "type": f"{object_type.url}",
+ "type": f"https://testserver{reverse('objecttype-detail', args=[object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
"startAt": "2020-01-01",
},
}
- # mocks
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{object_type.url}/versions/1",
- json=mock_objecttype_version(object_type.url),
- )
response = self.client.post(url, data, **GEO_WRITE_KWARGS)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- def test_create_superuser_no_service(self):
- url = reverse("object-list")
- data = {
- "type": f"{OBJECT_TYPES_API}objecttypes/8be76be2-6567-4f5c-a17b-05217ab6d7b2",
- "record": {
- "typeVersion": 1,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
- "startAt": "2020-01-01",
- },
- }
-
- response = self.client.post(url, data, **GEO_WRITE_KWARGS)
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- @requests_mock.Mocker()
- def test_create_superuser_no_object_type(self, m):
- objecttype_url = (
- f"{OBJECT_TYPES_API}objecttypes/8be76be2-6567-4f5c-a17b-05217ab6d7b2"
- )
- service = ServiceFactory.create(api_root=OBJECT_TYPES_API)
- url = reverse("object-list")
- data = {
- "type": objecttype_url,
- "record": {
- "typeVersion": 1,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
- "startAt": "2020-01-01",
- },
- }
- # mocks
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(objecttype_url, json=mock_objecttype(objecttype_url))
- m.get(
- f"{objecttype_url}/versions/1",
- json=mock_objecttype_version(objecttype_url),
- )
-
- response = self.client.post(url, data, **GEO_WRITE_KWARGS)
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- # check created object type
- object_type = ObjectType.objects.get()
- self.assertEqual(object_type.service, service)
- self.assertEqual(object_type.url, objecttype_url)
-
- @requests_mock.Mocker()
- def test_update_superuser(self, m):
- object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ def test_update_superuser(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(object_type=object_type)
record = ObjectRecordFactory.create(object__object_type=object_type, version=1)
url = reverse("object-detail", args=[record.object.uuid])
data = {
- "type": f"{object_type.url}",
+ "type": f"https://testserver{reverse('objecttype-detail', args=[object_type.uuid])}",
"record": {
"typeVersion": record.version,
"data": record.data,
"startAt": record.start_at,
},
}
- # mocks
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{object_type.url}/versions/1",
- json=mock_objecttype_version(object_type.url),
- )
response = self.client.put(url, data=data, **GEO_WRITE_KWARGS)
self.assertEqual(response.status_code, status.HTTP_200_OK)
- @requests_mock.Mocker()
- def test_patch_superuser(self, m):
- object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ def test_patch_superuser(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(object_type=object_type)
record = ObjectRecordFactory.create(
object__object_type=object_type, version=1, data__name="old"
)
url = reverse("object-detail", args=[record.object.uuid])
- # mocks
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{object_type.url}/versions/1",
- json=mock_objecttype_version(object_type.url),
- )
response = self.client.patch(
url,
@@ -482,3 +399,36 @@ def test_destroy_superuser(self):
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+
+class ObjectTypeAuthTests(APITestCase):
+ def setUp(self) -> None:
+ object_type = ObjectTypeFactory.create()
+ self.urls = [
+ reverse("objecttype-list"),
+ reverse("objecttype-detail", args=[object_type.uuid]),
+ ]
+
+ def test_non_auth(self):
+ for url in self.urls:
+ with self.subTest(url=url):
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_invalid_token(self):
+ TokenAuth.objects.create(contact_person="John Smith", email="smith@bomen.nl")
+ for url in self.urls:
+ with self.subTest(url=url):
+ response = self.client.get(url, HTTP_AUTHORIZATION="Token 12345")
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_valid_token(self):
+ token_auth = TokenAuth.objects.create(
+ contact_person="John Smith", email="smith@bomen.nl"
+ )
+ for url in self.urls:
+ with self.subTest(url=url):
+ response = self.client.get(
+ url, HTTP_AUTHORIZATION=f"Token {token_auth.token}"
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
diff --git a/src/objects/tests/v2/test_auth_fields.py b/src/objects/tests/v2/test_auth_fields.py
index 38dc5351..4df3efcd 100644
--- a/src/objects/tests/v2/test_auth_fields.py
+++ b/src/objects/tests/v2/test_auth_fields.py
@@ -17,15 +17,13 @@
from ..constants import GEO_WRITE_KWARGS, POLYGON_AMSTERDAM_CENTRUM
from .utils import reverse, reverse_lazy
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class RetrieveAuthFieldsTests(TokenAuthMixin, APITestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
def test_retrieve_without_query(self):
PermissionFactory.create(
@@ -48,7 +46,7 @@ def test_retrieve_without_query(self):
response.json(),
{
"url": f"http://testserver{url}",
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {"startAt": record.start_at.isoformat()},
},
)
@@ -88,7 +86,7 @@ def test_retrieve_with_query_fields(self):
response.json(),
{
"url": f"http://testserver{url}",
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {"data": {"name": record.data["name"]}},
},
)
@@ -136,7 +134,7 @@ def test_retrieve_query_fields_not_allowed(self):
{
"url": f"http://testserver{url}",
"record": {"data": {"name": "some"}},
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
)
self.assertEqual(
@@ -171,7 +169,7 @@ class ListAuthFieldsTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
cls.other_object_type = ObjectTypeFactory.create()
def test_list_without_query_different_object_types(self):
@@ -222,7 +220,7 @@ def test_list_without_query_different_object_types(self):
},
{
"url": f"http://testserver{reverse('object-detail', args=[record1.object.uuid])}",
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"index": record1.index,
"typeVersion": record1.version,
@@ -240,7 +238,7 @@ def test_list_without_query_different_object_types(self):
)
self.assertEqual(
response.headers["x-unauthorized-fields"],
- f"{self.other_object_type.url}(1)=type; {self.object_type.url}(1)=uuid",
+ f"http://testserver{reverse('objecttype-detail', args=[self.other_object_type.uuid])}(1)=type; http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}(1)=uuid",
)
def test_list_with_query_fields(self):
@@ -391,7 +389,7 @@ class SearchAuthFieldsTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
def test_search_with_fields_auth(self):
PermissionFactory.create(
@@ -426,7 +424,7 @@ def test_search_with_fields_auth(self):
[
{
"url": f"http://testserver{reverse('object-detail', args=[record.object.uuid])}",
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"geometry": {
"type": "Point",
diff --git a/src/objects/tests/v2/test_filters.py b/src/objects/tests/v2/test_filters.py
index 61f59faa..fc820f13 100644
--- a/src/objects/tests/v2/test_filters.py
+++ b/src/objects/tests/v2/test_filters.py
@@ -17,10 +17,9 @@
from objects.token.tests.factories import PermissionFactory
from objects.utils.test import TokenAuthMixin
+from ...core.constants import DataClassificationChoices
from .utils import reverse, reverse_lazy
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class FilterObjectTypeTests(TokenAuthMixin, APITestCase):
url = reverse_lazy("object-list")
@@ -29,10 +28,8 @@ class FilterObjectTypeTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
- cls.another_object_type = ObjectTypeFactory.create(
- service=cls.object_type.service
- )
+ cls.object_type = ObjectTypeFactory.create()
+ cls.another_object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
@@ -50,7 +47,12 @@ def test_filter_object_type(self):
ObjectRecordFactory.create(object=object)
ObjectFactory.create(object_type=self.another_object_type)
- response = self.client.get(self.url, {"type": self.object_type.url})
+ response = self.client.get(
+ self.url,
+ {
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
+ },
+ )
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -68,27 +70,6 @@ def test_filter_invalid_objecttype(self):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json()["type"], ["Invalid value."])
- def test_filter_unknown_objecttype(self):
- objecttype_url = (
- f"{OBJECT_TYPES_API}objecttypes/8be76be2-6567-4f5c-a17b-05217ab6d7b2"
- )
- response = self.client.get(self.url, {"type": objecttype_url})
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(
- response.json()["type"],
- [
- f"Select a valid object type. {objecttype_url} is not one of the available choices."
- ],
- )
-
- def test_filter_too_long_object_type(self):
- object_type_long = f"{OBJECT_TYPES_API}{'a' * 1000}/{self.object_type.uuid}"
- response = self.client.get(self.url, {"type": object_type_long})
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(response.json()["type"], ["The value has too many characters"])
-
class FilterDataAttrsTests(TokenAuthMixin, APITestCase):
url = reverse_lazy("object-list")
@@ -97,7 +78,7 @@ class FilterDataAttrsTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_only,
@@ -445,7 +426,7 @@ class FilterDataAttrTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_only,
@@ -838,7 +819,7 @@ class FilterDateTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_only,
@@ -973,7 +954,7 @@ class FilterDataIcontainsTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_only,
@@ -1051,7 +1032,7 @@ class FilterTypeVersionTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_only,
@@ -1084,3 +1065,27 @@ def test_filter_unkown_version(self):
data = response.json()["results"]
self.assertEqual(len(data), 0)
+
+
+class ObjectTypeFilterTests(TokenAuthMixin, APITestCase):
+ url = reverse_lazy("objecttype-list")
+
+ def test_filter_public_data(self):
+ object_type_1 = ObjectTypeFactory.create(
+ data_classification=DataClassificationChoices.open
+ )
+ ObjectTypeFactory.create(data_classification=DataClassificationChoices.intern)
+
+ response = self.client.get(
+ self.url, {"dataClassification": DataClassificationChoices.open}
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ data = response.json()["results"]
+
+ self.assertEqual(len(data), 1)
+ self.assertEqual(
+ data[0]["url"],
+ f"http://testserver{reverse('objecttype-detail', args=[object_type_1.uuid])}",
+ )
diff --git a/src/objects/tests/v2/test_geo_headers.py b/src/objects/tests/v2/test_geo_headers.py
index f481648a..24c219e6 100644
--- a/src/objects/tests/v2/test_geo_headers.py
+++ b/src/objects/tests/v2/test_geo_headers.py
@@ -13,15 +13,13 @@
from ..constants import GEO_READ_KWARGS, POLYGON_AMSTERDAM_CENTRUM
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class GeoHeaderTests(TokenAuthMixin, APITestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_and_write,
@@ -62,7 +60,7 @@ def test_get_with_incorrect_get_headers(self):
def test_create_without_geo_headers(self):
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"diameter": 30},
@@ -79,7 +77,7 @@ def test_update_without_geo_headers(self):
object = ObjectFactory.create(object_type=self.object_type)
url = reverse("object-detail", args=[object.uuid])
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"diameter": 30},
diff --git a/src/objects/tests/v2/test_geo_search.py b/src/objects/tests/v2/test_geo_search.py
index d375ec96..71e13496 100644
--- a/src/objects/tests/v2/test_geo_search.py
+++ b/src/objects/tests/v2/test_geo_search.py
@@ -11,8 +11,6 @@
from ..constants import GEO_WRITE_KWARGS, POLYGON_AMSTERDAM_CENTRUM
from .utils import reverse, reverse_lazy
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class GeoSearchTests(TokenAuthMixin, APITestCase):
url = reverse_lazy("object-search")
@@ -21,10 +19,8 @@ class GeoSearchTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
- cls.another_object_type = ObjectTypeFactory.create(
- service=cls.object_type.service
- )
+ cls.object_type = ObjectTypeFactory.create()
+ cls.another_object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
@@ -90,7 +86,7 @@ def test_filter_objecttype(self):
"coordinates": [POLYGON_AMSTERDAM_CENTRUM],
}
},
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
**GEO_WRITE_KWARGS,
)
@@ -110,7 +106,9 @@ def test_without_geometry(self):
ObjectRecordFactory.create(object__object_type=self.another_object_type)
response = self.client.post(
self.url,
- {"type": self.object_type.url},
+ {
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}"
+ },
**GEO_WRITE_KWARGS,
)
diff --git a/src/objects/tests/v2/test_jsonschema.py b/src/objects/tests/v2/test_jsonschema.py
index d26e8d31..2e4388ee 100644
--- a/src/objects/tests/v2/test_jsonschema.py
+++ b/src/objects/tests/v2/test_jsonschema.py
@@ -1,20 +1,15 @@
-import requests_mock
from rest_framework import status
from rest_framework.test import APITestCase
-from objects.core.tests.factories import ObjectTypeFactory
+from objects.core.tests.factories import ObjectTypeFactory, ObjectTypeVersionFactory
from objects.token.constants import PermissionModes
from objects.token.tests.factories import PermissionFactory
from objects.utils.test import ClearCachesMixin, TokenAuthMixin
from ..constants import GEO_WRITE_KWARGS
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
-@requests_mock.Mocker()
class JsonSchemaTests(TokenAuthMixin, ClearCachesMixin, APITestCase):
"""GH issue - https://github.com/maykinmedia/objects-api/issues/330"""
@@ -22,25 +17,21 @@ class JsonSchemaTests(TokenAuthMixin, ClearCachesMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_and_write,
token_auth=cls.token_auth,
)
- def test_create_object_with_additional_properties_allowed(self, m):
- object_type_data = mock_objecttype_version(self.object_type.url)
- object_type_data["jsonSchema"]["additionalProperties"] = True
-
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
-
- m.get(f"{self.object_type.url}/versions/1", json=object_type_data)
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
+ def test_create_object_with_additional_properties_allowed(self):
+ object_type_data = ObjectTypeVersionFactory.create(object_type=self.object_type)
+ object_type_data.json_schema["additionalProperties"] = True
+ object_type_data.save()
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"diameter": 30, "newProperty": "some value"},
@@ -52,18 +43,14 @@ def test_create_object_with_additional_properties_allowed(self, m):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- def test_create_object_with_additional_properties_not_allowed(self, m):
- object_type_data = mock_objecttype_version(self.object_type.url)
- object_type_data["jsonSchema"]["additionalProperties"] = False
-
- # mocks
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(f"{self.object_type.url}/versions/1", json=object_type_data)
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
+ def test_create_object_with_additional_properties_not_allowed(self):
+ object_type_data = ObjectTypeVersionFactory.create(object_type=self.object_type)
+ object_type_data.json_schema["additionalProperties"] = False
+ object_type_data.save()
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"diameter": 30, "newProperty": "some value"},
diff --git a/src/objects/tests/v2/test_metrics.py b/src/objects/tests/v2/test_metrics.py
index e70c1239..44722df2 100644
--- a/src/objects/tests/v2/test_metrics.py
+++ b/src/objects/tests/v2/test_metrics.py
@@ -1,36 +1,37 @@
from unittest.mock import MagicMock, patch
-import requests_mock
from freezegun import freeze_time
+from rest_framework import status
from rest_framework.test import APITestCase
from objects.api.metrics import (
objects_create_counter,
objects_delete_counter,
objects_update_counter,
+ objecttype_create_counter,
+ objecttype_delete_counter,
+ objecttype_update_counter,
)
from objects.core.tests.factories import (
ObjectFactory,
ObjectRecordFactory,
ObjectTypeFactory,
+ ObjectTypeVersionFactory,
)
from objects.token.constants import PermissionModes
from objects.token.tests.factories import PermissionFactory
from objects.utils.test import TokenAuthMixin
from ..constants import GEO_WRITE_KWARGS
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
@freeze_time("2024-08-31")
class ObjectMetricsTests(TokenAuthMixin, APITestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_and_write,
@@ -47,19 +48,13 @@ def create_object_with_record(self, diameter: int = 10):
)
return obj
- @requests_mock.Mocker()
@patch.object(objects_create_counter, "add", wraps=objects_create_counter.add)
- def test_objects_create_counter(self, m, mock_add: MagicMock):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
+ def test_objects_create_counter(self, mock_add: MagicMock):
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"diameter": 10},
@@ -70,15 +65,9 @@ def test_objects_create_counter(self, m, mock_add: MagicMock):
self.assertEqual(response.status_code, 201)
mock_add.assert_called_once_with(1)
- @requests_mock.Mocker()
@patch.object(objects_update_counter, "add", wraps=objects_update_counter.add)
- def test_objects_update_counter(self, m, mock_add: MagicMock):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
+ def test_objects_update_counter(self, mock_add: MagicMock):
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
obj = self.create_object_with_record()
url = reverse("object-detail", args=[obj.uuid])
@@ -93,15 +82,9 @@ def test_objects_update_counter(self, m, mock_add: MagicMock):
self.assertEqual(response.status_code, 200)
mock_add.assert_called_once_with(1)
- @requests_mock.Mocker()
@patch.object(objects_delete_counter, "add", wraps=objects_delete_counter.add)
- def test_objects_delete_counter(self, m, mock_add: MagicMock):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
+ def test_objects_delete_counter(self, mock_add: MagicMock):
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
obj = self.create_object_with_record()
url = reverse("object-detail", args=[obj.uuid])
@@ -109,3 +92,41 @@ def test_objects_delete_counter(self, m, mock_add: MagicMock):
self.assertEqual(response.status_code, 204)
mock_add.assert_called_once_with(1)
+
+
+@freeze_time("2020-01-01")
+class ObjectTypeMetricTests(TokenAuthMixin, APITestCase):
+ @patch.object(objecttype_create_counter, "add", wraps=objecttype_create_counter.add)
+ def test_objecttype_create_counter(self, mock_add: MagicMock):
+ url = reverse("objecttype-list")
+
+ response = self.client.post(
+ url,
+ {
+ "name": "Boom",
+ "namePlural": "Bomen",
+ },
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ mock_add.assert_called_once_with(1)
+
+ @patch.object(objecttype_update_counter, "add", wraps=objecttype_update_counter.add)
+ def test_objecttype_update_counter(self, mock_add: MagicMock):
+ obj = ObjectTypeFactory.create()
+ url = reverse("objecttype-detail", args=[obj.uuid])
+
+ response = self.client.patch(url, {"name": "test"})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ mock_add.assert_called_once_with(1)
+
+ @patch.object(objecttype_delete_counter, "add", wraps=objecttype_delete_counter.add)
+ def test_objecttype_delete_counter(self, mock_add: MagicMock):
+ obj = ObjectTypeFactory.create()
+ url = reverse("objecttype-detail", args=[obj.uuid])
+
+ response = self.client.delete(url)
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ mock_add.assert_called_once_with(1)
diff --git a/src/objects/tests/v2/test_notifications_send.py b/src/objects/tests/v2/test_notifications_send.py
index 9e8d9c02..6ce035a0 100644
--- a/src/objects/tests/v2/test_notifications_send.py
+++ b/src/objects/tests/v2/test_notifications_send.py
@@ -3,7 +3,6 @@
from django.test import override_settings
-import requests_mock
from freezegun import freeze_time
from notifications_api_common.models import NotificationsConfig
from rest_framework import status
@@ -17,6 +16,7 @@
ObjectFactory,
ObjectRecordFactory,
ObjectTypeFactory,
+ ObjectTypeVersionFactory,
ReferenceFactory,
)
from objects.token.constants import PermissionModes
@@ -24,21 +24,19 @@
from objects.utils.test import TokenAuthMixin
from ..constants import GEO_WRITE_KWARGS
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
@freeze_time("2018-09-07T00:00:00Z")
@override_settings(NOTIFICATIONS_DISABLED=False)
-@requests_mock.Mocker()
class SendNotifTestCase(TokenAuthMixin, APITestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(object_type=cls.object_type)
+
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_and_write,
@@ -63,20 +61,14 @@ def setUp(self):
config.save()
@patch("notifications_api_common.viewsets.send_notification.delay")
- def test_send_notif_create_object(self, mocker, mock_task):
+ def test_send_notif_create_object(self, mock_task):
"""
Check if notifications will be send when Object is created
"""
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -104,29 +96,23 @@ def test_send_notif_create_object(self, mocker, mock_task):
"actie": "create",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
},
)
@patch("notifications_api_common.viewsets.send_notification.delay")
- def test_send_notif_update_object(self, mocker, mock_task):
+ def test_send_notif_update_object(self, mock_task):
"""
Check if notifications will be send when Object is created
"""
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
obj = ObjectFactory.create(object_type=self.object_type)
ObjectRecordFactory.create(object=obj)
url = reverse("object-detail", args=[obj.uuid])
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -154,29 +140,23 @@ def test_send_notif_update_object(self, mocker, mock_task):
"actie": "update",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
},
)
@patch("notifications_api_common.viewsets.send_notification.delay")
- def test_send_notif_partial_update_object(self, mocker, mock_task):
+ def test_send_notif_partial_update_object(self, mock_task):
"""
Check if notifications will be send when Object is created
"""
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
obj = ObjectFactory.create(object_type=self.object_type)
ObjectRecordFactory.create(object=obj)
url = reverse("object-detail", args=[obj.uuid])
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -204,21 +184,16 @@ def test_send_notif_partial_update_object(self, mocker, mock_task):
"actie": "partial_update",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
},
)
@patch("notifications_api_common.viewsets.send_notification.delay")
- def test_send_notif_delete_object(self, mocker, mock_task):
+ def test_send_notif_delete_object(self, mock_task):
"""
Check if notifications will be send when Object is created
"""
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
obj = ObjectFactory.create(object_type=self.object_type)
ObjectRecordFactory.create(object=obj)
@@ -241,7 +216,7 @@ def test_send_notif_delete_object(self, mocker, mock_task):
"actie": "destroy",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
},
)
@@ -253,17 +228,10 @@ def test_send_notif_delete_object(self, mocker, mock_task):
ENABLE_CLOUD_EVENTS=True,
NOTIFICATIONS_SOURCE="objects-api-test",
)
- def test_send_cloudevent_adding_zaak(self, mocker, mock_notification, mock_event):
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_send_cloudevent_adding_zaak(self, mock_notification, mock_event):
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -309,7 +277,7 @@ def test_send_cloudevent_adding_zaak(self, mocker, mock_notification, mock_event
"actie": "create",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
},
)
@@ -321,16 +289,7 @@ def test_send_cloudevent_adding_zaak(self, mocker, mock_notification, mock_event
ENABLE_CLOUD_EVENTS=True,
NOTIFICATIONS_SOURCE="objects-api-test",
)
- def test_send_cloudevents_changing_zaak(
- self, mocker, mock_notification, mock_event
- ):
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_send_cloudevents_changing_zaak(self, mock_notification, mock_event):
obj = ObjectFactory.create(object_type=self.object_type)
ref = ReferenceFactory.create(
type="zaak", url="https://example.com/zaak/1", record__object=obj
@@ -342,7 +301,7 @@ def test_send_cloudevents_changing_zaak(
url = reverse("object-detail", args=[obj.uuid])
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -406,7 +365,7 @@ def test_send_cloudevents_changing_zaak(
"actie": "partial_update",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
"source": "objects-api-test",
},
@@ -419,16 +378,7 @@ def test_send_cloudevents_changing_zaak(
ENABLE_CLOUD_EVENTS=True,
NOTIFICATIONS_SOURCE="objects-api-test",
)
- def test_send_cloudevents_deleting_object(
- self, mocker, mock_notification, mock_event
- ):
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_send_cloudevents_deleting_object(self, mock_notification, mock_event):
obj = ObjectFactory.create(object_type=self.object_type)
ref = ReferenceFactory.create(
type="zaak", url="https://example.com/zaak/1", record__object=obj
@@ -472,7 +422,7 @@ def test_send_cloudevents_deleting_object(
"actie": "destroy",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
"source": "objects-api-test",
},
@@ -486,15 +436,9 @@ def test_send_cloudevents_deleting_object(
NOTIFICATIONS_SOURCE="objects-api-test",
)
def test_send_cloudevents_deleting_object_archiving_only_zaak(
- self, mocker, mock_notification, mock_event
+ self, mock_notification, mock_event
):
"Open Archiefbeheer DELETEs with zaak queryparam when archiving"
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
zaak_1 = "https://example.com/zaak/1"
@@ -537,7 +481,7 @@ def test_send_cloudevents_deleting_object_archiving_only_zaak(
"actie": "destroy",
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
"source": "objects-api-test",
},
@@ -551,15 +495,9 @@ def test_send_cloudevents_deleting_object_archiving_only_zaak(
NOTIFICATIONS_SOURCE="objects-api-test",
)
def test_send_cloudevents_deleting_object_archiving_a_zaak(
- self, mocker, mock_notification, mock_event
+ self, mock_notification, mock_event
):
"Open Archiefbeheer DELETEs with zaak queryparam when archiving"
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
zaak_1 = "https://example.com/zaak/1"
@@ -604,7 +542,7 @@ def test_send_cloudevents_deleting_object_archiving_a_zaak(
"actie": "update", # wasn't destroyed, but changed!
"aanmaakdatum": "2018-09-07T02:00:00+02:00",
"kenmerken": {
- "objectType": self.object_type.url,
+ "objectType": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
},
"source": "objects-api-test",
},
@@ -616,19 +554,10 @@ def test_send_cloudevents_deleting_object_archiving_a_zaak(
)
@patch("notifications_api_common.tasks.send_cloudevent.delay")
@patch("notifications_api_common.viewsets.send_notification.delay")
- def test_no_notifications_sent_when_disabled(
- self, mocker, mock_notification, mock_events
- ):
- mock_service_oas_get(mocker, OBJECT_TYPES_API, "objecttypes")
- mocker.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- mocker.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_no_notifications_sent_when_disabled(self, mock_notification, mock_events):
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
diff --git a/src/objects/tests/v2/test_object_api.py b/src/objects/tests/v2/test_object_api.py
index 7f1bd279..eca2a78d 100644
--- a/src/objects/tests/v2/test_object_api.py
+++ b/src/objects/tests/v2/test_object_api.py
@@ -2,7 +2,6 @@
import uuid
from datetime import date, timedelta
-import requests_mock
from freezegun import freeze_time
from rest_framework import status
from rest_framework.test import APITestCase
@@ -12,6 +11,7 @@
ObjectFactory,
ObjectRecordFactory,
ObjectTypeFactory,
+ ObjectTypeVersionFactory,
ReferenceFactory,
)
from objects.token.constants import PermissionModes
@@ -19,14 +19,10 @@
from objects.utils.test import TokenAuthMixin
from ..constants import GEO_WRITE_KWARGS
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
from .utils import reverse, reverse_lazy
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
@freeze_time("2020-08-08")
-@requests_mock.Mocker()
class ObjectApiTests(TokenAuthMixin, APITestCase):
maxDiff = None
@@ -34,14 +30,18 @@ class ObjectApiTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
+ cls.objecttype_version = ObjectTypeVersionFactory.create(
+ object_type=cls.object_type,
+ )
+
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_and_write,
token_auth=cls.token_auth,
)
- def test_list_actual_objects(self, m):
+ def test_list_actual_objects(self):
object_record1 = ObjectRecordFactory.create(
object__object_type=self.object_type,
start_at=date.today(),
@@ -69,7 +69,7 @@ def test_list_actual_objects(self, m):
{
"url": f"http://testserver{reverse('object-detail', args=[object_record1.object.uuid])}",
"uuid": str(object_record1.object.uuid),
- "type": object_record1.object.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[object_record1.object.object_type.uuid])}",
"record": {
"index": object_record1.index,
"typeVersion": object_record1.version,
@@ -87,7 +87,7 @@ def test_list_actual_objects(self, m):
},
)
- def test_retrieve_object(self, m):
+ def test_retrieve_object(self):
object = ObjectFactory.create(object_type=self.object_type)
object_record = ObjectRecordFactory.create(
object=object,
@@ -107,7 +107,7 @@ def test_retrieve_object(self, m):
{
"url": f"http://testserver{reverse('object-detail', args=[object.uuid])}",
"uuid": str(object.uuid),
- "type": object.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"index": object_record.index,
"typeVersion": object_record.version,
@@ -123,7 +123,7 @@ def test_retrieve_object(self, m):
},
)
- def test_retrieve_by_index(self, m):
+ def test_retrieve_by_index(self):
record1 = ObjectRecordFactory.create(
object__object_type=self.object_type,
start_at=date(2020, 1, 1),
@@ -185,17 +185,10 @@ def test_retrieve_by_index(self, m):
},
)
- def test_create_object(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_create_object(self):
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -209,7 +202,7 @@ def test_create_object(self, m):
response = self.client.post(url, data, **GEO_WRITE_KWARGS)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
object = Object.objects.get()
@@ -224,14 +217,7 @@ def test_create_object(self, m):
self.assertEqual(record.geometry.coords, (4.910649523925713, 52.37240093589432))
self.assertIsNone(record.end_at)
- def test_update_object(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_update_object(self):
# other object - to check that correction works when there is another record with the same index
ObjectRecordFactory.create(object__object_type=self.object_type)
initial_record = ObjectRecordFactory.create(
@@ -243,7 +229,7 @@ def test_update_object(self, m):
url = reverse("object-detail", args=[object.uuid])
data = {
- "type": object.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -284,13 +270,7 @@ def test_update_object(self, m):
self.assertEqual(initial_record.corrected, current_record)
self.assertEqual(initial_record.end_at, date(2020, 1, 1))
- def test_patch_object_record(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
-
+ def test_patch_object_record(self):
initial_record = ObjectRecordFactory.create(
version=1,
object__object_type=self.object_type,
@@ -310,7 +290,7 @@ def test_patch_object_record(self, m):
response = self.client.patch(url, data, **GEO_WRITE_KWARGS)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
initial_record.refresh_from_db()
@@ -333,13 +313,7 @@ def test_patch_object_record(self, m):
self.assertEqual(initial_record.corrected, current_record)
self.assertEqual(initial_record.end_at, date(2020, 1, 1))
- def test_patch_validates_merged_object_rather_than_partial_object(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
-
+ def test_patch_validates_merged_object_rather_than_partial_object(self):
initial_record = ObjectRecordFactory.create(
version=1,
object__object_type=self.object_type,
@@ -360,7 +334,7 @@ def test_patch_validates_merged_object_rather_than_partial_object(self, m):
}
response = self.client.patch(url, data, **GEO_WRITE_KWARGS)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
self.assertEqual(
response.json()["record"]["data"],
{"plantDate": "2024-10-09", "diameter": 20, "name": "Name"},
@@ -372,7 +346,7 @@ def test_patch_validates_merged_object_rather_than_partial_object(self, m):
{"plantDate": "2024-10-09", "diameter": 20, "name": "Name"},
)
- def test_delete_object(self, m):
+ def test_delete_object(self):
record = ObjectRecordFactory.create(object__object_type=self.object_type)
object = record.object
url = reverse("object-detail", args=[object.uuid])
@@ -382,7 +356,7 @@ def test_delete_object(self, m):
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Object.objects.count(), 0)
- def test_history_object(self, m):
+ def test_history_object(self):
record1 = ObjectRecordFactory.create(
object__object_type=self.object_type,
start_at=date(2020, 1, 1),
@@ -435,20 +409,13 @@ def test_history_object(self, m):
# In the ticket https://github.com/maykinmedia/objects-api/issues/282 we discovered that updating an object \
# where the startAt value has been modified with an earlier date causes an 500 response.
- def test_updating_object_after_changing_the_startAt_value_returns_200(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_updating_object_after_changing_the_startAt_value_returns_200(self):
object_uuid = uuid.uuid4()
url_object_list = reverse("object-list")
start_data = {
"uuid": object_uuid,
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -468,7 +435,7 @@ def test_updating_object_after_changing_the_startAt_value_returns_200(self, m):
url_object_update = reverse("object-detail", args=[object_uuid])
modified_data = {
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -496,14 +463,7 @@ def test_updating_object_after_changing_the_startAt_value_returns_200(self, m):
)
# regression test for https://github.com/maykinmedia/objects-api/issues/268
- def test_update_object_correctionFor(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_update_object_correctionFor(self):
initial_record = ObjectRecordFactory.create(
object__object_type=self.object_type, version=1
)
@@ -513,7 +473,7 @@ def test_update_object_correctionFor(self, m):
url = reverse("object-detail", args=[object.uuid])
modified_data = {
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -532,17 +492,10 @@ def test_update_object_correctionFor(self, m):
last_record = object.last_record
self.assertIsNone(last_record.correct)
- def test_create_object_with_references(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_create_object_with_references(self):
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -562,14 +515,7 @@ def test_create_object_with_references(self, m):
{("zaak", "https://example.com/zaak/1")},
)
- def test_update_object_with_references(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_update_object_with_references(self):
# other object - to check that correction works when there is another record with the same index
ObjectRecordFactory.create(object__object_type=self.object_type)
initial_record = ObjectRecordFactory.create(
@@ -581,7 +527,7 @@ def test_update_object_with_references(self, m):
url = reverse("object-detail", args=[object.uuid])
data = {
- "type": object.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -621,16 +567,10 @@ def test_update_object_with_references(self, m):
self.assertEqual(initial_record.corrected, current_record)
self.assertEqual(initial_record.end_at, date(2020, 1, 1))
- def test_patch_object_record_with_references(self, m):
+ def test_patch_object_record_with_references(self):
# NOTE: An almost standard JSON Merge PATCH algorithm is applied,
# but *only* on record.data, not on the record itself!
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
-
initial_record = ObjectRecordFactory.create(
version=1,
object__object_type=self.object_type,
@@ -680,14 +620,8 @@ def test_patch_object_record_with_references(self, m):
self.assertEqual(initial_record.end_at, date(2020, 1, 1))
def test_patch_validates_merged_object_rather_than_partial_object_with_references(
- self, m
+ self,
):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
-
initial_record = ObjectRecordFactory.create(
version=1,
object__object_type=self.object_type,
@@ -724,7 +658,7 @@ def test_patch_validates_merged_object_rather_than_partial_object_with_reference
{"plantDate": "2020-04-10", "diameter": 20, "name": "Name"},
)
- def test_delete_object_with_references(self, m):
+ def test_delete_object_with_references(self):
record = ObjectRecordFactory.create(object__object_type=self.object_type)
ReferenceFactory.create_batch(2, record=record)
object = record.object
@@ -758,7 +692,7 @@ class ObjectsAvailableRecordsTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_and_write,
diff --git a/src/objects/tests/v2/test_object_api_fields.py b/src/objects/tests/v2/test_object_api_fields.py
index e6addd58..f849e05e 100644
--- a/src/objects/tests/v2/test_object_api_fields.py
+++ b/src/objects/tests/v2/test_object_api_fields.py
@@ -14,8 +14,6 @@
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class DynamicFieldsTests(TokenAuthMixin, APITestCase):
maxDiff = None
@@ -24,7 +22,7 @@ class DynamicFieldsTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_and_write,
@@ -53,12 +51,12 @@ def test_list_with_selected_fields(self):
[
{
"url": f"http://testserver{reverse('object-detail', args=[object_record2.object.uuid])}",
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {"index": 1, "typeVersion": object_record2.version},
},
{
"url": f"http://testserver{reverse('object-detail', args=[object_record1.object.uuid])}",
- "type": self.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {"index": 1, "typeVersion": object_record1.version},
},
],
@@ -83,7 +81,7 @@ def test_retrieve_with_selected_fields(self):
data,
{
"url": f"http://testserver{reverse('object-detail', args=[object.uuid])}",
- "type": object.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[object.object_type.uuid])}",
"record": {
"geometry": {
"type": "Point",
diff --git a/src/objects/tests/v2/test_objecttype_api.py b/src/objects/tests/v2/test_objecttype_api.py
new file mode 100644
index 00000000..5db49115
--- /dev/null
+++ b/src/objects/tests/v2/test_objecttype_api.py
@@ -0,0 +1,180 @@
+from datetime import date
+
+from freezegun import freeze_time
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from objects.core.constants import DataClassificationChoices, UpdateFrequencyChoices
+from objects.core.models import ObjectType
+from objects.core.tests.factories import ObjectTypeFactory, ObjectTypeVersionFactory
+from objects.utils.test import TokenAuthMixin
+
+from .utils import reverse
+
+
+@freeze_time("2020-01-01")
+class ObjectTypeAPITests(TokenAuthMixin, APITestCase):
+ def test_get_objecttypes(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(object_type=object_type)
+ url = reverse("objecttype-detail", args=[object_type.uuid])
+
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ response.json(),
+ {
+ "url": f"http://testserver{url}",
+ "uuid": str(object_type.uuid),
+ "name": object_type.name,
+ "namePlural": object_type.name_plural,
+ "description": object_type.description,
+ "dataClassification": object_type.data_classification,
+ "maintainerOrganization": object_type.maintainer_organization,
+ "maintainerDepartment": object_type.maintainer_department,
+ "contactPerson": object_type.contact_person,
+ "contactEmail": object_type.contact_email,
+ "source": object_type.source,
+ "updateFrequency": object_type.update_frequency,
+ "providerOrganization": object_type.provider_organization,
+ "documentationUrl": object_type.documentation_url,
+ "labels": object_type.labels,
+ "createdAt": "2020-01-01",
+ "modifiedAt": "2020-01-01",
+ "allowGeometry": object_type.allow_geometry,
+ "versions": [
+ "http://testserver{url}".format(
+ url=reverse(
+ "objecttypeversion-detail",
+ args=[object_type.uuid, object_version.version],
+ )
+ ),
+ ],
+ },
+ )
+
+ def test_get_objecttypes_with_versions(self):
+ object_types = ObjectTypeFactory.create_batch(2)
+ object_versions = [
+ ObjectTypeVersionFactory.create(object_type=object_type)
+ for object_type in object_types
+ ]
+ for i, object_type in enumerate(object_types):
+ with self.subTest(object_type=object_type):
+ url = reverse("objecttype-detail", args=[object_type.uuid])
+
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ data = response.json()
+ self.assertEqual(len(data["versions"]), 1)
+ self.assertEqual(
+ data["versions"],
+ [
+ "http://testserver{url}".format(
+ url=reverse(
+ "objecttypeversion-detail",
+ args=[object_type.uuid, object_versions[i].version],
+ )
+ ),
+ ],
+ )
+
+ def test_create_objecttype(self):
+ url = reverse("objecttype-list")
+ data = {
+ "name": "boom",
+ "namePlural": "bomen",
+ "description": "tree type description",
+ "dataClassification": DataClassificationChoices.intern,
+ "maintainerOrganization": "tree municipality",
+ "maintainerDepartment": "object types department",
+ "contactPerson": "John Smith",
+ "contactEmail": "John.Smith@objecttypes.nl",
+ "source": "tree system",
+ "updateFrequency": UpdateFrequencyChoices.monthly,
+ "providerOrganization": "tree provider",
+ "documentationUrl": "http://example.com/doc/trees",
+ "labels": {"key1": "value1"},
+ }
+
+ response = self.client.post(url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(ObjectType.objects.count(), 1)
+
+ object_type = ObjectType.objects.get()
+
+ self.assertEqual(object_type.name, "boom")
+ self.assertEqual(object_type.name_plural, "bomen")
+ self.assertEqual(object_type.description, "tree type description")
+ self.assertEqual(
+ object_type.data_classification, DataClassificationChoices.intern
+ )
+ self.assertEqual(object_type.maintainer_organization, "tree municipality")
+ self.assertEqual(object_type.maintainer_department, "object types department")
+ self.assertEqual(object_type.contact_person, "John Smith")
+ self.assertEqual(object_type.contact_email, "John.Smith@objecttypes.nl")
+ self.assertEqual(object_type.source, "tree system")
+ self.assertEqual(object_type.update_frequency, UpdateFrequencyChoices.monthly)
+ self.assertEqual(object_type.provider_organization, "tree provider")
+ self.assertEqual(object_type.documentation_url, "http://example.com/doc/trees")
+ self.assertEqual(object_type.labels, {"key1": "value1"})
+ self.assertEqual(object_type.created_at, date(2020, 1, 1))
+ self.assertEqual(object_type.modified_at, date(2020, 1, 1))
+
+ def test_create_objecttype_with_duplicate_uuid(self):
+ object_type = ObjectTypeFactory.create()
+
+ url = reverse("objecttype-list")
+ data = {
+ "uuid": object_type.uuid,
+ "name": "boom",
+ "namePlural": "bomen",
+ "description": "tree type description",
+ "dataClassification": DataClassificationChoices.intern,
+ "maintainerOrganization": "tree municipality",
+ "maintainerDepartment": "object types department",
+ "contactPerson": "John Smith",
+ "contactEmail": "John.Smith@objecttypes.nl",
+ "source": "tree system",
+ "updateFrequency": UpdateFrequencyChoices.monthly,
+ "providerOrganization": "tree provider",
+ "documentationUrl": "http://example.com/doc/trees",
+ "labels": {"key1": "value1"},
+ }
+
+ response = self.client.post(url, data)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_update_objecttype(self):
+ object_type = ObjectTypeFactory.create(
+ data_classification=DataClassificationChoices.intern
+ )
+ url = reverse("objecttype-detail", args=[object_type.uuid])
+
+ response = self.client.patch(
+ url,
+ {
+ "dataClassification": DataClassificationChoices.open,
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ object_type.refresh_from_db()
+
+ self.assertEqual(
+ object_type.data_classification, DataClassificationChoices.open
+ )
+
+ def test_delete_objecttype(self):
+ object_type = ObjectTypeFactory.create()
+ url = reverse("objecttype-detail", args=[object_type.uuid])
+
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+ self.assertEqual(ObjectType.objects.count(), 0)
diff --git a/src/objects/tests/v2/test_objecttypeversion_api.py b/src/objects/tests/v2/test_objecttypeversion_api.py
new file mode 100644
index 00000000..23348e43
--- /dev/null
+++ b/src/objects/tests/v2/test_objecttypeversion_api.py
@@ -0,0 +1,141 @@
+from datetime import date
+
+from freezegun import freeze_time
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from objects.core.constants import ObjectTypeVersionStatus
+from objects.core.models import ObjectTypeVersion
+from objects.core.tests.factories import ObjectTypeFactory, ObjectTypeVersionFactory
+from objects.utils.test import TokenAuthMixin
+
+from .utils import reverse
+
+JSON_SCHEMA = {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": ["diameter"],
+ "properties": {
+ "diameter": {"type": "integer", "description": "size in cm."},
+ "plantDate": {
+ "type": "string",
+ "format": "date",
+ "description": "Date the tree was planted.",
+ },
+ },
+}
+
+
+@freeze_time("2020-01-01")
+class ObjectTypeVersionAPITests(TokenAuthMixin, APITestCase):
+ def test_get_versions(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(object_type=object_type)
+ url = reverse("objecttypeversion-list", args=[object_type.uuid])
+
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ response.json(),
+ {
+ "count": 1,
+ "next": None,
+ "previous": None,
+ "results": [
+ {
+ "url": "http://testserver{url}".format(
+ url=reverse(
+ "objecttypeversion-detail",
+ args=[object_type.uuid, object_version.version],
+ ),
+ ),
+ "version": object_version.version,
+ "objectType": "http://testserver{url}".format(
+ url=reverse(
+ "objecttype-detail",
+ args=[object_version.object_type.uuid],
+ )
+ ),
+ "status": object_version.status,
+ "createdAt": "2020-01-01",
+ "modifiedAt": "2020-01-01",
+ "publishedAt": None,
+ "jsonSchema": JSON_SCHEMA,
+ }
+ ],
+ },
+ )
+
+ def test_get_versions_incorrect_format_uuid(self):
+ """
+ Regression test for https://github.com/maykinmedia/objects-api/issues/361
+ """
+ url = reverse("objecttypeversion-list", args=["aaa"])
+
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_create_version(self):
+ object_type = ObjectTypeFactory.create()
+ data = {"jsonSchema": JSON_SCHEMA, "status": ObjectTypeVersionStatus.published}
+ url = reverse("objecttypeversion-list", args=[object_type.uuid])
+
+ response = self.client.post(url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(ObjectTypeVersion.objects.count(), 1)
+
+ object_version = ObjectTypeVersion.objects.get()
+
+ self.assertEqual(object_version.object_type, object_type)
+ self.assertEqual(object_version.json_schema, JSON_SCHEMA)
+ self.assertEqual(object_version.version, 1)
+ self.assertEqual(object_version.status, ObjectTypeVersionStatus.published)
+ self.assertEqual(object_version.created_at, date(2020, 1, 1))
+ self.assertEqual(object_version.modified_at, date(2020, 1, 1))
+ self.assertEqual(object_version.published_at, date(2020, 1, 1))
+
+ def test_update_version(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(object_type=object_type)
+ url = reverse(
+ "objecttypeversion-detail", args=[object_type.uuid, object_version.version]
+ )
+ new_json_schema = {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": ["diameter"],
+ "properties": {"diameter": {"type": "number"}},
+ }
+
+ response = self.client.put(
+ url,
+ {
+ "jsonSchema": new_json_schema,
+ "status": ObjectTypeVersionStatus.published,
+ },
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ object_version.refresh_from_db()
+
+ self.assertEqual(object_version.json_schema, new_json_schema)
+ self.assertEqual(object_version.status, ObjectTypeVersionStatus.published)
+ self.assertEqual(object_version.published_at, date(2020, 1, 1))
+
+ def test_delete_version(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(object_type=object_type)
+ url = reverse(
+ "objecttypeversion-detail", args=[object_type.uuid, object_version.version]
+ )
+
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+ self.assertEqual(ObjectTypeVersion.objects.count(), 0)
diff --git a/src/objects/tests/v2/test_ordering.py b/src/objects/tests/v2/test_ordering.py
index cc4d950d..e29a5052 100644
--- a/src/objects/tests/v2/test_ordering.py
+++ b/src/objects/tests/v2/test_ordering.py
@@ -10,8 +10,6 @@
from .utils import reverse_lazy
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class OrderingTests(TokenAuthMixin, APITestCase):
url = reverse_lazy("object-list")
@@ -20,7 +18,7 @@ class OrderingTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
@@ -128,7 +126,7 @@ class OrderingAllowedTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
def test_not_allowed_field(self):
PermissionFactory.create(
diff --git a/src/objects/tests/v2/test_pagination.py b/src/objects/tests/v2/test_pagination.py
index 80475fc3..1bfbb006 100644
--- a/src/objects/tests/v2/test_pagination.py
+++ b/src/objects/tests/v2/test_pagination.py
@@ -10,8 +10,6 @@
from .utils import reverse_lazy
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class FilterObjectTypeTests(TokenAuthMixin, APITestCase):
url = reverse_lazy("object-list")
@@ -20,7 +18,7 @@ class FilterObjectTypeTests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=cls.object_type,
mode=PermissionModes.read_only,
diff --git a/src/objects/tests/v2/test_permissions_api.py b/src/objects/tests/v2/test_permissions_api.py
index efb954de..f2af7979 100644
--- a/src/objects/tests/v2/test_permissions_api.py
+++ b/src/objects/tests/v2/test_permissions_api.py
@@ -7,8 +7,6 @@
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class ObjectApiTests(TokenAuthMixin, APITestCase):
maxDiff = None
@@ -45,13 +43,13 @@ def test_list_permissions(self):
"previous": None,
"results": [
{
- "type": permission1.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[permission1.object_type.uuid])}",
"mode": PermissionModes.read_and_write,
"use_fields": False,
"fields": {},
},
{
- "type": permission2.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[permission2.object_type.uuid])}",
"mode": "read_only",
"use_fields": True,
"fields": {"1": ["url", "uuid"], "2": ["url", "record"]},
@@ -83,7 +81,7 @@ def test_list_permissions_for_only_user(self):
data,
[
{
- "type": permission1.object_type.url,
+ "type": f"http://testserver{reverse('objecttype-detail', args=[permission1.object_type.uuid])}",
"mode": PermissionModes.read_and_write,
"use_fields": False,
"fields": {},
diff --git a/src/objects/tests/v2/test_stuf.py b/src/objects/tests/v2/test_stuf.py
index ba29ce75..86b64cad 100644
--- a/src/objects/tests/v2/test_stuf.py
+++ b/src/objects/tests/v2/test_stuf.py
@@ -20,8 +20,6 @@
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
class Stuf21Tests(TokenAuthMixin, APITestCase):
"""# noqa
@@ -37,7 +35,7 @@ class Stuf21Tests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
cls.object = ObjectFactory.create(object_type=cls.object_type)
PermissionFactory.create(
object_type=cls.object_type,
@@ -288,7 +286,7 @@ class Stuf22Tests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
cls.object = ObjectFactory.create(object_type=cls.object_type)
cls.record_1 = ObjectRecordFactory.create(
object=cls.object,
@@ -417,7 +415,7 @@ class Stuf23Tests(TokenAuthMixin, APITestCase):
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
+ cls.object_type = ObjectTypeFactory.create()
cls.object = ObjectFactory.create(object_type=cls.object_type)
cls.record_1 = ObjectRecordFactory.create(
object=cls.object,
diff --git a/src/objects/tests/v2/test_validation.py b/src/objects/tests/v2/test_validation.py
index cfe22645..29c1330b 100644
--- a/src/objects/tests/v2/test_validation.py
+++ b/src/objects/tests/v2/test_validation.py
@@ -1,142 +1,42 @@
-import datetime
import uuid
-from django.conf import settings
-
-import requests
-import requests_mock
-from freezegun import freeze_time
from rest_framework import status
from rest_framework.test import APITestCase
from objects.core.models import Object
-from objects.core.tests.factories import ObjectRecordFactory, ObjectTypeFactory
+from objects.core.tests.factories import (
+ ObjectRecordFactory,
+ ObjectTypeFactory,
+ ObjectTypeVersionFactory,
+)
from objects.token.constants import PermissionModes
from objects.token.tests.factories import PermissionFactory
from objects.utils.test import ClearCachesMixin, TokenAuthMixin
+from ...core.constants import ObjectTypeVersionStatus
from ..constants import GEO_WRITE_KWARGS
-from ..utils import mock_objecttype, mock_objecttype_version, mock_service_oas_get
from .utils import reverse
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
-@requests_mock.Mocker()
class ObjectTypeValidationTests(TokenAuthMixin, ClearCachesMixin, APITestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
- PermissionFactory.create(
- object_type=cls.object_type,
- mode=PermissionModes.read_and_write,
- token_auth=cls.token_auth,
- )
+ def setUp(self):
+ super().setUp()
+
+ self.object_type = ObjectTypeFactory.create()
- def test_valid_create_object_check_cache(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- url = reverse("object-list")
- data = {
- "type": self.object_type.url,
- "record": {
- "typeVersion": 1,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
- "startAt": "2020-01-01",
- },
- }
- with self.subTest("ok_cache"):
- self.assertEqual(m.call_count, 0)
- self.assertEqual(Object.objects.count(), 0)
- for n in range(5):
- self.client.post(url, data, **GEO_WRITE_KWARGS)
- # just one request should run — the first one
- self.assertEqual(m.call_count, 1)
- self.assertEqual(Object.objects.count(), 5)
-
- with self.subTest("clear_cache"):
- m.reset_mock()
- self.assertEqual(m.call_count, 0)
- for n in range(5):
- self._clear_caches()
- self.client.post(url, data, **GEO_WRITE_KWARGS)
- self.assertEqual(m.call_count, 5)
- self.assertEqual(Object.objects.count(), 10)
-
- with self.subTest("cache_timeout"):
- m.reset_mock()
- self._clear_caches()
- old_datetime = datetime.datetime(2025, 5, 1, 12, 0)
- with freeze_time(old_datetime.isoformat()):
- self.assertEqual(m.call_count, 0)
- self.client.post(url, data, **GEO_WRITE_KWARGS)
- self.client.post(url, data, **GEO_WRITE_KWARGS)
- # only one request for two post
- self.assertEqual(m.call_count, 1)
-
- # cache_timeout is still ok
- cache_timeout = settings.OBJECTTYPE_VERSION_CACHE_TIMEOUT
- new_datetime = old_datetime + datetime.timedelta(
- seconds=(cache_timeout - 60)
- )
- with freeze_time(new_datetime.isoformat()):
- # same request as before
- self.assertEqual(m.call_count, 1)
- self.client.post(url, data, **GEO_WRITE_KWARGS)
- # same request as before
- self.assertEqual(m.call_count, 1)
-
- # cache_timeout is expired
- cache_timeout = settings.OBJECTTYPE_VERSION_CACHE_TIMEOUT
- new_datetime = old_datetime + datetime.timedelta(
- seconds=(cache_timeout + 60)
- )
- with freeze_time(new_datetime.isoformat()):
- # same request as before
- self.assertEqual(m.call_count, 1)
- self.client.post(url, data, **GEO_WRITE_KWARGS)
- # new request
- self.assertEqual(m.call_count, 2)
-
- def test_create_object_with_not_found_objecttype_url(self, m):
- object_type_invalid = ObjectTypeFactory.create(service=self.object_type.service)
PermissionFactory.create(
- object_type=object_type_invalid,
+ object_type=self.object_type,
mode=PermissionModes.read_and_write,
token_auth=self.token_auth,
)
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(f"{object_type_invalid.url}/versions/1", status_code=404)
-
- url = reverse("object-list")
- data = {
- "type": object_type_invalid.url,
- "record": {
- "typeVersion": 1,
- "data": {"plantDate": "2020-04-12"},
- "startAt": "2020-01-01",
- },
- }
- response = self.client.post(url, data, **GEO_WRITE_KWARGS)
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(Object.objects.count(), 0)
-
- def test_create_object_with_invalid_length(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- object_type_long = f"{OBJECT_TYPES_API}{'a' * 1000}/{self.object_type.uuid}"
+ def test_create_object_with_invalid_length(self):
data = {
- "type": object_type_long,
+ "type": f"https://testserver/{'a' * 1000}/{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -153,65 +53,10 @@ def test_create_object_with_invalid_length(self, m):
data = response.json()
self.assertEqual(data["type"], ["The value has too many characters"])
- def test_create_object_no_version(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(f"{self.object_type.url}/versions/10", status_code=404)
-
- url = reverse("object-list")
- data = {
- "type": self.object_type.url,
- "record": {
- "typeVersion": 10,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
- "startAt": "2020-01-01",
- },
- }
-
- response = self.client.post(url, data, **GEO_WRITE_KWARGS)
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(Object.objects.count(), 0)
-
- data = response.json()
- self.assertEqual(
- data["non_field_errors"], ["Object type version can not be retrieved."]
- )
-
- def test_create_object_objecttype_request_error(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(f"{self.object_type.url}/versions/10", exc=requests.HTTPError)
-
- url = reverse("object-list")
- data = {
- "type": self.object_type.url,
- "record": {
- "typeVersion": 10,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
- "startAt": "2020-01-01",
- },
- }
-
- response = self.client.post(url, data, **GEO_WRITE_KWARGS)
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(Object.objects.count(), 0)
-
- data = response.json()
- self.assertEqual(
- data["non_field_errors"], ["Object type version can not be retrieved."]
- )
-
- def test_create_object_objecttype_with_no_jsonSchema(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/10",
- status_code=200,
- json={"key": "value"},
- )
-
+ def test_create_object_no_version(self):
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 10,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -227,21 +72,15 @@ def test_create_object_objecttype_with_no_jsonSchema(self, m):
data = response.json()
self.assertEqual(
data["non_field_errors"],
- [
- f"{self.object_type.versions_url} does not appear to be a valid objecttype."
- ],
+ [f"{self.object_type} version: 10 does not appear to exist."],
)
- def test_create_object_schema_invalid(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
+ def test_create_object_schema_invalid(self):
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12"},
@@ -259,12 +98,10 @@ def test_create_object_schema_invalid(self, m):
data["non_field_errors"], ["'diameter' is a required property"]
)
- def test_create_object_without_record_invalid(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
-
+ def test_create_object_without_record_invalid(self):
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
}
response = self.client.post(url, data, **GEO_WRITE_KWARGS)
@@ -272,17 +109,13 @@ def test_create_object_without_record_invalid(self, m):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Object.objects.count(), 0)
- def test_create_object_correction_invalid(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
+ def test_create_object_correction_invalid(self):
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
record = ObjectRecordFactory.create()
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -302,23 +135,18 @@ def test_create_object_correction_invalid(self, m):
[f"Object with index={record.index} does not exist."],
)
- def test_create_object_geometry_not_allowed(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(
- self.object_type.url,
- json=mock_objecttype(self.object_type.url, attrs={"allowGeometry": False}),
- )
+ def test_create_object_geometry_not_allowed(self):
+ self.object_type.allow_geometry = False
+ self.object_type.save()
+
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
+ "data": {"diameter": 30},
"geometry": {
"type": "Point",
"coordinates": [4.910649523925713, 52.37240093589432],
@@ -335,51 +163,17 @@ def test_create_object_geometry_not_allowed(self, m):
["This object type doesn't support geometry"],
)
- def test_create_object_with_geometry_without_allowGeometry(self, m):
- """test the support of Objecttypes api without allowGeometry property"""
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- object_type_response = mock_objecttype(self.object_type.url)
- del object_type_response["allowGeometry"]
- m.get(self.object_type.url, json=object_type_response)
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
-
- url = reverse("object-list")
- data = {
- "type": self.object_type.url,
- "record": {
- "typeVersion": 1,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
- "geometry": {
- "type": "Point",
- "coordinates": [4.910649523925713, 52.37240093589432],
- },
- "startAt": "2020-01-01",
- },
- }
-
- response = self.client.post(url, data, **GEO_WRITE_KWARGS)
-
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- def test_create_object_with_empty_data_valid(self, m):
+ def test_create_object_with_empty_data_valid(self):
"""
regression test for https://github.com/maykinmedia/objects-api/issues/371
"""
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- objecttype_version_response = mock_objecttype_version(self.object_type.url)
- objecttype_version_response["jsonSchema"]["required"] = []
- m.get(
- f"{self.object_type.url}/versions/1",
- json=objecttype_version_response,
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
+ version = ObjectTypeVersionFactory.create(object_type=self.object_type)
+ version.json_schema["required"] = []
+ version.save()
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {},
@@ -391,20 +185,17 @@ def test_create_object_with_empty_data_valid(self, m):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- def test_create_object_with_empty_data_invalid(self, m):
+ def test_create_object_with_empty_data_invalid(
+ self,
+ ):
"""
regression test for https://github.com/maykinmedia/objects-api/issues/371
"""
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
url = reverse("object-list")
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {},
@@ -416,12 +207,8 @@ def test_create_object_with_empty_data_invalid(self, m):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- def test_update_object_with_correction_invalid(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
+ def test_update_object_with_correction_invalid(self):
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
corrected_record, initial_record = ObjectRecordFactory.create_batch(
2, object__object_type=self.object_type
@@ -429,7 +216,7 @@ def test_update_object_with_correction_invalid(self, m):
object = initial_record.object
url = reverse("object-detail", args=[object.uuid])
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"plantDate": "2020-04-12", "diameter": 30},
@@ -448,15 +235,13 @@ def test_update_object_with_correction_invalid(self, m):
["Object with index=5 does not exist."],
)
- def test_update_object_type_invalid(self, m):
- old_object_type = ObjectTypeFactory.create(service=self.object_type.service)
+ def test_update_object_type_invalid(self):
+ old_object_type = ObjectTypeFactory.create()
PermissionFactory.create(
object_type=old_object_type,
mode=PermissionModes.read_and_write,
token_auth=self.token_auth,
)
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
initial_record = ObjectRecordFactory.create(
object__object_type=old_object_type,
@@ -467,7 +252,7 @@ def test_update_object_type_invalid(self, m):
url = reverse("object-detail", args=[object.uuid])
data = {
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
}
response = self.client.patch(url, data, **GEO_WRITE_KWARGS)
@@ -480,10 +265,7 @@ def test_update_object_type_invalid(self, m):
["This field can't be changed"],
)
- def test_update_uuid_invalid(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(self.object_type.url, json=mock_objecttype(self.object_type.url))
-
+ def test_update_uuid_invalid(self):
initial_record = ObjectRecordFactory.create(
object__object_type=self.object_type
)
@@ -499,19 +281,13 @@ def test_update_uuid_invalid(self, m):
data = response.json()
self.assertEqual(data["uuid"], ["This field can't be changed"])
- def test_update_geometry_not_allowed(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
- m.get(
- self.object_type.url,
- json=mock_objecttype(self.object_type.url, attrs={"allowGeometry": False}),
- )
+ def test_update_geometry_not_allowed(self):
+ self.object_type.allow_geometry = False
+ self.object_type.save()
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
initial_record = ObjectRecordFactory.create(
- object__object_type=self.object_type, geometry=None
+ object__object_type=self.object_type, geometry=None, data={"diameter": 20}
)
object = initial_record.object
@@ -519,7 +295,7 @@ def test_update_geometry_not_allowed(self, m):
data = {
"record": {
"typeVersion": 1,
- "data": {"plantDate": "2020-04-12", "diameter": 30},
+ "data": {"diameter": 30},
"geometry": {
"type": "Point",
"coordinates": [4.910649523925713, 52.37240093589432],
@@ -536,22 +312,17 @@ def test_update_geometry_not_allowed(self, m):
["This object type doesn't support geometry"],
)
- def test_create_object_with_duplicate_uuid_returns_400(self, m):
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
-
+ def test_create_object_with_duplicate_uuid_returns_400(self):
+ ObjectTypeVersionFactory.create(object_type=self.object_type)
url = reverse("object-list")
data = {
"uuid": "11111111-1111-1111-1111-111111111111",
- "type": self.object_type.url,
+ "type": f"https://testserver{reverse('objecttype-detail', args=[self.object_type.uuid])}",
"record": {
"typeVersion": 1,
"data": {"diameter": 30},
- "startAt": "2026-02-05",
+ "startAt": "2020-01-01",
},
}
@@ -563,3 +334,113 @@ def test_create_object_with_duplicate_uuid_returns_400(self, m):
self.assertEqual(
response.json()["uuid"], ["An object with this UUID already exists."]
)
+
+ def test_patch_objecttype_with_uuid_fail(self):
+ object_type = ObjectTypeFactory.create()
+ url = reverse("objecttype-detail", args=[object_type.uuid])
+
+ response = self.client.patch(url, {"uuid": uuid.uuid4()})
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ data = response.json()
+ self.assertEqual(data["uuid"], ["This field can't be changed"])
+
+ def test_delete_objecttype_with_versions_fail(self):
+ object_type = ObjectTypeFactory.create()
+ ObjectTypeVersionFactory.create(object_type=object_type)
+ url = reverse("objecttype-detail", args=[object_type.uuid])
+
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ data = response.json()
+ self.assertEqual(
+ data["non_field_errors"],
+ [
+ "All related versions should be destroyed before destroying the objecttype"
+ ],
+ )
+
+
+class ObjectTypeVersionValidationTests(TokenAuthMixin, APITestCase):
+ def test_create_version_with_incorrect_schema_fail(self):
+ object_type = ObjectTypeFactory.create()
+ url = reverse("objecttypeversion-list", args=[object_type.uuid])
+ data = {
+ "jsonSchema": {
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "any",
+ }
+ }
+
+ response = self.client.post(url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertTrue("jsonSchema" in response.json())
+
+ def test_create_version_with_incorrect_objecttype_fail(self):
+ url = reverse("objecttypeversion-list", args=[uuid.uuid4()])
+ data = {
+ "jsonSchema": {
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "diameter": {"type": "integer", "description": "size in cm."}
+ },
+ }
+ }
+
+ response = self.client.post(url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(
+ response.json()["non_field_errors"], ["Objecttype url is invalid"]
+ )
+
+ def test_update_published_version_fail(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(
+ object_type=object_type, status=ObjectTypeVersionStatus.published
+ )
+ url = reverse(
+ "objecttypeversion-detail",
+ args=[object_type.uuid, object_version.version],
+ )
+ new_json_schema = {
+ "type": "object",
+ "title": "Tree",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "required": ["diameter"],
+ "properties": {"diameter": {"type": "number"}},
+ }
+
+ response = self.client.put(url, {"jsonSchema": new_json_schema})
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ data = response.json()
+ self.assertEqual(
+ data["non_field_errors"], ["Only draft versions can be changed"]
+ )
+
+ def test_delete_puclished_version_fail(self):
+ object_type = ObjectTypeFactory.create()
+ object_version = ObjectTypeVersionFactory.create(
+ object_type=object_type, status=ObjectTypeVersionStatus.published
+ )
+ url = reverse(
+ "objecttypeversion-detail",
+ args=[object_type.uuid, object_version.version],
+ )
+
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ data = response.json()
+ self.assertEqual(
+ data["non_field_errors"], ["Only draft versions can be destroyed"]
+ )
diff --git a/src/objects/token/admin.py b/src/objects/token/admin.py
index e2472669..8578ce4f 100644
--- a/src/objects/token/admin.py
+++ b/src/objects/token/admin.py
@@ -1,10 +1,8 @@
-from django.contrib import admin, messages
+from django.contrib import admin
from django.contrib.admin.utils import unquote
-from django.utils.translation import gettext_lazy as _
from objects.api.serializers import ObjectSerializer
from objects.core.models import ObjectType
-from objects.core.utils import can_connect_to_objecttypes
from objects.utils.admin import EditInlineAdminMixin
from objects.utils.serializers import build_spec, get_field_names
@@ -78,19 +76,12 @@ def get_extra_context(self, request, object_id):
"object_type_choices": object_type_choices,
"mode_choices": mode_choices,
"form_data": self.get_form_data(request, object_id),
- "objecttypes_available": can_connect_to_objecttypes(),
}
def change_view(self, request, object_id, form_url="", extra_context=None):
extra_context = extra_context or {}
extra_context.update(self.get_extra_context(request, object_id))
- if extra_context["objecttypes_available"] is False:
- msg = _(
- "ObjectTypes API is not reachable. Field-based authorization is impossible"
- )
- self.message_user(request, msg, messages.WARNING)
-
return super().change_view(
request,
object_id,
@@ -102,12 +93,6 @@ def add_view(self, request, form_url="", extra_context=None):
extra_context = extra_context or {}
extra_context.update(self.get_extra_context(request, object_id=None))
- if extra_context["objecttypes_available"] is False:
- msg = _(
- "ObjectTypes API is not reachable. Field-based authorization is impossible"
- )
- self.message_user(request, msg, messages.WARNING)
-
return super().add_view(request, form_url, extra_context)
diff --git a/src/objects/token/permissions.py b/src/objects/token/permissions.py
index aa2e318f..1b673715 100644
--- a/src/objects/token/permissions.py
+++ b/src/objects/token/permissions.py
@@ -63,3 +63,8 @@ def has_object_permission(self, request, view, obj):
return True
return bool(object_permission.mode == PermissionModes.read_and_write)
+
+
+class IsTokenAuthenticated(BasePermission):
+ def has_permission(self, request, view):
+ return bool(request.auth)
diff --git a/src/objects/token/tests/test_admin.py b/src/objects/token/tests/test_admin.py
index 16fdcce2..78b6f55a 100644
--- a/src/objects/token/tests/test_admin.py
+++ b/src/objects/token/tests/test_admin.py
@@ -3,8 +3,6 @@
from maykin_2fa.test import disable_admin_mfa
from maykin_common.vcr import VCRMixin
-from zgw_consumers.constants import AuthTypes
-from zgw_consumers.test.factories import ServiceFactory
from objects.accounts.tests.factories import UserFactory
from objects.core.tests.factories import ObjectTypeFactory
@@ -12,8 +10,6 @@
@disable_admin_mfa()
class PermissionAdminTests(VCRMixin, TestCase):
- object_types_api = "http://127.0.0.1:8008/api/{version}/"
-
def setUp(self):
super().setUp()
@@ -27,14 +23,8 @@ def test_with_object_types_api_v2(self):
Regression test for #449.
Test if Permission admin can handle objecttypes API V2 which added pagination
"""
- v2_service = ServiceFactory.create(
- api_root=self.object_types_api.format(version="v2"),
- auth_type=AuthTypes.api_key,
- header_key="Authorization",
- header_value="Token 5cebbb33ffa725b6ed5e9e98300061218ba98d71",
- )
object_type = ObjectTypeFactory.create(
- service=v2_service, uuid="71a2452a-66c3-4030-b5ec-a06035102e9e"
+ uuid="71a2452a-66c3-4030-b5ec-a06035102e9e"
)
response = self.client.get(self.url)
@@ -49,5 +39,5 @@ def test_with_object_types_api_v2(self):
)
self.assertEqual(
choices[1][1],
- f"{v2_service.label}: {object_type._name}",
+ str(object_type),
)
diff --git a/src/objects/token/tests/test_objecttype_authentication.py b/src/objects/token/tests/test_objecttype_authentication.py
new file mode 100644
index 00000000..56fff49d
--- /dev/null
+++ b/src/objects/token/tests/test_objecttype_authentication.py
@@ -0,0 +1,63 @@
+from django.urls import reverse
+
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from objects.token.models import TokenAuth
+
+
+class TestObjectTypeTokenAuthAuthorization(APITestCase):
+ def test_valid_token(self):
+ token_auth = TokenAuth.objects.create(
+ contact_person="contact_person",
+ email="contact_person@gmail.nl",
+ identifier="token-1",
+ )
+ response = self.client.get(
+ reverse("v2:objecttype-list"),
+ HTTP_AUTHORIZATION=f"Token {token_auth.token}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_valid_token_with_no_spaces(self):
+ token_auth = TokenAuth.objects.create(
+ contact_person="contact_person",
+ email="contact_person@gmail.nl",
+ identifier="token-1",
+ token="1234-Token-5678",
+ )
+ response = self.client.get(
+ reverse("v2:objecttype-list"),
+ HTTP_AUTHORIZATION=f"Token {token_auth.token}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_invalid_token_with_spaces(self):
+ token_auth = TokenAuth.objects.create(
+ contact_person="contact_person",
+ email="contact_person@gmail.nl",
+ identifier="token-1",
+ token="1234 Token 5678",
+ )
+ response = self.client.get(
+ reverse("v2:objecttype-list"),
+ HTTP_AUTHORIZATION=f"Token {token_auth.token}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_invalid_token_existing(self):
+ response = self.client.get(
+ reverse("v2:objecttype-list"),
+ HTTP_AUTHORIZATION="Token 1234-Token-5678",
+ )
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_empty_token(self):
+ response = self.client.get(
+ reverse("v2:objecttype-list"), HTTP_AUTHORIZATION="Token "
+ )
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_non_token(self):
+ response = self.client.get(reverse("v2:objecttype-list"))
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
diff --git a/src/objects/utils/autoschema.py b/src/objects/utils/autoschema.py
index 1e9a2a97..eb087a20 100644
--- a/src/objects/utils/autoschema.py
+++ b/src/objects/utils/autoschema.py
@@ -1,3 +1,5 @@
+from uuid import UUID
+
from django.conf import settings
from django.utils.translation import gettext_lazy as _
@@ -5,6 +7,7 @@
from drf_spectacular.openapi import AutoSchema as _AutoSchema
from drf_spectacular.plumbing import build_parameter_type, get_view_model
from drf_spectacular.utils import OpenApiParameter
+from rest_framework_nested.viewsets import NestedViewSetMixin
from vng_api_common.constants import VERSION_HEADER
from vng_api_common.geo import DEFAULT_CRS, HEADER_ACCEPT, HEADER_CONTENT
from vng_api_common.schema import HTTP_STATUS_CODE_TITLES
@@ -43,9 +46,15 @@ def get_override_parameters(self):
geo_headers = self.get_geo_headers()
content_type_headers = self.get_content_type_headers()
version_headers = self.get_version_headers()
+ parent_path_headers = self.get_parent_path_headers()
field_params = self.get_fields_params()
return (
- params + geo_headers + content_type_headers + version_headers + field_params
+ params
+ + geo_headers
+ + content_type_headers
+ + version_headers
+ + parent_path_headers
+ + field_params
)
def _get_filter_parameters(self):
@@ -217,6 +226,29 @@ def get_filter_params_for_search(self):
)
return parameters
+ def get_parent_path_headers(self) -> list:
+ """for nested viewsets"""
+ if not isinstance(self.view, NestedViewSetMixin):
+ return []
+
+ parent_lookup_kwargs = self.view._get_parent_lookup_kwargs()
+ path_params = list(parent_lookup_kwargs.keys())
+
+ return [
+ OpenApiParameter(
+ name=path_param,
+ type=UUID if "uuid" in path_param else str,
+ location=OpenApiParameter.PATH,
+ required=True,
+ description=(
+ _("Unique identifier (UUID4)")
+ if "uuid" in path_param
+ else _("Unique identifier")
+ ),
+ )
+ for path_param in path_params
+ ]
+
def _resolve_path_parameters(self, variables):
object_path = "uuid"
if variables == [object_path] and self.view.basename == "object":
diff --git a/src/objects/utils/cache.py b/src/objects/utils/cache.py
deleted file mode 100644
index 6a457744..00000000
--- a/src/objects/utils/cache.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from functools import wraps
-
-from django.core.cache import caches
-
-
-def cache(key: str, alias: str = "default", **set_options):
- def decorator(func: callable):
- @wraps(func)
- def wrapped(*args, **kwargs):
- _cache = caches[alias]
- result = _cache.get(key)
- if result is not None:
- return result
- result = func(*args, **kwargs)
- _cache.set(key, result, **set_options)
- return result
-
- return wrapped
-
- return decorator
diff --git a/src/objects/utils/oas_extensions/__init__.py b/src/objects/utils/oas_extensions/__init__.py
index df74f7e8..94b01412 100644
--- a/src/objects/utils/oas_extensions/__init__.py
+++ b/src/objects/utils/oas_extensions/__init__.py
@@ -1,10 +1,9 @@
-from .fields import HyperlinkedIdentityFieldExtension, ObjectTypeField
+from .fields import HyperlinkedRelatedFieldExtension
from .geojson import GeometryFieldExtension
from .query import DjangoFilterExtension
__all__ = (
"DjangoFilterExtension",
"GeometryFieldExtension",
- "HyperlinkedIdentityFieldExtension",
- "ObjectTypeField",
+ "HyperlinkedRelatedFieldExtension",
)
diff --git a/src/objects/utils/oas_extensions/fields.py b/src/objects/utils/oas_extensions/fields.py
index 72519b5b..085eb971 100644
--- a/src/objects/utils/oas_extensions/fields.py
+++ b/src/objects/utils/oas_extensions/fields.py
@@ -3,25 +3,9 @@
from drf_spectacular.types import OpenApiTypes
from rest_framework import serializers
-from objects.api.fields import ObjectTypeField
-
-class ObjectTypeExtension(OpenApiSerializerFieldExtension):
- target_class = ObjectTypeField
-
- def map_serializer_field(self, auto_schema, direction):
- schema = build_basic_type(OpenApiTypes.URI)
- schema.update(
- {
- "minLength": 1,
- "maxLength": 1000,
- }
- )
- return schema
-
-
-class HyperlinkedIdentityFieldExtension(OpenApiSerializerFieldExtension):
- target_class = serializers.HyperlinkedIdentityField
+class HyperlinkedRelatedFieldExtension(OpenApiSerializerFieldExtension):
+ target_class = serializers.HyperlinkedRelatedField
match_subclasses = True
def map_serializer_field(self, auto_schema, direction):
diff --git a/src/objects/utils/serializers.py b/src/objects/utils/serializers.py
index 4fdc2cca..d613cce6 100644
--- a/src/objects/utils/serializers.py
+++ b/src/objects/utils/serializers.py
@@ -3,6 +3,7 @@
from glom import SKIP, GlomError, glom
from rest_framework import fields, serializers
+from objects.tests.v2.utils import reverse
from objects.token.constants import PermissionModes
ALL_FIELDS = ["*"]
@@ -106,7 +107,13 @@ def to_representation(self, instance):
not_allowed = set(get_field_names(data)) - set(get_field_names(result_data))
if not_allowed:
self.not_allowed[
- f"{instance._object_type.url}({instance.version})"
+ f"{
+ self.context['request'].build_absolute_uri(
+ reverse(
+ 'objecttype-detail', args=[instance._object_type.uuid]
+ )
+ )
+ }({instance.version})"
] |= not_allowed
else:
spec_query = build_spec(query_fields)
@@ -121,7 +128,13 @@ def to_representation(self, instance):
)
if not_allowed:
self.not_allowed[
- f"{instance._object_type.url}({instance.version})"
+ f"{
+ self.context['request'].build_absolute_uri(
+ reverse(
+ 'objecttype-detail', args=[instance._object_type.uuid]
+ )
+ )
+ }({instance.version})"
] |= not_allowed
return result_data
diff --git a/src/objects/utils/test.py b/src/objects/utils/test.py
index ab51f2f6..1a3c599a 100644
--- a/src/objects/utils/test.py
+++ b/src/objects/utils/test.py
@@ -8,7 +8,7 @@ class TokenAuthMixin:
def setUpTestData(cls):
super().setUpTestData()
- cls.token_auth = TokenAuthFactory(
+ cls.token_auth = TokenAuthFactory.create(
contact_person="testsuite", email="test@letmein.nl"
)
diff --git a/src/objects/utils/tests/test_client.py b/src/objects/utils/tests/test_client.py
deleted file mode 100644
index 701ad768..00000000
--- a/src/objects/utils/tests/test_client.py
+++ /dev/null
@@ -1,105 +0,0 @@
-import requests_mock
-from rest_framework.test import APITestCase
-
-from objects.core.models import ObjectType
-from objects.core.tests.factories import ObjectTypeFactory
-from objects.tests.utils import (
- mock_objecttype,
- mock_objecttype_version,
- mock_service_oas_get,
-)
-from objects.token.constants import PermissionModes
-from objects.token.tests.factories import PermissionFactory
-from objects.utils.client import get_objecttypes_client
-
-from ..test import TokenAuthMixin
-
-OBJECT_TYPES_API = "https://example.com/objecttypes/v1/"
-
-
-@requests_mock.Mocker()
-class ObjecttypesClientTest(TokenAuthMixin, APITestCase):
- @classmethod
- def setUpTestData(cls):
- super().setUpTestData()
-
- cls.object_type = ObjectTypeFactory.create(service__api_root=OBJECT_TYPES_API)
- PermissionFactory.create(
- object_type=cls.object_type,
- mode=PermissionModes.read_and_write,
- token_auth=cls.token_auth,
- )
-
- def test_list_objecttypes(self, m):
- object_type = ObjectType.objects.first()
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- object_type_mock = mock_objecttype(object_type.url)
- m.get(
- f"{OBJECT_TYPES_API}objecttypes",
- json={
- "count": 1,
- "next": None,
- "previous": None,
- "results": [object_type_mock],
- },
- )
- with get_objecttypes_client(object_type.service) as client:
- self.assertTrue(client.can_connect)
- data = client.list_objecttypes()
- self.assertEqual(data, [object_type_mock])
- self.assertEqual(len(data), 1)
-
- def test_get_objecttype(self, m):
- object_type = ObjectType.objects.first()
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(object_type.url, json=mock_objecttype(object_type.url))
-
- with get_objecttypes_client(object_type.service) as client:
- data = client.get_objecttype(object_type.uuid)
- self.assertTrue(data["url"], str(object_type.url))
-
- def test_list_objecttype_versions(self, m):
- object_type = ObjectType.objects.first()
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- object_type_mock = mock_objecttype(object_type.url)
- m.get(
- f"{OBJECT_TYPES_API}objecttypes",
- json={
- "count": 1,
- "next": None,
- "previous": None,
- "results": [object_type_mock],
- },
- )
- version_mock = mock_objecttype_version(object_type.url)
- m.get(
- f"{object_type.url}/versions",
- json={
- "count": 1,
- "next": None,
- "previous": None,
- "results": [version_mock],
- },
- )
-
- with get_objecttypes_client(object_type.service) as client:
- self.assertTrue(client.can_connect)
- data = client.list_objecttypes()
- self.assertEqual(data, [object_type_mock])
- self.assertEqual(len(data), 1)
- data = client.list_objecttype_versions(object_type.uuid)
- self.assertEqual(data, [version_mock])
- self.assertEqual(len(data), 1)
- self.assertEqual(data[0]["version"], 1)
-
- def test_get_objecttype_version(self, m):
- object_type = ObjectType.objects.first()
- mock_service_oas_get(m, OBJECT_TYPES_API, "objecttypes")
- m.get(
- f"{self.object_type.url}/versions/1",
- json=mock_objecttype_version(self.object_type.url),
- )
-
- with get_objecttypes_client(object_type.service) as client:
- data = client.get_objecttype_version(object_type.uuid, 1)
- self.assertEqual(data["url"], f"{self.object_type.url}/versions/1")