From b660a420bc9c34d7e3b3ac151ff982bef781aecc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 29 Oct 2021 10:40:42 +0200
Subject: [PATCH 3/3] api: bypass_delays parameter for event fillslot endpoints
 (#57961)

---
 chrono/agendas/models.py   |  16 +++--
 chrono/api/serializers.py  |   3 +-
 chrono/api/views.py        |   3 +-
 tests/api/test_fillslot.py | 131 +++++++++++++++++++++++++++++++++++++
 4 files changed, 146 insertions(+), 7 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 3811cc4..1115bf4 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -1451,19 +1451,25 @@ class Event(models.Model):
         self.checked = True
         self.save(update_fields=['checked'])
 
-    def in_bookable_period(self):
+    def in_bookable_period(self, bypass_delays=False):
         if self.publication_datetime and now() < self.publication_datetime:
             return False
-        if self.agenda.maximal_booking_delay and self.start_datetime > self.agenda.max_booking_datetime:
+        if (
+            not bypass_delays
+            and self.agenda.maximal_booking_delay
+            and self.start_datetime > self.agenda.max_booking_datetime
+        ):
             return False
         if self.recurrence_days is not None:
             # bookable recurrences probably exist
             return True
-        if self.agenda.minimal_booking_delay and self.start_datetime < self.agenda.min_booking_datetime:
+        if (
+            not bypass_delays
+            and self.agenda.minimal_booking_delay
+            and self.start_datetime < self.agenda.min_booking_datetime
+        ):
             return False
         if self.start_datetime < now():
-            # past the event date, we may want in the future to allow for some
-            # extra late booking but it's forbidden for now.
             return False
         return True
 
diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py
index 72ebe4f..ee21f2d 100644
--- a/chrono/api/serializers.py
+++ b/chrono/api/serializers.py
@@ -33,6 +33,7 @@ class SlotSerializer(serializers.Serializer):
     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)
+    bypass_delays = serializers.BooleanField(default=False)
     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)
@@ -57,7 +58,7 @@ class EventsSlotsSerializer(SlotSerializer):
 
     def validate(self, attrs):
         super().validate(attrs)
-        if not 'slots' in attrs:
+        if 'slots' not in attrs:
             raise serializers.ValidationError({'slots': _('This field is required.')})
         if not attrs.get('user_external_id'):
             raise serializers.ValidationError({'user_external_id': _('This field is required.')})
diff --git a/chrono/api/views.py b/chrono/api/views.py
index bbc9f2f..36abcba 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -598,6 +598,7 @@ def get_events_from_slots(slots, request, agenda, payload):
     book_events = payload.get('events') or request.query_params.get('events') or 'future'
     book_past = book_events in ['all', 'past']
     book_future = book_events in ['all', 'future']
+    bypass_delays = payload.get('bypass_delays')
 
     # convert event recurrence identifiers to real event slugs
     for i, slot in enumerate(slots.copy()):
@@ -613,7 +614,7 @@ def get_events_from_slots(slots, request, agenda, payload):
 
     for event in events:
         if event.start_datetime >= now():
-            if not book_future or not event.in_bookable_period():
+            if not book_future or not event.in_bookable_period(bypass_delays=bypass_delays):
                 raise APIError(_('event %s is not bookable') % event.slug, err_class='event not bookable')
         else:
             if not book_past:
diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py
index 545571b..dd97d59 100644
--- a/tests/api/test_fillslot.py
+++ b/tests/api/test_fillslot.py
@@ -168,6 +168,43 @@ def test_booking_api(app, some_data, user):
     resp = app.post('/api/agenda/0/fillslot/%s/' % event.id, status=404)
 
 
+def test_booking_api_check_delays(app, user):
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
+    )
+    event = Event.objects.create(
+        slug='event-slug',
+        start_datetime=localtime() + datetime.timedelta(days=5),
+        places=5,
+        agenda=agenda,
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
+    assert resp.json['err'] == 0
+
+    # test minimal_booking_delay
+    agenda.minimal_booking_delay = 6
+    agenda.save()
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event not bookable'
+    agenda.save()
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'bypass_delays': True})
+    assert resp.json['err'] == 0
+
+    # test maximal_booking_delay
+    agenda.minimal_booking_delay = 0
+    agenda.maximal_booking_delay = 3
+    agenda.save()
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event not bookable'
+    agenda.save()
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'bypass_delays': True})
+    assert resp.json['err'] == 0
+
+
 @pytest.mark.freeze_time('2021-02-23')
 def test_booking_api_exclude_slots(app, user):
     agenda = Agenda.objects.create(
@@ -2398,6 +2435,48 @@ def test_api_events_fillslots(app, user):
     resp = app.post('/api/agenda/0/events/fillslots/', status=404)
 
 
+def test_api_events_fillslots_check_delays(app, user):
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
+    )
+    Event.objects.create(
+        slug='event-slug',
+        start_datetime=localtime() + datetime.timedelta(days=5),
+        places=5,
+        agenda=agenda,
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
+    resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
+    assert resp.json['err'] == 0
+
+    # test minimal_booking_delay
+    agenda.minimal_booking_delay = 6
+    agenda.save()
+    resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event not bookable'
+    agenda.save()
+    resp = app.post(
+        fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug', 'bypass_delays': True}
+    )
+    assert resp.json['err'] == 0
+
+    # test maximal_booking_delay
+    agenda.minimal_booking_delay = 0
+    agenda.maximal_booking_delay = 3
+    agenda.save()
+    resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event not bookable'
+    agenda.save()
+    resp = app.post(
+        fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug', 'bypass_delays': True}
+    )
+    assert resp.json['err'] == 0
+
+
 @pytest.mark.freeze_time('2021-09-06 12:00')
 def test_api_events_fillslots_past_event(app, user):
     app.authorization = ('Basic', ('john.doe', 'password'))
@@ -2512,6 +2591,58 @@ def test_api_events_fillslots_multiple_agendas(app, user):
     )
 
 
+def test_api_events_fillslots_multiple_agendas_check_delays(app, user):
+    agenda = Agenda.objects.create(
+        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
+    )
+    Event.objects.create(
+        slug='event-slug',
+        start_datetime=localtime() + datetime.timedelta(days=5),
+        places=5,
+        agenda=agenda,
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post(
+        '/api/agendas/events/fillslots/?agendas=foo-bar',
+        params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug'},
+    )
+    assert resp.json['err'] == 0
+
+    # test minimal_booking_delay
+    agenda.minimal_booking_delay = 6
+    agenda.save()
+    resp = app.post(
+        '/api/agendas/events/fillslots/?agendas=foo-bar',
+        params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug'},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event not bookable'
+    agenda.save()
+    resp = app.post(
+        '/api/agendas/events/fillslots/?agendas=foo-bar',
+        params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug', 'bypass_delays': True},
+    )
+    assert resp.json['err'] == 0
+
+    # test maximal_booking_delay
+    agenda.minimal_booking_delay = 0
+    agenda.maximal_booking_delay = 3
+    agenda.save()
+    resp = app.post(
+        '/api/agendas/events/fillslots/?agendas=foo-bar',
+        params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug'},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event not bookable'
+    agenda.save()
+    resp = app.post(
+        '/api/agendas/events/fillslots/?agendas=foo-bar',
+        params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug', 'bypass_delays': True},
+    )
+    assert resp.json['err'] == 0
+
+
 def test_url_translation(app, some_data, user):
     app.authorization = ('Basic', ('john.doe', 'password'))
     agenda_id = Agenda.objects.filter(label='Foo bar')[0].id
-- 
2.33.0

