From 508fcec734b18dfef8b086440bdb0d210482e1c3 Mon Sep 17 00:00:00 2001
From: Valentin Deniaud <vdeniaud@entrouvert.com>
Date: Thu, 29 Jul 2021 11:20:38 +0200
Subject: [PATCH 3/4] api: add endpoint to book multiple events independently
 (#55367)

---
 chrono/api/urls.py         |  5 ++++
 chrono/api/views.py        | 59 ++++++++++++++++++++++++++++++++++++--
 tests/api/test_fillslot.py | 39 +++++++++++++++++++++++++
 3 files changed, 101 insertions(+), 2 deletions(-)

diff --git a/chrono/api/urls.py b/chrono/api/urls.py
index 7ba6019..af9087d 100644
--- a/chrono/api/urls.py
+++ b/chrono/api/urls.py
@@ -33,6 +33,11 @@ urlpatterns = [
         name='api-fillslot',
     ),
     url(r'^agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$', views.fillslots, name='api-agenda-fillslots'),
+    url(
+        r'^agenda/(?P<agenda_identifier>[\w-]+)/events/fillslots/$',
+        views.events_fillslots,
+        name='api-agenda-events-fillslots',
+    ),
     url(
         r'^agenda/(?P<agenda_identifier>[\w-]+)/recurring_fillslots/$',
         views.recurring_fillslots,
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 37dc22c..e54c004 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -1163,7 +1163,7 @@ class SlotsSerializer(SlotSerializer):
         return attrs
 
 
-class EventSlotsSerializer(SlotsSerializer):
+class EventsSlotsSerializer(SlotsSerializer):
     def validate(self, attrs):
         super().validate(attrs)
         if not attrs.get('user_external_id'):
@@ -1551,7 +1551,7 @@ fillslot = Fillslot.as_view()
 
 class RecurringFillslots(APIView):
     permission_classes = (permissions.IsAuthenticated,)
-    serializer_class = EventSlotsSerializer
+    serializer_class = EventsSlotsSerializer
 
     def post(self, request, agenda_identifier):
         if not settings.ENABLE_RECURRING_EVENT_BOOKING:
@@ -1640,6 +1640,61 @@ class RecurringFillslots(APIView):
 recurring_fillslots = RecurringFillslots.as_view()
 
 
+class EventsFillslots(APIView):
+    permission_classes = (permissions.IsAuthenticated,)
+    serializer_class = EventsSlotsSerializer
+
+    def post(self, request, agenda_identifier):
+        agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
+        serializer = self.serializer_class(data=request.data, partial=True)
+        if not serializer.is_valid():
+            raise APIError(
+                _('invalid payload'),
+                err_class='invalid payload',
+                errors=serializer.errors,
+                http_status=status.HTTP_400_BAD_REQUEST,
+            )
+        payload = serializer.validated_data
+
+        events = get_events_from_slots(payload['slots'], request, agenda, payload)
+        events = Event.annotate_queryset(events)
+
+        full_events = [str(event) for event in events.filter(full=True)]
+        if full_events:
+            raise APIError(
+                _('some events are full: %s') % ', '.join(full_events), err_class='some events are full'
+            )
+
+        events = events.annotate(
+            in_waiting_list=ExpressionWrapper(
+                Q(booked_places_count__gte=F('places')) | Q(waiting_list_count__gt=0),
+                output_field=BooleanField(),
+            )
+        )
+        waiting_list_events = [event for event in events if event.in_waiting_list]
+
+        extra_data = {k: v for k, v in request.data.items() if k not in payload}
+        bookings = [make_booking(event, payload, extra_data) for event in events]
+
+        with transaction.atomic():
+            Booking.objects.bulk_create(bookings)
+            events.update(
+                full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
+                | Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
+                almost_full=Q(booked_places_count__gte=0.9 * F('places')),
+            )
+
+        response = {
+            'err': 0,
+            'booking_count': len(bookings),
+            'waiting_list_events': [get_event_detail(request, x, agenda=agenda) for x in waiting_list_events],
+        }
+        return Response(response)
+
+
+events_fillslots = EventsFillslots.as_view()
+
+
 class BookingSerializer(serializers.ModelSerializer):
     user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
 
diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py
index ecf587b..92ec715 100644
--- a/tests/api/test_fillslot.py
+++ b/tests/api/test_fillslot.py
@@ -2224,3 +2224,42 @@ def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
     resp = app.post_json('/api/agenda/%s/recurring_fillslots/' % agenda.slug, params=params)
     assert resp.json['booking_count'] == 5
     assert events.filter(waiting_list_count=2).count() == 5
+
+
+def test_api_events_fillslots(app, user, freezer):
+    freezer.move_to('2021-09-06 12:00')
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    event = Event.objects.create(
+        label='Event',
+        start_datetime=now() + datetime.timedelta(days=1),
+        places=2,
+        waiting_list_places=1,
+        agenda=agenda,
+    )
+    second_event = Event.objects.create(
+        label='Event 2', start_datetime=now() + datetime.timedelta(days=2), places=2, agenda=agenda
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
+    params = {'user_external_id': 'user_id', 'slots': 'event,event-2'}
+    resp = app.post_json(fillslots_url, params=params)
+    assert resp.json['booking_count'] == 2
+    assert len(resp.json['waiting_list_events']) == 0
+
+    events = Event.annotate_queryset(Event.objects.all())
+    assert events.filter(booked_places_count=1).count() == 2
+
+    resp = app.post_json(fillslots_url, params=params)
+    assert resp.json['booking_count'] == 2
+    assert len(resp.json['waiting_list_events']) == 0
+
+    resp = app.post_json(fillslots_url, params=params)
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'some events are full: Event 2'
+
+    params['slots'] = 'event'
+    resp = app.post_json(fillslots_url, params=params)
+    assert resp.json['booking_count'] == 1
+    assert resp.json['waiting_list_events'][0]['slug'] == event.slug
+    assert Booking.objects.filter(in_waiting_list=True, event=event).count() == 1
-- 
2.20.1

