Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/writable api #20

Merged
merged 13 commits into from
Dec 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ django-sniplates
djangorestframework
# django-extra-fields
# django-filter
drf-nested-routers
drf-yasg # api documentation

# WSGI servers & monitoring - production oriented
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ django-solo==1.1.3 # via vng-api-common
django==2.2.12 # via -r requirements/base.in, django-appconf, django-axes, django-choices, django-filter, django-jsonsuit, django-redis, django-rest-framework-condition, django-rosetta, django-sniplates, drf-nested-routers, drf-yasg, vng-api-common
djangorestframework-camel-case==1.2.0 # via vng-api-common
djangorestframework==3.9.4 # via -r requirements/base.in, drf-nested-routers, drf-yasg, vng-api-common
drf-nested-routers==0.91 # via vng-api-common
drf-nested-routers==0.91 # via -r requirements/base.in, vng-api-common
drf-yasg==1.16.0 # via -r requirements/base.in, vng-api-common
elastic-apm==5.5.2 # via -r requirements/base.in
gemma-zds-client==0.13.3 # via vng-api-common
Expand Down
41 changes: 36 additions & 5 deletions src/objecttypes/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,57 @@
from rest_framework import serializers
from rest_framework_nested.relations import NestedHyperlinkedRelatedField
from rest_framework_nested.serializers import NestedHyperlinkedModelSerializer

from objecttypes.core.models import ObjectType, ObjectVersion

from .validators import JsonSchemaValidator, VersionUpdateValidator


class ObjectVersionSerializer(NestedHyperlinkedModelSerializer):
parent_lookup_kwargs = {"objecttype_uuid": "object_type__uuid"}

class ObjectVersionSerializer(serializers.ModelSerializer):
class Meta:
model = ObjectVersion
fields = (
"url",
"version",
"objectType",
"publicationDate",
"status",
"jsonSchema",
)
extra_kwargs = {
"publicationDate": {"source": "publication_date"},
"jsonSchema": {"source": "json_schema"},
"status": {"read_only": True},
"url": {"lookup_field": "version"},
"version": {"read_only": True},
"objectType": {
"source": "object_type",
"lookup_field": "uuid",
"read_only": True,
},
"publicationDate": {"source": "publication_date", "read_only": True},
"jsonSchema": {
"source": "json_schema",
"validators": [JsonSchemaValidator()],
},
}
validators = [VersionUpdateValidator()]

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)


class ObjectTypeSerializer(serializers.HyperlinkedModelSerializer):
versions = ObjectVersionSerializer(many=True)
versions = NestedHyperlinkedRelatedField(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just realized that this change breaks compatibility with Objects API validator. But I like that it's consistent with Zaken API now.
@joeribekker Should we display all versions data here? Or I can make a small change in Objects API

Copy link
Member

@joeribekker joeribekker Nov 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this as well. I don't see the need to expand this property by default.

If you manually saw that this breaks Objects API validation, then please change yes, BUT it also means its a blind spot in our CI-setup. You can make an issue for that :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, we don't have integration tests and we don't even have docker-compose with two APIs now. Perhaps we can set up a separate repo with integration tests?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it, but lets make an issue for now so it won't be missed again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

many=True,
read_only=True,
lookup_field="version",
view_name="objectversion-detail",
parent_lookup_kwargs={"objecttype_uuid": "object_type__uuid"},
)

class Meta:
model = ObjectType
Expand Down
12 changes: 8 additions & 4 deletions src/objecttypes/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from django.conf.urls import include
from django.urls import path, re_path

from rest_framework import routers
from vng_api_common import routers
from vng_api_common.schema import SchemaView

from .views import ObjectTypeViewSet
from .views import ObjectTypeViewSet, ObjectVersionViewSet

router = routers.DefaultRouter(trailing_slash=False)
router.register(r"objecttypes", ObjectTypeViewSet)
router = routers.DefaultRouter()
router.register(
r"objecttypes",
ObjectTypeViewSet,
[routers.nested("versions", ObjectVersionViewSet)],
)


