From 76894851c315efe052585b0ed69f1256d2930b88 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 15:22:04 +0200
Subject: [PATCH 3/3] api: show events out of minimal delay (#58286)

---
 chrono/agendas/models.py    |  7 ++++---
 chrono/api/views.py         | 33 +++++++++++++++++++++++++++++----
 tests/api/test_datetimes.py | 25 +++++++++++++++++++++++++
 tests/test_agendas.py       |  3 +++
 4 files changed, 61 insertions(+), 7 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 1115bf4..94a4af4 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -600,7 +600,7 @@ class Agenda(models.Model):
                 for weektime_interval in IntervalSet.simple(*time_period_interval) - closed_hours_by_days:
                     yield SharedTimePeriod.from_weektime_interval(weektime_interval, desks=desks)
 
-    @property
+    @functional.cached_property
     def max_booking_datetime(self):
         if self.maximal_booking_delay is None:
             return None
@@ -618,7 +618,7 @@ class Agenda(models.Model):
         t = t.replace(hour=0, minute=0, second=0, microsecond=0)
         return localtime(t)
 
-    @property
+    @functional.cached_property
     def min_booking_datetime(self):
         if self.minimal_booking_delay is None:
             return None
@@ -651,6 +651,7 @@ class Agenda(models.Model):
         max_start=None,
         user_external_id=None,
         bypass_delays=False,
+        show_out_of_minimal_delay=False,
     ):
         assert self.kind == 'events'
 
@@ -672,7 +673,7 @@ class Agenda(models.Model):
             if not include_full:
                 entries = entries.filter(Q(full=False) | Q(primary_event__isnull=False))
 
-        if not bypass_delays and self.minimal_booking_delay:
+        if not bypass_delays and not show_out_of_minimal_delay and self.minimal_booking_delay:
             min_start = max(self.min_booking_datetime, min_start) if min_start else self.min_booking_datetime
 
         if min_start:
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 36abcba..240e2df 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -420,7 +420,7 @@ def get_event_places(event):
     return places
 
 
-def is_event_disabled(event, min_places=1, disable_booked=True, bookable_events=None):
+def is_event_disabled(event, min_places=1, disable_booked=True, bookable_events=None, bypass_delays=False):
     if disable_booked and getattr(event, 'user_places_count', 0) > 0:
         return True
     if event.start_datetime < now():
@@ -430,6 +430,13 @@ def is_event_disabled(event, min_places=1, disable_booked=True, bookable_events=
             return False
         # we just want to show past events, but they are not bookable
         return True
+    elif (
+        not bypass_delays
+        and event.agenda.min_booking_datetime
+        and event.start_datetime < event.agenda.min_booking_datetime
+    ):
+        # event is out of minimal delay and we don't want to bypass delays
+        return True
     if event.remaining_places < min_places and event.remaining_waiting_list_places < min_places:
         return True
     return False
@@ -466,6 +473,7 @@ def get_event_detail(
     bookable_events=None,
     multiple_agendas=False,
     disable_booked=True,
+    bypass_delays=False,
 ):
     agenda = agenda or event.agenda
     details = {
@@ -496,6 +504,7 @@ def get_event_detail(
                     min_places=min_places,
                     disable_booked=disable_booked,
                     bookable_events=bookable_events,
+                    bypass_delays=bypass_delays,
                 ),
                 'api': {
                     'bookings_url': request.build_absolute_uri(
@@ -538,14 +547,22 @@ def get_event_detail(
 
 
 def get_events_meta_detail(
-    request, events, agenda=None, min_places=1, bookable_events=None, multiple_agendas=False
+    request,
+    events,
+    agenda=None,
+    min_places=1,
+    bookable_events=None,
+    multiple_agendas=False,
+    bypass_delays=False,
 ):
     bookable_datetimes_number_total = 0
     bookable_datetimes_number_available = 0
     first_bookable_slot = None
     for event in events:
         bookable_datetimes_number_total += 1
-        if not is_event_disabled(event, min_places=min_places, bookable_events=bookable_events):
+        if not is_event_disabled(
+            event, min_places=min_places, bookable_events=bookable_events, bypass_delays=bypass_delays
+        ):
             bookable_datetimes_number_available += 1
             if not first_bookable_slot:
                 first_bookable_slot = get_event_detail(
@@ -555,6 +572,7 @@ def get_events_meta_detail(
                     min_places=min_places,
                     bookable_events=bookable_events,
                     multiple_agendas=multiple_agendas,
+                    bypass_delays=bypass_delays,
                 )
     return {
         'no_bookable_datetimes': bool(bookable_datetimes_number_available == 0),
@@ -824,7 +842,11 @@ class Datetimes(APIView):
                 e
                 for e in entries
                 if not is_event_disabled(
-                    e, payload['min_places'], disable_booked=disable_booked, bookable_events=bookable_events
+                    e,
+                    payload['min_places'],
+                    disable_booked=disable_booked,
+                    bookable_events=bookable_events,
+                    bypass_delays=payload.get('bypass_delays'),
                 )
             ]
 
@@ -838,6 +860,7 @@ class Datetimes(APIView):
                     booked_user_external_id=payload.get('user_external_id'),
                     bookable_events=bookable_events_raw,
                     disable_booked=disable_booked,
+                    bypass_delays=payload.get('bypass_delays'),
                 )
                 for x in entries
             ],
@@ -900,6 +923,7 @@ class MultipleAgendasDatetimes(APIView):
                     min_start=payload.get('date_start'),
                     max_start=payload.get('date_end'),
                     bypass_delays=payload.get('bypass_delays'),
+                    show_out_of_minimal_delay=show_past_events,
                 )
             )
 
@@ -915,6 +939,7 @@ class MultipleAgendasDatetimes(APIView):
                     booked_user_external_id=payload.get('user_external_id'),
                     multiple_agendas=True,
                     disable_booked=disable_booked,
+                    bypass_delays=payload.get('bypass_delays'),
                 )
                 for x in entries
             ],
