From e7a40ed2059790732592686a1cb67ab5f89ca2df Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 17 Dec 2024 12:21:01 +0100 Subject: [PATCH 1/2] :sparkles: [#207] Add experimental PUT and PATCH for Kanaal this is required to support new kenmerken on the zaken kanaal, because the Kanaal has to be updated in Open Notificaties for it to work --- src/nrc/api/serializers.py | 9 +- src/nrc/api/viewsets.py | 10 +- src/nrc/utils/help_text.py | 5 + src/openapi.yaml | 373 ++++++++++++++++++++++++++++++++++++- 4 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 src/nrc/utils/help_text.py diff --git a/src/nrc/api/serializers.py b/src/nrc/api/serializers.py index dd742f6e..23c9222b 100644 --- a/src/nrc/api/serializers.py +++ b/src/nrc/api/serializers.py @@ -8,7 +8,8 @@ from djangorestframework_camel_case.util import camelize, underscoreize from notifications_api_common.api.serializers import NotificatieSerializer from rest_framework import fields, serializers -from vng_api_common.validators import URLValidator +from rest_framework.validators import UniqueValidator +from vng_api_common.validators import IsImmutableValidator, URLValidator from nrc.api.tasks import deliver_message from nrc.datamodel.models import Abonnement, Filter, FilterGroup, Kanaal, Notificatie @@ -39,6 +40,12 @@ class Meta: fields = ("url", "naam", "documentatie_link", "filters") extra_kwargs = { "url": {"lookup_field": "uuid"}, + "naam": { + "validators": [ + UniqueValidator(queryset=Kanaal.objects.all()), + IsImmutableValidator(), + ] + }, "documentatie_link": {"required": False, "validators": [URLValidator()]}, "filters": {"required": False}, } diff --git a/src/nrc/api/viewsets.py b/src/nrc/api/viewsets.py index 4c3489cf..9b780312 100644 --- a/src/nrc/api/viewsets.py +++ b/src/nrc/api/viewsets.py @@ -7,6 +7,7 @@ from vng_api_common.viewsets import CheckQueryParamsMixin from nrc.datamodel.models import Abonnement, Kanaal +from nrc.utils.help_text import mark_experimental from .filters import KanaalFilter from .scopes import SCOPE_NOTIFICATIES_CONSUMEREN, SCOPE_NOTIFICATIES_PUBLICEREN @@ -54,17 +55,22 @@ def perform_create(self, serializer): @extend_schema_view( list=extend_schema(summary="Alle KANAALen opvragen."), retrieve=extend_schema(summary="Een specifiek KANAAL opvragen."), + update=extend_schema(summary=mark_experimental("Een specifiek KANAAL bewerken.")), + partial_update=extend_schema( + summary=mark_experimental("Een specifiek KANAAL deels bewerken.") + ), create=extend_schema(summary="Maak een KANAAL aan."), ) class KanaalViewSet( CheckQueryParamsMixin, mixins.CreateModelMixin, mixins.ListModelMixin, + mixins.UpdateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet, ): """ - Opvragen en aanmaken van KANAALen. + Opvragen, aanmaken en bewerken van KANAALen. Op een KANAAL publiceren componenten (bronnen) hun NOTIFICATIEs. Alleen componenten die NOTIFICATIEs willen publiceren dienen een KANAAL aan te @@ -80,6 +86,8 @@ class KanaalViewSet( "list": SCOPE_NOTIFICATIES_PUBLICEREN | SCOPE_NOTIFICATIES_CONSUMEREN, "retrieve": SCOPE_NOTIFICATIES_PUBLICEREN | SCOPE_NOTIFICATIES_CONSUMEREN, "create": SCOPE_NOTIFICATIES_PUBLICEREN, + "update": SCOPE_NOTIFICATIES_PUBLICEREN, + "partial_update": SCOPE_NOTIFICATIES_PUBLICEREN, } diff --git a/src/nrc/utils/help_text.py b/src/nrc/utils/help_text.py new file mode 100644 index 00000000..04673a29 --- /dev/null +++ b/src/nrc/utils/help_text.py @@ -0,0 +1,5 @@ +from django.utils.translation import gettext_lazy as _ + + +def mark_experimental(text): + return _("**EXPERIMENTEEL** {}").format(text) diff --git a/src/openapi.yaml b/src/openapi.yaml index 7fea5175..eb9be8df 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -997,7 +997,7 @@ paths: get: operationId: kanaal_list description: |- - Opvragen en aanmaken van KANAALen. + Opvragen, aanmaken en bewerken van KANAALen. Op een KANAAL publiceren componenten (bronnen) hun NOTIFICATIEs. Alleen componenten die NOTIFICATIEs willen publiceren dienen een KANAAL aan te @@ -1142,7 +1142,7 @@ paths: post: operationId: kanaal_create description: |- - Opvragen en aanmaken van KANAALen. + Opvragen, aanmaken en bewerken van KANAALen. Op een KANAAL publiceren componenten (bronnen) hun NOTIFICATIEs. Alleen componenten die NOTIFICATIEs willen publiceren dienen een KANAAL aan te @@ -1299,7 +1299,7 @@ paths: get: operationId: kanaal_read description: |- - Opvragen en aanmaken van KANAALen. + Opvragen, aanmaken en bewerken van KANAALen. Op een KANAAL publiceren componenten (bronnen) hun NOTIFICATIEs. Alleen componenten die NOTIFICATIEs willen publiceren dienen een KANAAL aan te @@ -1440,6 +1440,345 @@ paths: schema: $ref: '#/components/schemas/Fout' description: Internal server error + put: + operationId: kanaal_update + description: |- + Opvragen, aanmaken en bewerken van KANAALen. + + Op een KANAAL publiceren componenten (bronnen) hun NOTIFICATIEs. Alleen + componenten die NOTIFICATIEs willen publiceren dienen een KANAAL aan te + maken. Dit KANAAL kan vervolgens aan consumers worden gegeven om zich op te + abonneren. + summary: '**EXPERIMENTEEL** Een specifiek KANAAL bewerken.' + parameters: + - in: header + name: Content-Type + schema: + type: string + enum: + - application/json + description: Content type van de verzoekinhoud. + required: true + - in: path + name: uuid + schema: + type: string + format: uuid + description: Unique resource identifier (UUID4) + required: true + tags: + - kanaal + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Kanaal' + required: true + security: + - JWT-Claims: + - notificaties.publiceren + 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/Kanaal' + description: OK + '400': + 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/problem+json: + schema: + $ref: '#/components/schemas/ValidatieFout' + description: Bad request + '401': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Unauthorized + '403': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Forbidden + '404': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Not found + '406': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Not acceptable + '409': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Conflict + '410': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Gone + '415': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Unsupported media type + '429': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Too many requests + '500': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Internal server error + patch: + operationId: kanaal_partial_update + description: |- + Opvragen, aanmaken en bewerken van KANAALen. + + Op een KANAAL publiceren componenten (bronnen) hun NOTIFICATIEs. Alleen + componenten die NOTIFICATIEs willen publiceren dienen een KANAAL aan te + maken. Dit KANAAL kan vervolgens aan consumers worden gegeven om zich op te + abonneren. + summary: '**EXPERIMENTEEL** Een specifiek KANAAL deels bewerken.' + parameters: + - in: header + name: Content-Type + schema: + type: string + enum: + - application/json + description: Content type van de verzoekinhoud. + required: true + - in: path + name: uuid + schema: + type: string + format: uuid + description: Unique resource identifier (UUID4) + required: true + tags: + - kanaal + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedKanaal' + security: + - JWT-Claims: + - notificaties.publiceren + 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/Kanaal' + description: OK + '400': + 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/problem+json: + schema: + $ref: '#/components/schemas/ValidatieFout' + description: Bad request + '401': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Unauthorized + '403': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Forbidden + '404': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Not found + '406': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Not acceptable + '409': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Conflict + '410': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Gone + '415': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Unsupported media type + '429': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Too many requests + '500': + 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/problem+json: + schema: + $ref: '#/components/schemas/Fout' + description: Internal server error /notificaties: post: operationId: notificaties_create @@ -1800,6 +2139,34 @@ components: $ref: '#/components/schemas/FilterGroup' description: Een lijst van kanalen en filters waarop het ABONNEMENT wordt afgenomen. + PatchedKanaal: + type: object + properties: + url: + type: string + format: uri + readOnly: true + description: URL-referentie naar dit object. Dit is de unieke identificatie + en locatie van dit object. + minLength: 1 + maxLength: 1000 + naam: + type: string + description: Naam van het KANAAL (ook wel "Exchange" genoemd) dat de bron + vertegenwoordigd. + maxLength: 50 + documentatieLink: + type: string + format: uri + description: URL naar documentatie. + maxLength: 200 + filters: + type: array + items: + type: string + maxLength: 100 + description: Lijst van mogelijke filter kenmerken van een KANAAL. Deze filter + kenmerken kunnen worden gebruikt bij het aanmaken van een ABONNEMENT. ValidatieFout: type: object description: Formaat van HTTP 4xx en 5xx fouten. From a4c6760849a31c122e282eed80988a9b0ca90936 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 17 Dec 2024 12:21:55 +0100 Subject: [PATCH 2/2] :white_check_mark: [#207] Add tests for PUT and PATCH on Kanaal --- src/nrc/api/tests/test_kanaal.py | 98 +++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/src/nrc/api/tests/test_kanaal.py b/src/nrc/api/tests/test_kanaal.py index 58250669..196aa89a 100644 --- a/src/nrc/api/tests/test_kanaal.py +++ b/src/nrc/api/tests/test_kanaal.py @@ -2,7 +2,12 @@ from rest_framework import status from rest_framework.test import APITestCase -from vng_api_common.tests import JWTAuthMixin, get_operation_url +from vng_api_common.tests import ( + JWTAuthMixin, + get_operation_url, + get_validation_errors, + reverse, +) from nrc.datamodel.models import Kanaal from nrc.datamodel.tests.factories import KanaalFactory @@ -46,25 +51,90 @@ def test_kanaal_create_nonunique(self): response.status_code, status.HTTP_400_BAD_REQUEST, response.data ) - def test_kanaal_update_delete(self): + def test_kanaal_update(self): + kanaal = KanaalFactory.create( + naam="zaken", + documentatie_link="https://example.com/doc", + filters=["zaaktype"], + ) + kanaal_url = reverse(kanaal) + data = { + "naam": "zaken", + "documentatie_link": "https://example.com/updated", + "filters": ["zaaktype", "zaaktype.catalogus"], + } + + response = self.client.put(kanaal_url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + + # check parsing to model + data = response.json() + kanaal = Kanaal.objects.get() + self.assertEqual(kanaal.naam, "zaken") + self.assertEqual(kanaal.documentatie_link, "https://example.com/updated") + self.assertEqual(kanaal.filters, ["zaaktype", "zaaktype.catalogus"]) + + def test_kanaal_partial_update(self): + kanaal = KanaalFactory.create( + naam="zaken", + documentatie_link="https://example.com/doc", + filters=["zaaktype"], + ) + kanaal_url = reverse(kanaal) + data = {"filters": ["zaaktype", "zaaktype.catalogus"]} + + response = self.client.patch(kanaal_url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + + # check parsing to model + data = response.json() + kanaal = Kanaal.objects.get() + self.assertEqual(kanaal.naam, "zaken") + self.assertEqual(kanaal.documentatie_link, "https://example.com/doc") + self.assertEqual(kanaal.filters, ["zaaktype", "zaaktype.catalogus"]) + + def test_kanaal_cannot_update_naam(self): + kanaal = KanaalFactory.create( + naam="zaken", + documentatie_link="https://example.com/doc", + filters=["zaaktype"], + ) + kanaal_url = reverse(kanaal) + data = { + "naam": "modified", + "documentatie_link": "https://example.com/updated", + "filters": ["zaaktype", "zaaktype.catalogus"], + } + + response = self.client.put(kanaal_url, data) + + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + + # check parsing to model + data = response.json() + + error = get_validation_errors(response, "naam") + self.assertEqual(error["code"], "wijzigen-niet-toegelaten") + + kanaal = Kanaal.objects.get() + self.assertEqual(kanaal.naam, "zaken") + self.assertEqual(kanaal.documentatie_link, "https://example.com/doc") + self.assertEqual(kanaal.filters, ["zaaktype"]) + + def test_kanaal_delete(self): """ - test /kanaal PUT, DELETE: - attempt to update and destroy kanaal via request + test /kanaal DELETE: + attempt to destroy kanaal via request check if response contents status 405 """ kanaal = Kanaal.objects.create(naam="zaken") kanaal_url = get_operation_url("kanaal_read", uuid=kanaal.uuid) - data = {"documentatie_link": "https://example.com/doc"} - - response_put = self.client.put(kanaal_url, data) - - self.assertEqual( - response_put.status_code, - status.HTTP_405_METHOD_NOT_ALLOWED, - response_put.data, - ) - response_delete = self.client.delete(kanaal_url, data) + response_delete = self.client.delete(kanaal_url) self.assertEqual( response_delete.status_code,