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/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, 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.