From b815b6cb8649b7ae66d904b397585876d93c587a Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Mon, 2 Nov 2020 13:55:48 +0100
Subject: [PATCH] agendas: add date filter params on datetimes api (#48078)

---
 chrono/api/views.py | 58 +++++++++++++++++++++++++++--------
 tests/test_api.py   | 75 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 121 insertions(+), 12 deletions(-)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index cde824d..79e0fde 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -64,25 +64,38 @@ def format_response_datetime(dt):
     return localtime(dt).strftime('%Y-%m-%d %H:%M:%S')
 
 
-def get_min_datetime(agenda):
-    if agenda.minimal_booking_delay is None:
+def get_min_datetime(agenda, start_datetime=None):
+    if agenda.minimal_booking_delay is None and start_datetime is None:
         return None
+    elif agenda.minimal_booking_delay is None:
+        return start_datetime
+
     min_datetime = now() + datetime.timedelta(days=agenda.minimal_booking_delay)
-    return min_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
+    min_datetime = min_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
+    if start_datetime is None:
+        return min_datetime
+    return max(min_datetime, start_datetime)
 
 
-def get_max_datetime(agenda):
-    if agenda.maximal_booking_delay is None:
+def get_max_datetime(agenda, end_datetime=None):
+    if agenda.maximal_booking_delay is None and end_datetime is None:
         return None
+    elif agenda.maximal_booking_delay is None:
+        return end_datetime
+
     max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay)
     max_datetime = max_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
-    return max_datetime
+    if end_datetime is None:
+        return max_datetime
+    return min(max_datetime, end_datetime)
 
 
 TimeSlot = collections.namedtuple('TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk'])
 
 
