Skip to content

Commit

Permalink
Reporter cancel endpoint (Amsterdam#1338)
Browse files Browse the repository at this point in the history
* Allow cancelling the reporter
  • Loading branch information
4c0n committed Aug 14, 2023
1 parent e4a8ef0 commit 79f6070
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 5 deletions.
1 change: 0 additions & 1 deletion app/signals/apps/api/generics/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,4 @@ def has_object_permission(self, request: Request, view: View, obj: Reporter) ->
) or SignalPermissionService.has_permission(
user=request.user,
permission='signals.sia_can_view_all_categories',
signal=obj._signal
)
10 changes: 7 additions & 3 deletions app/signals/apps/api/serializers/signal_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from django.db import transaction
from django.utils import timezone
from django_fsm import TransitionNotAllowed
from rest_framework.fields import BooleanField
from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from rest_framework.fields import BooleanField, CharField

from signals.apps.history.models import Log
from signals.apps.signals.models import Reporter, Signal


class SignalReporterSerializer(ModelSerializer):
class SignalReporterSerializer(serializers.ModelSerializer):
allows_contact = BooleanField(source='_signal.allows_contact', read_only=True)
sharing_allowed = BooleanField(required=True)

Expand Down Expand Up @@ -116,3 +116,7 @@ def create(self, validated_data: dict) -> Reporter:
)

return reporter


class CancelSignalReporterSerializer(serializers.Serializer):
reason = CharField(required=False)
73 changes: 73 additions & 0 deletions app/signals/apps/api/tests/test_private_signal_reporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,76 @@ def test_can_create(self) -> None:

reporter = response.json()
self.assertEqual(reporter.get('state'), Reporter.REPORTER_STATE_VERIFICATION_EMAIL_SENT)

def test_can_cancel_without_reason(self) -> None:
signal = SignalFactory.create(reporter__state=Reporter.REPORTER_STATE_APPROVED)
reporter = ReporterFactory.create(_signal=signal, state=Reporter.REPORTER_STATE_VERIFICATION_EMAIL_SENT)

response = self.client.post(
f'/signals/v1/private/signals/{signal.pk}/reporters/{reporter.pk}/cancel',
data={},
format='json',
)

self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.json().get('state'), Reporter.REPORTER_STATE_CANCELLED)

log = reporter.history_log.all()[0]
self.assertEqual(log.what, 'UPDATE_REPORTER')
self.assertEqual(log.description, 'Contactgegevens wijziging geannuleerd.')

def test_can_cancel_with_reason(self) -> None:
signal = SignalFactory.create(reporter__state=Reporter.REPORTER_STATE_APPROVED)
reporter = ReporterFactory.create(_signal=signal, state=Reporter.REPORTER_STATE_VERIFICATION_EMAIL_SENT)

reason = 'Wijziging aangevraagd door buurman.'

response = self.client.post(
f'/signals/v1/private/signals/{signal.pk}/reporters/{reporter.pk}/cancel',
data={'reason': reason},
format='json',
)

self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.json().get('state'), Reporter.REPORTER_STATE_CANCELLED)

log = reporter.history_log.all()[0]
self.assertEqual(log.what, 'UPDATE_REPORTER')
self.assertEqual(log.description, f'Contactgegevens wijziging geannuleerd: {reason}')

def test_404_when_signal_not_found(self) -> None:
response = self.client.post(
'/signals/v1/private/signals/11145/reporters/1/cancel',
data={},
format='json',
)

self.assertEqual(response.status_code, 404)

def test_404_when_reporter_not_found(self) -> None:
signal = SignalFactory.create(reporter__state=Reporter.REPORTER_STATE_APPROVED)

response = self.client.post(
f'/signals/v1/private/signals/{signal.pk}/reporters/11145/cancel',
data={},
format='json',
)

self.assertEqual(response.status_code, 404)

def test_400_when_transition_not_allowed(self) -> None:
signal = SignalFactory.create(reporter__state=Reporter.REPORTER_STATE_APPROVED)

response = self.client.post(
f'/signals/v1/private/signals/{signal.pk}/reporters/{signal.reporter.pk}/cancel',
data={},
format='json',
)

self.assertEqual(response.status_code, 400)

body = response.json()
non_field_errors = body.get('non_field_errors')
self.assertIsNotNone(non_field_errors)
self.assertEqual(len(non_field_errors), 1)
self.assertEqual(non_field_errors[0], 'Cancelling this reporter is not possible.')
57 changes: 56 additions & 1 deletion app/signals/apps/api/views/signals/private/signal_reporters.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
# SPDX-License-Identifier: MPL-2.0
# Copyright (C) 2023 Gemeente Amsterdam
from django.db import transaction
from django.utils import timezone
from django_fsm import TransitionNotAllowed
from drf_spectacular.utils import extend_schema
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.mixins import CreateModelMixin, ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework_extensions.mixins import NestedViewSetMixin

from signals.apps.api.filters.signal_reporter import ReporterFilterSet
from signals.apps.api.generics.permissions import ReporterPermission
from signals.apps.api.serializers.signal_reporter import SignalReporterSerializer
from signals.apps.api.serializers.signal_reporter import (
CancelSignalReporterSerializer,
SignalReporterSerializer
)
from signals.apps.history.models import Log
from signals.apps.signals.models import Reporter, Signal
from signals.auth.backend import JWTAuthBackend

Expand All @@ -32,3 +44,46 @@ def get_signal(self) -> Signal:
raise NotFound()

return reporter._signal

@extend_schema(request=CancelSignalReporterSerializer)
@action(methods=['post'], detail=True, url_path='cancel', url_name='private-signal-reporter-cancel')
def cancel(self, request: Request, *args, **kwargs):
"""
Cancel a reporter, this allows cancelling a reporter update.
Cancelling is allowed, when the state of the reporter is "new" or "verification_email_sent"
and the reporter is not the original/first reporter of the signal.
Optionally a reason for the cancellation can be provided using the reason field.
"""
instance = self.get_object()
cancel_serializer = CancelSignalReporterSerializer(data=request.data)
cancel_serializer.is_valid(raise_exception=True)
description = 'Contactgegevens wijziging geannuleerd'
reason = cancel_serializer.validated_data.get('reason')
if reason is None:
description += '.'
else:
description += f': {reason}'

try:
self.perform_cancel(instance)

instance.history_log.create(
action=Log.ACTION_UPDATE,
created_by=request.user,
created_at=timezone.now(),
description=description,
_signal=instance._signal,
)
except TransitionNotAllowed:
return Response(
data={'non_field_errors': ['Cancelling this reporter is not possible.']},
status=status.HTTP_400_BAD_REQUEST,
)

serializer = self.get_serializer(instance)
return Response(serializer.data)

@transaction.atomic()
def perform_cancel(self, instance: Reporter) -> None:
instance.cancel()
instance.save()

0 comments on commit 79f6070

Please sign in to comment.