diff --git a/tests/api/test_datetimes.py b/tests/api/test_datetimes.py
index d43c468..b3f89af 100644
--- a/tests/api/test_datetimes.py
+++ b/tests/api/test_datetimes.py
@@ -694,15 +694,19 @@ def test_recurring_events_api(app, user, freezer):
     # check delays
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
     assert len(resp.json['data']) == 4
+    assert [e['disabled'] for e in resp.json['data']] == [False] * 4
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'bypass_delays': True})
     assert len(resp.json['data']) == 53
+    assert [e['disabled'] for e in resp.json['data']] == [False] * 53
     agenda.minimal_booking_delay = 10
     agenda.mmaximal_booking_delay = 20
     agenda.save()
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
     assert len(resp.json['data']) == 3
+    assert [e['disabled'] for e in resp.json['data']] == [False] * 3
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params={'bypass_delays': True})
     assert len(resp.json['data']) == 53
+    assert [e['disabled'] for e in resp.json['data']] == [False] * 53
 
 
 def test_recurring_events_api_various_times(app, user, mock_now):
@@ -1498,11 +1502,20 @@ def test_datetimes_multiple_agendas(app):
     date_end = localtime() + datetime.timedelta(days=60)  # with a date end to have recurring events
     resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'date_end': date_end})
     assert len(resp.json['data']) == 2
+    assert [d['disabled'] for d in resp.json['data']] == [False, False]
+    assert resp.json['data'][0]['id'] == 'second-agenda@event'
+    assert resp.json['data'][1]['id'] == 'first-agenda@recurring:2021-05-13-1700'
     resp = app.get(
         '/api/agendas/datetimes/',
         params={'agendas': agenda_slugs, 'date_end': date_end, 'bypass_delays': True},
     )
     assert len(resp.json['data']) == 5
+    assert resp.json['data'][0]['id'] == 'first-agenda@recurring:2021-05-06-1700'
+    assert resp.json['data'][1]['id'] == 'first-agenda@event'
+    assert resp.json['data'][2]['id'] == 'second-agenda@event'
+    assert resp.json['data'][3]['id'] == 'first-agenda@recurring:2021-05-13-1700'
+    assert resp.json['data'][4]['id'] == 'first-agenda@recurring:2021-05-20-1700'
+    assert [d['disabled'] for d in resp.json['data']] == [False, False, False, False, False]
     Agenda.objects.update(minimal_booking_delay=0, maximal_booking_delay=45)
 
     # invalid slugs
@@ -1520,6 +1533,18 @@ def test_datetimes_multiple_agendas(app):
     assert resp.json['data'][2]['id'] == 'second-agenda@event'
     assert resp.json['data'][3]['id'] == 'first-agenda@recurring:2021-05-13-1700'
     assert resp.json['data'][4]['id'] == 'first-agenda@recurring:2021-05-20-1700'
+    assert [d['disabled'] for d in resp.json['data']] == [False, False, False, False, False]
+
+    # and events out of minimal_booking_delay
+    Agenda.objects.update(minimal_booking_delay=6, maximal_booking_delay=14)
+    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
+    assert len(resp.json['data']) == 4
+    assert resp.json['data'][0]['id'] == 'first-agenda@recurring:2021-05-06-1700'
+    assert resp.json['data'][1]['id'] == 'first-agenda@event'
+    assert resp.json['data'][2]['id'] == 'second-agenda@event'
+    assert resp.json['data'][3]['id'] == 'first-agenda@recurring:2021-05-13-1700'
+    assert [d['disabled'] for d in resp.json['data']] == [True, True, False, False]
+    Agenda.objects.update(minimal_booking_delay=0, maximal_booking_delay=45)
 
     Event.objects.create(
         slug='event-in-past',
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index 0866641..5714fa2 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -206,14 +206,17 @@ def test_agenda_minimal_booking_delay_in_working_days(settings):
     assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
     agenda.minimal_booking_delay_in_working_days = False
     agenda.save()
+    del agenda.min_booking_datetime
     assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
 
     settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France'
     agenda.minimal_booking_delay_in_working_days = True
     agenda.save()
+    del agenda.min_booking_datetime
     assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 12)
     agenda.minimal_booking_delay_in_working_days = False
     agenda.save()
+    del agenda.min_booking_datetime
     assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
 
 
-- 
2.33.0

