From bcc985708d272577bd1cdc5a69f86d273bad6f03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 30 Nov 2020 09:51:48 +0100
Subject: [PATCH] api: mark user as present or not (#38678)

---
 .../migrations/0071_booking_attendance.py     |  19 +++
 chrono/agendas/models.py                      |   2 +
 chrono/api/views.py                           |  71 ++++++++++-
 tests/test_api.py                             | 111 ++++++++++++++++++
 4 files changed, 200 insertions(+), 3 deletions(-)
 create mode 100644 chrono/agendas/migrations/0071_booking_attendance.py

diff --git a/chrono/agendas/migrations/0071_booking_attendance.py b/chrono/agendas/migrations/0071_booking_attendance.py
new file mode 100644
index 0000000..6b79a4f
--- /dev/null
+++ b/chrono/agendas/migrations/0071_booking_attendance.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0070_auto_20201202_1834'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='booking',
+            name='user_was_present',
+            field=models.NullBooleanField(),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index aa8c06a..22b0881 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -1098,6 +1098,8 @@ class Booking(models.Model):
     user_name = models.CharField(max_length=250, blank=True)
     user_email = models.EmailField(blank=True)
     user_phone_number = models.CharField(max_length=16, blank=True)
+    user_was_present = models.NullBooleanField()
+
     form_url = models.URLField(blank=True)
     backoffice_url = models.URLField(blank=True)
     cancel_callback_url = models.URLField(blank=True)
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 13b988f..7d9f624 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -1162,16 +1162,81 @@ class Fillslot(Fillslots):
 fillslot = Fillslot.as_view()
 
 
+class BookingSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Booking
+        fields = ['user_was_present']
+
+
 class BookingAPI(APIView):
     permission_classes = (permissions.IsAuthenticated,)
+    serializer_class = BookingSerializer
 
     def initial(self, request, *args, **kwargs):
-        super(BookingAPI, self).initial(request, *args, **kwargs)
-        self.booking = Booking.objects.get(id=kwargs.get('booking_pk'), cancellation_datetime__isnull=True)
+        super().initial(request, *args, **kwargs)
+        self.booking = get_object_or_404(Booking, pk=kwargs.get('booking_pk'))
+
+    def check_booking(self, check_waiting_list=False):
+        if self.booking.cancellation_datetime:
+            return Response(
+                {'err': 1, 'err_class': 'booking is cancelled', 'err_desc': _('booking is cancelled')}
+            )
+
+        if self.booking.primary_booking is not None:
+            return Response({'err': 2, 'err_class': 'secondary booking', 'err_desc': _('secondary booking')})
+
+        if check_waiting_list and self.booking.in_waiting_list:
+            response = {
+                'err': 3,
+                'err_class': 'booking is in waiting list',
+                'err_desc': _('booking is in waiting list'),
+            }
+            return Response(response)
+
+    def get(self, request, *args, **kwargs):
+        response = self.check_booking()
+        if response:
+            return response
+
+        response = {
+            'err': 0,
+            'booking_id': self.booking.pk,
+            'in_waiting_list': self.booking.in_waiting_list,
+            'user_was_present': self.booking.user_was_present,
+        }
+        return Response(response)
+
+    def patch(self, request, *args, **kwargs):
+        response = self.check_booking(check_waiting_list=True)
+        if response:
+            return response
+
+        serializer = self.serializer_class(self.booking, data=request.data, partial=True)
+
+        if not serializer.is_valid():
+            return Response(
+                {
+                    'err': 4,
+                    'err_class': 'invalid payload',
+                    'err_desc': _('invalid payload'),
+                    'errors': serializer.errors,
+                },
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+
+        serializer.save()
+        self.booking.secondary_booking_set.update(user_was_present=self.booking.user_was_present)
+
+        response = {'err': 0, 'booking_id': self.booking.pk}
+        return Response(response)
 
     def delete(self, request, *args, **kwargs):
+        response = self.check_booking()
+        if response:
+            return response
+
         self.booking.cancel()
-        response = {'err': 0, 'booking_id': self.booking.id}
+        response = {'err': 0, 'booking_id': self.booking.pk}
         return Response(response)
 
 
diff --git a/tests/test_api.py b/tests/test_api.py
index b55b6c6..294080a 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1904,6 +1904,117 @@ def test_booking_api_with_cancel_booking(app, some_data, user):
     assert Booking.objects.count() == 6
 
 
+@pytest.mark.parametrize('flag', [True, False, None])
+def test_booking_api_present(app, user, flag):
+    agenda = Agenda.objects.create(kind='events')
+    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+    booking = Booking.objects.create(event=event, user_was_present=flag)
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/booking/%s/' % booking.pk)
+    assert resp.json['booking_id'] == booking.pk
+    assert resp.json['user_was_present'] == flag
+
+
+@pytest.mark.parametrize('flag', [True, False])
+def test_booking_api_waiting_list(app, user, flag):
+    agenda = Agenda.objects.create(kind='events')
+    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+    booking = Booking.objects.create(event=event, in_waiting_list=flag)
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    resp = app.get('/api/booking/%s/' % booking.pk)
+    assert resp.json['booking_id'] == booking.pk
+    assert resp.json['in_waiting_list'] == flag
+
+
+def test_booking_api_error(app, user):
+    agenda = Agenda.objects.create(kind='events')
+    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+    booking = Booking.objects.create(event=event)
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    # unknown
+    booking.cancel()
+    for method in ['get', 'delete', 'patch']:
+        getattr(app, method)('/api/booking/0/', status=404)
+
+    # cancelled
+    booking.cancel()
+    for method in ['get', 'delete', 'patch']:
+        resp = getattr(app, method)('/api/booking/%s/' % booking.pk)
+        assert resp.json['err'] == 1
+        assert resp.json['err_desc'] == 'booking is cancelled'
+
+    # not a primary booking
+    secondary = Booking.objects.create(event=event, primary_booking=booking)
+    for method in ['get', 'delete', 'patch']:
+        resp = getattr(app, method)('/api/booking/%s/' % secondary.pk)
+        assert resp.json['err'] == 2
+        assert resp.json['err_desc'] == 'secondary booking'
+
+    # in waiting list
+    booking.cancellation_datetime = None
+    booking.in_waiting_list = True
+    booking.save()
+    resp = app.get('/api/booking/%s/' % booking.pk)
+    assert resp.json['err'] == 0
+    resp = app.patch('/api/booking/%s/' % booking.pk)
+    assert resp.json['err'] == 3
+    assert resp.json['err_desc'] == 'booking is in waiting list'
+    resp = app.delete('/api/booking/%s/' % booking.pk)
+    assert resp.json['err'] == 0
+
+
+def test_booking_patch_api(app, user):
+    agenda = Agenda.objects.create(kind='events')
+    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+    booking = Booking.objects.create(event=event)
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    resp = app.patch('/api/booking/%s/' % booking.pk)
+    assert resp.json['err'] == 0
+    resp = app.patch('/api/booking/%s/' % booking.pk, params={'user_was_present': 'foobar'}, status=400)
+    assert resp.json['err'] == 4
+    assert resp.json['err_desc'] == 'invalid payload'
+
+
+@pytest.mark.parametrize('flag', [True, False, None])
+def test_booking_patch_api_present(app, user, flag):
+    agenda = Agenda.objects.create(kind='events')
+    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+    booking = Booking.objects.create(event=event)
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    # set flag
+    app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
+    booking.refresh_from_db()
+    assert booking.user_was_present == flag
+
+    # reset
+    app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': None})
+    booking.refresh_from_db()
+    assert booking.user_was_present is None
+
+    # make secondary bookings
+    Booking.objects.create(event=event, primary_booking=booking)
+    Booking.objects.create(event=event, primary_booking=booking)
+    # and other booking
+    other_booking = Booking.objects.create(event=event)
+
+    app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
+    booking.refresh_from_db()
+    assert booking.user_was_present == flag
+    # all secondary bookings are upadted
+    assert list(booking.secondary_booking_set.values_list('user_was_present', flat=True)) == [flag, flag]
+    other_booking.refresh_from_db()
+    assert other_booking.user_was_present is None  # not changed
+
+
 def test_booking_cancellation_api(app, some_data, user):
     agenda = Agenda.objects.filter(label=u'Foo bar')[0]
     event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
-- 
2.29.2

