From 54d15baf30a888302e579c3af223d4f299ac72d3 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 19 Jan 2021 16:35:57 +0100
Subject: [PATCH 2/2] api: add a meta dict on datetimes endpoints (#50278)

---
 chrono/api/views.py |  34 +++++++
 tests/test_api.py   | 210 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 244 insertions(+)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index d4aec0c..8636168 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -393,16 +393,34 @@ def get_event_detail(request, event, agenda=None):
                     kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
                 )
             ),
         },
         'places': get_event_places(event),
     }
 
 
+def get_events_meta_detail(events):
+    bookable_datetimes_number_total = 0
+    bookable_datetimes_number_available = 0
+    bookable_datetimes_first = None
+    for event in events:
+        bookable_datetimes_number_total += 1
+        if not bool(event.full):
+            bookable_datetimes_number_available += 1
+            if not bookable_datetimes_first:
+                bookable_datetimes_first = format_response_datetime(event.start_datetime)
+    return {
+        'no_bookable_datetimes': bookable_datetimes_number_available == 0,
+        'bookable_datetimes_number_total': bookable_datetimes_number_total,
+        'bookable_datetimes_number_available': bookable_datetimes_number_available,
+        'bookable_datetimes_first': bookable_datetimes_first
+    }
+
+
 def get_resources_from_request(request, agenda):
     if agenda.kind != 'meetings' or 'resources' not in request.GET:
         return []
     resources_slugs = [s for s in request.GET['resources'].split(',') if s]
     resources = list(agenda.resources.filter(slug__in=resources_slugs))
     if len(resources) != len(resources_slugs):
         unknown_slugs = set(resources_slugs) - set([r.slug for r in resources])
         unknown_slugs = sorted(list(unknown_slugs))
@@ -499,16 +517,17 @@ class Datetimes(APIView):
             entries = entries.filter(
                 start_datetime__lt=make_aware(
                     datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
                 )
             )
 
         response = {
             'data': [get_event_detail(request, x, agenda=agenda) for x in entries],
+            'meta': get_events_meta_detail(entries),
         }
         return Response(response)
 
 
 datetimes = Datetimes.as_view()
 
 
 class MeetingDatetimes(APIView):
