From 3356373663561c09f029c361fdd5575acebe31a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 4 Jun 2021 10:35:57 +0200
Subject: [PATCH 2/2] api: fillslot & past events (#48397)

---
 chrono/api/views.py        |  55 +++++-----
 tests/api/test_fillslot.py | 200 +++++++++++++++++++++++++++++++++++++
 2 files changed, 232 insertions(+), 23 deletions(-)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index d03a4cb..0a1f8d7 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -951,6 +951,7 @@ class SlotSerializer(serializers.Serializer):
     user_email = serializers.CharField(max_length=250, allow_blank=True)
     user_phone_number = serializers.CharField(max_length=16, allow_blank=True)
     exclude_user = serializers.BooleanField(default=False)
+    events = serializers.CharField(max_length=16, allow_blank=True)
     form_url = serializers.CharField(max_length=250, allow_blank=True)
     backoffice_url = serializers.URLField(allow_blank=True)
     cancel_callback_url = serializers.URLField(allow_blank=True)
@@ -1086,6 +1087,9 @@ class Fillslots(APIView):
         color = None
         user_external_id = payload.get('user_external_id') or None
         exclude_user = payload.get('exclude_user')
+        book_events = payload.get('events') or 'future'
+        book_past = book_events in ['all', 'past']
+        book_future = book_events in ['all', 'future']
 
         if agenda.accept_meetings():
             # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
@@ -1241,8 +1245,12 @@ class Fillslots(APIView):
                 events = agenda.event_set.filter(slug__in=slots).order_by('start_datetime')
 
             for event in events:
-                if not event.in_bookable_period():
-                    raise APIError(_('event not bookable'), err_class='event not bookable')
+                if event.start_datetime >= now():
+                    if not book_future or not event.in_bookable_period():
+                        raise APIError(_('event not bookable'), err_class='event not bookable')
+                else:
+                    if not book_past:
+                        raise APIError(_('event not bookable'), err_class='event not bookable')
                 if event.cancelled:
                     raise APIError(_('event is cancelled'), err_class='event is cancelled')
                 if exclude_user and user_external_id:
@@ -1261,32 +1269,33 @@ class Fillslots(APIView):
         # search free places. Switch to waiting list if necessary.
         in_waiting_list = False
         for event in events:
-            if payload.get('force_waiting_list') and not event.waiting_list_places:
-                raise APIError(
-                    _('no waiting list'),
-                    err_class='no waiting list',
-                )
+            if event.start_datetime > now():
+                if payload.get('force_waiting_list') and not event.waiting_list_places:
+                    raise APIError(
+                        _('no waiting list'),
+                        err_class='no waiting list',
+                    )
 
-            if event.waiting_list_places:
-                if (
-                    payload.get('force_waiting_list')
-                    or (event.booked_places + places_count) > event.places
-                    or event.waiting_list
-                ):
-                    # if this is full or there are people waiting, put new bookings
-                    # in the waiting list.
-                    in_waiting_list = True
-                    if (event.waiting_list + places_count) > event.waiting_list_places:
+                if event.waiting_list_places:
+                    if (
+                        payload.get('force_waiting_list')
+                        or (event.booked_places + places_count) > event.places
+                        or event.waiting_list
+                    ):
+                        # if this is full or there are people waiting, put new bookings
+                        # in the waiting list.
+                        in_waiting_list = True
+                        if (event.waiting_list + places_count) > event.waiting_list_places:
+                            raise APIError(
+                                _('sold out'),
+                                err_class='sold out',
+                            )
+                else:
+                    if (event.booked_places + places_count) > event.places:
                         raise APIError(
                             _('sold out'),
                             err_class='sold out',
                         )
-            else:
-                if (event.booked_places + places_count) > event.places:
-                    raise APIError(
-                        _('sold out'),
-                        err_class='sold out',
-                    )
 
         with transaction.atomic():
             if to_cancel_booking:
diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py
index fce38b2..b35a01e 100644
--- a/tests/api/test_fillslot.py
+++ b/tests/api/test_fillslot.py
@@ -1890,3 +1890,203 @@ def test_duration_on_booking_api_fillslots_response(app, user):
     ics = app.get(resp.json['api']['ics_url']).text
     assert 'DTSTART:20170519T231200Z' in ics
     assert 'DTEND:20170520T004200Z' in ics
+
+
+def test_fillslot_past_event(app, user):
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
+    )
+    event1 = Event.objects.create(
+        label='Today before now',
+        start_datetime=localtime(now() - datetime.timedelta(hours=1)),
+        places=5,
+        agenda=agenda,
+    )
+    event2 = Event.objects.create(
+        label='Today after now',
+        start_datetime=localtime(now() + datetime.timedelta(hours=1)),
+        places=5,
+        agenda=agenda,
+    )
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug))
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event not bookable'
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'future'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event not bookable'
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'past'})
+    assert resp.json['err'] == 0
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'all'})
+    assert resp.json['err'] == 0
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug))
+    assert resp.json['err'] == 0
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'future'})
+    assert resp.json['err'] == 0
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'past'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event not bookable'
+
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'all'})
+    assert resp.json['err'] == 0
+
+    # check canceled
+    event1.cancel()
+    event2.cancel()
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'all'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event is cancelled'
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'all'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event is cancelled'
+
+
+def test_fillslot_past_event_places(app, user):
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
+    )
+    event = Event.objects.create(
+        label='Today before now',
+        start_datetime=localtime(now() - datetime.timedelta(hours=1)),
+        places=1,
+        agenda=agenda,
+    )
+    Booking.objects.create(event=event)
+
+    # always bookable if past
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'events': 'past'})
+    assert resp.json['err'] == 0
+
+    event.waiting_list_places = 1
+    event.save()
+    Booking.objects.create(event=event, in_waiting_list=True)
+    # always bookable if past
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'events': 'past'})
+    assert resp.json['err'] == 0
+
+
+def test_fillslot_past_events_min_places(app, user):
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
+    )
+    event = Event.objects.create(
+        label='Today before now',
+        start_datetime=localtime(now() - datetime.timedelta(hours=1)),
+        places=1,
+        agenda=agenda,
+    )
+
+    # always bookable if past
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'events': 'past', 'count': 2}
+    )
+    assert resp.json['err'] == 0
+
+
+def test_fillslot_past_events_exclude_slots(app, user):
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
+    )
+    event = Event.objects.create(
+        label='Today before now',
+        start_datetime=localtime(now() - datetime.timedelta(hours=1)),
+        places=5,
+        agenda=agenda,
+    )
+    Booking.objects.create(event=event, user_external_id='42')
+
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
+        params={'events': 'past', 'user_external_id': '35', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 0
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
+        params={'events': 'past', 'user_external_id': '35', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event is already booked by user'
+
+
+def test_fillslot_past_events_recurring_event(app, user):
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
+    )
+    event = Event.objects.create(
+        label='Recurring',
+        start_datetime=localtime(now() - datetime.timedelta(days=3 * 7)),
+        repeat='weekly',
+        places=5,
+        agenda=agenda,
+    )
+
+    event_slug = '%s:%s' % (
+        event.slug,
+        (event.start_datetime - datetime.timedelta(days=7)).strftime('%Y-%m-%d-%H%M'),
+    )  # too soon
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'}, status=400
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid datetime for event %s' % event_slug
+
+    event_slug = '%s:%s' % (
+        event.slug,
+        (event.start_datetime + datetime.timedelta(days=7)).strftime('%Y-%m-%d-%H%M'),
+    )
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'past'})
+    assert resp.json['err'] == 0
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'})
+    assert resp.json['err'] == 0
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'future'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event not bookable'
+
+    event_slug = '%s:%s' % (
+        event.slug,
+        (event.start_datetime + datetime.timedelta(days=50 * 7)).strftime('%Y-%m-%d-%H%M'),
+    )  # too late
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event not bookable'
+
+    event_slug = '%s:%s' % (
+        event.slug,
+        (event.start_datetime + datetime.timedelta(days=4 * 7)).strftime('%Y-%m-%d-%H%M'),
+    )
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'past'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event not bookable'
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'})
+    assert resp.json['err'] == 0
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'future'})
+    assert resp.json['err'] == 0
+
+    # check exclude_user_external_id
+    first_recurrence = event.get_or_create_event_recurrence(event.start_datetime)
+    Booking.objects.create(event=first_recurrence, user_external_id='42')
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, first_recurrence.slug),
+        params={'events': 'past', 'user_external_id': '42', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event is already booked by user'
+
+    # check canceled
+    first_recurrence.cancel()
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, first_recurrence.slug), params={'events': 'past'}
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'event is cancelled'
-- 
2.30.2