-def get_all_slots(base_agenda, meeting_type, resources=None, unique=False):
+def get_all_slots(
+    base_agenda, meeting_type, resources=None, unique=False, start_datetime=None, end_datetime=None
+):
     '''Get all occupation state of all possible slots for the given agenda (of
        its real agendas for a virtual agenda) and the given meeting_type.
 
@@ -107,8 +120,8 @@ def get_all_slots(base_agenda, meeting_type, resources=None, unique=False):
     # that the base_meeting_duration for the virtual agenda is always the same
     # as the base meeting duration of each real agenda.
     base_meeting_duration = base_agenda.get_base_meeting_duration()
-    base_min_datetime = get_min_datetime(base_agenda)
-    base_max_datetime = get_max_datetime(base_agenda)
+    base_min_datetime = get_min_datetime(base_agenda, start_datetime)
+    base_max_datetime = get_max_datetime(base_agenda, end_datetime)
 
     meeting_duration = meeting_type.duration
     meeting_duration_td = datetime.timedelta(minutes=meeting_duration)
@@ -121,8 +134,8 @@ def get_all_slots(base_agenda, meeting_type, resources=None, unique=False):
     agenda_ids_by_min_max_datetimes = collections.defaultdict(set)
     agenda_id_min_max_datetime = {}
     for agenda in agendas:
-        used_min_datetime = base_min_datetime or get_min_datetime(agenda)
-        used_max_datetime = base_max_datetime or get_max_datetime(agenda)
+        used_min_datetime = base_min_datetime or get_min_datetime(agenda, start_datetime)
+        used_max_datetime = base_max_datetime or get_max_datetime(agenda, end_datetime)
         agenda_ids_by_min_max_datetimes[(used_min_datetime, used_max_datetime)].add(agenda.id)
         agenda_id_min_max_datetime[agenda.id] = (used_min_datetime, used_max_datetime)
 
@@ -518,6 +531,18 @@ class MeetingDatetimes(APIView):
         except APIError as e:
             return e.to_response()
 
+        start_datetime = None
+        if 'date_start' in request.GET:
+            start_datetime = make_aware(
+                datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))
+            )
+
+        end_datetime = None
+        if 'date_end' in request.GET:
+            end_datetime = make_aware(
+                datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
+            )
+
         # Generate an unique slot for each possible meeting [start_datetime,
         # end_datetime] range.
         # First use get_all_slots() to get each possible meeting by desk and
@@ -531,7 +556,16 @@ class MeetingDatetimes(APIView):
         # The generator also remove slots starting before the current time.
         def unique_slots():
             last_slot = None
-            all_slots = list(get_all_slots(agenda, meeting_type, resources=resources, unique=True))
+            all_slots = list(
+                get_all_slots(
+                    agenda,
+                    meeting_type,
+                    resources=resources,
+                    unique=True,
+                    start_datetime=start_datetime,
+                    end_datetime=end_datetime,
+                )
+            )
             for slot in sorted(all_slots, key=lambda slot: slot[:3]):
                 if slot.start_datetime < now_datetime:
                     continue
diff --git a/tests/test_api.py b/tests/test_api.py
index 826e32a..41f4cc0 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -4451,3 +4451,78 @@ def test_datetimes_maximal_booking_delay(app, user):
     resp = app.get(api_url)
     data = resp.json['data']
     assert len(data) == 4
+
+
+def test_meetings_and_virtual_datetimes_date_filter(app):
+    agenda_foo = Agenda.objects.create(
+        label=u'Agenda Foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=7
+    )
+    meeting_type = MeetingType.objects.create(agenda=agenda_foo, label='Meeting Type', duration=30)
+    desk_foo = Desk.objects.create(agenda=agenda_foo, label='Desk 1')
+    weekday1 = (localtime(now())).weekday() + 1 % 7
+    weekday2 = (localtime(now())).weekday() + 2 % 7
+    weekday3 = (localtime(now())).weekday() + 3 % 7
+    weekday4 = (localtime(now())).weekday() + 4 % 7
+    weekday5 = (localtime(now())).weekday() + 5 % 7
+    weekday6 = (localtime(now())).weekday() + 6 % 7
+    for weekday in (weekday1, weekday2, weekday3, weekday4, weekday5, weekday6):
+        TimePeriod.objects.create(
+            weekday=weekday, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=desk_foo,
+        )
+
+    virtual_agenda = Agenda.objects.create(
+        label=u'Agenda Virtual', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=7
+    )
+    VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_foo)
+
+    # 4 slots each day * 6 days
+    foo_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda_foo.slug, meeting_type.slug)
+    resp = app.get(foo_api_url)
+    assert len(resp.json['data']) == 24
+    virtual_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug)
+    resp = app.get(virtual_api_url)
+    assert len(resp.json['data']) == 24
+
+    # exclude weekday1 through date_start, 4 slots each day * 5 days
+    params = {'date_start': (localtime(now()) + datetime.timedelta(days=2)).date().isoformat()}
+    resp = app.get(foo_api_url, params=params)
+    assert len(resp.json['data']) == 20
+    resp = app.get(virtual_api_url, params=params)
+    assert len(resp.json['data']) == 20
+
+    # minimal_booking_delay (which exclude weekday1 and wekkday2 ) takes precedence
+    # 4 slots each day * 4 days
+    agenda_foo.minimal_booking_delay = 3
+    agenda_foo.save()
+    resp = app.get(foo_api_url, params=params)
+    assert len(resp.json['data']) == 16
+    # also on virtual agenda
+    virtual_agenda.minimal_booking_delay = 3
+    virtual_agenda.save()
+    resp = app.get(virtual_api_url, params=params)
+    assert len(resp.json['data']) == 16
+
+    # reset
+    agenda_foo.minimal_booking_delay = 1
+    virtual_agenda.minimal_booking_delay = 1
+    agenda_foo.save()
+    virtual_agenda.save()
+
+    # exclude weekday6 through date_end, 4 slots each day * 5 days
+    params = {'date_end': (localtime(now()) + datetime.timedelta(days=6)).date().isoformat()}
+    resp = app.get(foo_api_url, params=params)
+    assert len(resp.json['data']) == 20
+    resp = app.get(virtual_api_url, params=params)
+    assert len(resp.json['data']) == 20
+
+    # maximal_booking_delay (which exclude weekday5 and weekday6 ) takes precedence
+    # 4 slots each day * 4 days
+    agenda_foo.maximal_booking_delay = 5
+    agenda_foo.save()
+    resp = app.get(foo_api_url, params=params)
+    assert len(resp.json['data']) == 16
+    # also on virtual agenda
+    virtual_agenda.maximal_booking_delay = 5
+    virtual_agenda.save()
+    resp = app.get(virtual_api_url, params=params)
+    assert len(resp.json['data']) == 16
-- 
2.20.1