@@ -613,30 +632,45 @@ class MeetingDatetimes(APIView):
             It MUST be a garanty of SharedTimePeriod.get_time_slots(),
             !!!
             """
             return '%s:%s' % (
                 meeting_type.slug,
                 start_datetime.strftime('%Y-%m-%d-%H%M'),
             )
 
+        bookable_datetimes_number_total = 0
+        bookable_datetimes_number_available = 0
+        bookable_datetimes_first = None
         data = []
         for slot in generator_of_unique_slots:
+            bookable_datetimes_number_total += 1
+            if not bool(slot.full):
+                bookable_datetimes_number_available += 1
+                if not bookable_datetimes_first:
+                    bookable_datetimes_first = format_response_datetime(slot.start_datetime)
+
             # we do not have the := operator, so we do that
             for slot_id in [make_id(slot.start_datetime, meeting_type)]:
                 data.append({
                     'id': slot_id,
                     'datetime': format_response_datetime(slot.start_datetime),
                     'text': date_format(slot.start_datetime, format='DATETIME_FORMAT'),
                     'disabled': bool(slot.full),
                     'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, slot_id)},
                 })
 
         response = {
             'data': data,
+            'meta': {
+                'no_bookable_datetimes': bookable_datetimes_number_available == 0,
+                'bookable_datetimes_number_total': bookable_datetimes_number_total,
+                'bookable_datetimes_number_available': bookable_datetimes_number_available,
+                'bookable_datetimes_first': bookable_datetimes_first,
+            },
         }
         return Response(response)
 
 
 meeting_datetimes = MeetingDatetimes.as_view()
 
 
 class MeetingList(APIView):
diff --git a/tests/test_api.py b/tests/test_api.py
index b55b6c6..f15575a 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -4812,8 +4812,218 @@ def test_meetings_and_virtual_datetimes_date_filter(app):
     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
+
+
+def test_datetimes_api_meta(app, some_data):
+    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
+    events = Event.objects.filter(agenda_id=agenda.id).exclude(start_datetime__lt=now())
+    assert len(events) == 3
+    api_url = '/api/agenda/%s/datetimes/' % agenda.slug
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 3
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 3,
+        'bookable_datetimes_number_available': 3,
+        'bookable_datetimes_first': '2017-05-21 17:00:00'
+    }
+
+    def simulate_booking(event, nb_places):
+        for i in range(nb_places):
+            Booking(event=event).save()
+
+    simulate_booking(events[0], 10)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 3
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 3,
+        'bookable_datetimes_number_available': 3,
+        'bookable_datetimes_first': '2017-05-21 17:00:00',
+    }
+
+    simulate_booking(events[0], 10)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 3
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 3,
+        'bookable_datetimes_number_available': 2,
+        'bookable_datetimes_first': '2017-05-22 17:00:00',
+    }
+
+    simulate_booking(events[1], 20)
+    simulate_booking(events[2], 20)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 3
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': True,
+        'bookable_datetimes_number_total': 3,
+        'bookable_datetimes_number_available': 0,
+        'bookable_datetimes_first': None,
+    }
+
+
+def test_datetimes_api_meetings_agenda_meta(app, mock_now):
+    meetings_agenda = Agenda.objects.create(
+        label=u'Foo bar Meeting', kind='meetings', maximal_booking_delay=3)
+    meeting_type = MeetingType(agenda=meetings_agenda, label='Blah', duration=30)
+    meeting_type.save()
+
+    desk1 = Desk.objects.create(agenda=meetings_agenda, label='Desk 1')
+    desk2 = Desk.objects.create(agenda=meetings_agenda, label='Desk 2')
+    test_1st_weekday = (localtime(now()).weekday() + 2) % 7
+    for desk in desk1, desk2:
+        TimePeriod(
+            weekday=test_1st_weekday,
+            start_time=datetime.time(10, 0),
+            end_time=datetime.time(12, 0),
+            desk=desk).save()
+
+    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
+    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['data'][2]['disabled'] is False
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 4,
+        'bookable_datetimes_first': '2017-05-22 10:00:00',
+    }
+
+    def simulate_booking(slot, desk):
+        dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M')
+        ev = Event(
+            agenda=meetings_agenda,
+            meeting_type=meeting_type,
+            places=1,
+            full=False,
+            start_datetime=make_aware(dt),
+            desk=desk,
+        )
+        ev.save()
+        booking = Booking(event=ev)
+        booking.save()
+
+    simulate_booking(resp.json['data'][0], desk1)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['data'][0]['disabled'] is False
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 4,
+        'bookable_datetimes_first': '2017-05-22 10:00:00',
+    }
+
+    simulate_booking(resp.json['data'][0], desk2)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['data'][0]['disabled'] is True
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 3,
+        'bookable_datetimes_first': '2017-05-22 10:30:00',
+    }
+
+    for idx in range(1,4):
+        simulate_booking(resp.json['data'][idx], desk1)
+        simulate_booking(resp.json['data'][idx], desk2)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': True,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 0,
+        'bookable_datetimes_first': None,
+    }
+
+
+def test_datetimes_api_virtual_meetings_agenda_meta(app, virtual_meetings_agenda):
+    meetings_agenda1 = Agenda.objects.create(
+        label=u'Foo Meeting', kind='meetings', maximal_booking_delay=3)
+    meetings_agenda2 = Agenda.objects.create(
+        label=u'Bar Meeting', kind='meetings', maximal_booking_delay=3)
+    virtual_agenda = Agenda.objects.create(
+        label=u'Agenda Virtual', kind='virtual', maximal_booking_delay=3)
+    VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda1)
+    VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda2)
+    desk1 = Desk.objects.create(agenda=meetings_agenda1, label='Desk 1')
+    desk2 = Desk.objects.create(agenda=meetings_agenda2, label='Desk 2')
+
+    test_1st_weekday = (localtime(now()).weekday() + 2) % 7
+    for agenda, desk in zip((meetings_agenda1, meetings_agenda2), (desk1, desk2)):
+        meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
+        TimePeriod(
+            weekday=test_1st_weekday,
+            start_time=datetime.time(10, 0),
+            end_time=datetime.time(12, 0),
+            desk=desk).save()
+
+    virt_meeting_type = virtual_agenda.iter_meetingtypes()[0]
+    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (
+        virtual_agenda.slug, virt_meeting_type.slug)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['data'][2]['disabled'] is False
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 4,
+        'bookable_datetimes_first': '2017-05-22 10:00:00',
+    }
+
+    def simulate_booking(slot, agenda, desk):
+        dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M')
+        ev = Event(
+            agenda=agenda,
+            meeting_type=meeting_type,
+            places=1,
+            full=False,
+            start_datetime=make_aware(dt),
+            desk=desk,
+        )
+        ev.save()
+        booking = Booking(event=ev)
+        booking.save()
+
+    simulate_booking(resp.json['data'][0], meetings_agenda1, desk1)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['data'][0]['disabled'] is False
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 4,
+        'bookable_datetimes_first': '2017-05-22 10:00:00',
+    }
+
+    simulate_booking(resp.json['data'][0], meetings_agenda2, desk2)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['data'][0]['disabled'] is True
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': False,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 3,
+        'bookable_datetimes_first': '2017-05-22 10:30:00',
+    }
+
+    for idx in range(1,4):
+        simulate_booking(resp.json['data'][idx], meetings_agenda1, desk1)
+        simulate_booking(resp.json['data'][idx], meetings_agenda2, desk2)
+    resp = app.get(api_url)
+    assert len(resp.json['data']) == 4
+    assert resp.json['meta'] == {
+        'no_bookable_datetimes': True,
+        'bookable_datetimes_number_total': 4,
+        'bookable_datetimes_number_available': 0,
+        'bookable_datetimes_first': None,
+    }
-- 
2.29.2