urlpatterns = [
Expand Down
38 changes: 38 additions & 0 deletions src/objecttypes/api/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _

from rest_framework import serializers

from objecttypes.core.constants import ObjectVersionStatus
from objecttypes.core.utils import check_json_schema


class VersionUpdateValidator:
message = _("Only draft versions can be changed")
code = "non-draft-version-update"

def set_context(self, serializer):
"""
This hook is called by the serializer instance,
prior to the validation call being made.
"""
# Determine the existing instance, if this is an update operation.
self.instance = getattr(serializer, "instance", None)
self.request = serializer.context["request"]

def __call__(self, attrs):
if not self.instance:
return

if self.instance.status != ObjectVersionStatus.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
46 changes: 43 additions & 3 deletions src/objecttypes/api/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
from django.utils.translation import ugettext_lazy as _

from rest_framework import viewsets
from rest_framework.exceptions import ValidationError
from rest_framework.settings import api_settings
from vng_api_common.viewsets import NestedViewSetMixin

from objecttypes.core.models import ObjectType
from objecttypes.core.constants import ObjectVersionStatus
from objecttypes.core.models import ObjectType, ObjectVersion

from .filters import ObjectTypeFilterSet
from .serializers import ObjectTypeSerializer
from .serializers import ObjectTypeSerializer, ObjectVersionSerializer


class ObjectTypeViewSet(viewsets.ReadOnlyModelViewSet):
class ObjectTypeViewSet(viewsets.ModelViewSet):
queryset = ObjectType.objects.prefetch_related("versions").order_by("-pk")
serializer_class = ObjectTypeSerializer
lookup_field = "uuid"
filterset_class = ObjectTypeFilterSet

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)


class ObjectVersionViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
queryset = ObjectVersion.objects.order_by("object_type", "-version")
serializer_class = ObjectVersionSerializer
lookup_field = "version"

def perform_destroy(self, instance):
if instance.status != ObjectVersionStatus.draft:
raise ValidationError(
{
api_settings.NON_FIELD_ERRORS_KEY: [
_("Only draft versions can be destroyed")
]
},
code="non-draft-version-destroy",
)

super().perform_destroy(instance)
20 changes: 20 additions & 0 deletions src/objecttypes/core/migrations/0013_auto_20201118_1251.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 2.2.12 on 2020-11-18 11:51

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0012_auto_20201112_1123"),
]

operations = [
migrations.AlterField(
model_name="objectversion",
name="version",
field=models.PositiveSmallIntegerField(
help_text="Integer version of the OBJECTTYPE", verbose_name="version"
),
),
]
31 changes: 21 additions & 10 deletions src/objecttypes/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@
from datetime import date

from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _

from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for

from .constants import (
DataClassificationChoices,
ObjectVersionStatus,
UpdateFrequencyChoices,
)
from .utils import check_json_schema


class ObjectType(models.Model):
Expand Down Expand Up @@ -120,7 +117,7 @@ class ObjectVersion(models.Model):
ObjectType, on_delete=models.CASCADE, related_name="versions"
)
version = models.PositiveSmallIntegerField(
_("version"), help_text=_("Integer version of the OBJECTTYPE"), default=1
_("version"), help_text=_("Integer version of the OBJECTTYPE")
)
publication_date = models.DateField(
_("publication date"),
Expand All @@ -147,8 +144,22 @@ def __str__(self):
def clean(self):
super().clean()

schema_validator = validator_for(self.json_schema)
try:
schema_validator.check_schema(self.json_schema)
except SchemaError as exc:
raise ValidationError(exc.args[0]) from exc
check_json_schema(self.json_schema)

def save(self, *args, **kwargs):
if not self.version:
self.version = self.generate_version_number()

super().save(*args, **kwargs)

def generate_version_number(self) -> int:
existed_versions = ObjectVersion.objects.filter(object_type=self.object_type)

max_version = 0
if existed_versions.exists():
max_version = existed_versions.aggregate(models.Max("version"))[
"version__max"
]

version_number = max_version + 1
return version_number
1 change: 0 additions & 1 deletion src/objecttypes/core/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class Meta:

class ObjectVersionFactory(factory.django.DjangoModelFactory):
object_type = factory.SubFactory(ObjectTypeFactory)
version = factory.Sequence(lambda n: n)
json_schema = {
"type": "object",
"title": "Tree",
Expand Down
12 changes: 12 additions & 0 deletions src/objecttypes/core/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.core.exceptions import ValidationError

from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for


def check_json_schema(json_schema: dict):
schema_validator = validator_for(json_schema)
try:
schema_validator.check_schema(json_schema)
except SchemaError as exc:
raise ValidationError(exc.args[0]) from exc
Loading