From 92a31b7f386385ef5cd95eb23f954ff56b3eca6e Mon Sep 17 00:00:00 2001 From: Dirk Doesburg Date: Wed, 26 Oct 2022 20:06:42 +0200 Subject: [PATCH] Add mark-present api (#2610) * Add mark-present api Adds an api view basically identical to that at the mark-present-url, but using PATCH. And adds the mark_present_url to the event admin serializer. * Fix linting --- .../events/api/v2/admin/serializers/event.py | 1 + website/events/api/v2/urls.py | 6 ++ website/events/api/v2/views.py | 39 ++++++++++ website/events/tests/test_api.py | 74 +++++++++++++++++++ website/sales/api/v2/views.py | 4 +- 5 files changed, 123 insertions(+), 1 deletion(-) diff --git a/website/events/api/v2/admin/serializers/event.py b/website/events/api/v2/admin/serializers/event.py index 628a58f36..a475bd9ff 100644 --- a/website/events/api/v2/admin/serializers/event.py +++ b/website/events/api/v2/admin/serializers/event.py @@ -24,6 +24,7 @@ class Meta: description = CleanedHTMLSerializer() price = PaymentAmountSerializer() fine = PaymentAmountSerializer() + mark_present_url = serializers.ReadOnlyField() def to_internal_value(self, data): self.fields["organisers"] = serializers.PrimaryKeyRelatedField( diff --git a/website/events/api/v2/urls.py b/website/events/api/v2/urls.py index 4e86746e8..1c7abfbd7 100644 --- a/website/events/api/v2/urls.py +++ b/website/events/api/v2/urls.py @@ -9,6 +9,7 @@ EventRegistrationsView, ExternalEventDetailView, ExternalEventListView, + MarkPresentAPIView, ) app_name = "events" @@ -35,6 +36,11 @@ EventRegistrationFieldsView.as_view(), name="event-registration-fields", ), + path( + "events//mark-present//", + MarkPresentAPIView.as_view(), + name="mark-present", + ), path( "events/external/", ExternalEventListView.as_view(), name="external-events-list" ), diff --git a/website/events/api/v2/views.py b/website/events/api/v2/views.py index 9716b9d0b..85debe84d 100644 --- a/website/events/api/v2/views.py +++ b/website/events/api/v2/views.py @@ -1,4 +1,5 @@ from django.db.models import Count, Q +from django.utils import timezone from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope from rest_framework import filters as framework_filters @@ -21,6 +22,7 @@ from events.exceptions import RegistrationError from events.models import Event, EventRegistration from events.models.external_event import ExternalEvent +from events.services import is_user_registered from thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod from thaliawebsite.api.v2.serializers import EmptySerializer @@ -275,3 +277,40 @@ class ExternalEventDetailView(RetrieveAPIView): queryset = ExternalEvent.objects.filter(published=True) permission_classes = [IsAuthenticatedOrTokenHasScope] required_scopes = ["events:read"] + + +class MarkPresentAPIView(APIView): + """A view that allows uses to mark their presence at an event using a secret token.""" + + def patch(self, request, *args, **kwargs): + """Mark a user as present. + + Checks if the url is correct, the event has not ended yet, and the user is registered. + """ + event = get_object_or_404(Event, pk=kwargs["pk"]) + if kwargs["token"] != event.mark_present_url_token: + raise PermissionDenied(detail="Invalid url.") + + if not request.member or not is_user_registered(request.member, event): + raise PermissionDenied(detail="You are not registered for this event.") + + registration = event.registrations.get( + member=request.member, date_cancelled=None + ) + + if registration.present: + return Response( + data={"detail": "You were already marked as present."}, + status=status.HTTP_200_OK, + ) + if event.end < timezone.now(): + raise PermissionDenied( + detail="This event has already ended.", + ) + + registration.present = True + registration.save() + return Response( + data={"detail": "You have been marked as present."}, + status=status.HTTP_200_OK, + ) diff --git a/website/events/tests/test_api.py b/website/events/tests/test_api.py index 71d8db12d..ad5adc406 100644 --- a/website/events/tests/test_api.py +++ b/website/events/tests/test_api.py @@ -1,6 +1,7 @@ import datetime from django.test import TestCase, override_settings +from django.urls import reverse from django.utils import timezone from rest_framework.test import APIClient @@ -40,6 +41,14 @@ def setUpTestData(cls): cls.event.organisers.add(Committee.objects.get(pk=1)) cls.member = Member.objects.filter(last_name="Wiggers").first() + cls.mark_present_api_url = reverse( + "api:v2:events:mark-present", + kwargs={ + "pk": cls.event.pk, + "token": cls.event.mark_present_url_token, + }, + ) + def setUp(self): self.client = APIClient() self.client.force_login(self.member) @@ -354,3 +363,68 @@ def test_registration_organiser(self): reg2.refresh_from_db() self.assertEqual(reg2.payment.type, "cash_payment") self.assertFalse(reg2.present) + + def test_mark_present_url_registered(self): + registration = EventRegistration.objects.create( + event=self.event, + member=self.member, + date=timezone.now() - datetime.timedelta(hours=1), + ) + + response = self.client.patch(self.mark_present_api_url, follow=True) + self.assertContains(response, "You have been marked as present.") + registration.refresh_from_db() + self.assertTrue(registration.present) + + def test_mark_present_url_already_present(self): + registration = EventRegistration.objects.create( + event=self.event, + member=self.member, + date=timezone.now() - datetime.timedelta(hours=1), + present=True, + ) + + response = self.client.patch(self.mark_present_api_url, follow=True) + self.assertContains(response, "You were already marked as present.") + registration.refresh_from_db() + self.assertTrue(registration.present) + + def test_mark_present_url_not_registered(self): + response = self.client.patch(self.mark_present_api_url, follow=True) + self.assertContains( + response, "You are not registered for this event.", status_code=403 + ) + + def test_mark_present_url_wrong_token(self): + registration = EventRegistration.objects.create( + event=self.event, + member=self.member, + date=timezone.now() - datetime.timedelta(hours=3), + ) + response = self.client.patch( + reverse( + "api:v2:events:mark-present", + kwargs={ + "pk": self.event.pk, + "token": "11111111-2222-3333-4444-555555555555", + }, + ), + follow=True, + ) + + self.assertContains(response, "Invalid url.", status_code=403) + self.assertFalse(registration.present) + + def test_mark_present_url_past_event(self): + registration = EventRegistration.objects.create( + event=self.event, + member=self.member, + date=timezone.now() - datetime.timedelta(hours=3), + ) + self.event.start = timezone.now() - datetime.timedelta(hours=2) + self.event.end = timezone.now() - datetime.timedelta(hours=1) + self.event.save() + response = self.client.patch(self.mark_present_api_url, follow=True) + + self.assertContains(response, "This event has already ended.", status_code=403) + self.assertFalse(registration.present) diff --git a/website/sales/api/v2/views.py b/website/sales/api/v2/views.py index fcc215353..f861a2f12 100644 --- a/website/sales/api/v2/views.py +++ b/website/sales/api/v2/views.py @@ -128,7 +128,9 @@ def get_request_serializer(self, path, method): def patch(self, request, *args, **kwargs): if request.member is None: - raise PermissionDenied("You need to be a member to pay for an order.") + raise PermissionDenied( + detail="You need to be a member to pay for an order." + ) order = self.get_object() if order.payment: