From 31170a263d11d03c199c532b73ea11674c8ad384 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Mon, 24 Feb 2020 15:02:42 +0100
Subject: [PATCH 3/8] virtual agendas: balance fill rate (#40056)

---
 chrono/api/views.py | 57 ++++++++++++++++++++++++++-----
 tests/test_api.py   | 81 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 130 insertions(+), 8 deletions(-)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index c271ffd..608991a 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -561,18 +561,59 @@ class Fillslots(APIView):
 
             # get all free slots and separate them by desk
             all_slots = get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id))
-            all_slots = [slot for slot in all_slots if not slot.full]
+            all_free_slots = [slot for slot in all_slots if not slot.full]
             datetimes_by_desk = defaultdict(set)
-            for slot in all_slots:
+            for slot in all_free_slots:
                 datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
 
-            # TODO: fill policy for virtual agendas
-            # search first desk where all requested slots are free
-            for available_desk_id in sorted(datetimes_by_desk.keys()):
-                if datetimes.issubset(datetimes_by_desk[available_desk_id]):
-                    available_desk = Desk.objects.get(id=available_desk_id)
-                    break
+            available_desk = None
+
+            if agenda.kind == 'virtual':
+                # Compute fill_rate by agenda/date
+                fill_rates = defaultdict(dict)
+                for slot in all_slots:
+                    ref_date = slot.start_datetime.date()
+                    if ref_date not in fill_rates[slot.desk.agenda]:
+                        date_dict = fill_rates[slot.desk.agenda][ref_date] = {'free': 0, 'full': 0}
+                    else:
+                        date_dict = fill_rates[slot.desk.agenda][ref_date]
+                    if slot.full:
+                        date_dict['full'] += 1
+                    else:
+                        date_dict['free'] += 1
+                for dd in fill_rates.values():
+                    for date_dict in dd.values():
+                        date_dict['fill_rate'] = date_dict['full'] / (date_dict['full'] + date_dict['free'])
+
+                # select a desk on the agenda with min fill_rate on the given date
+                for available_desk_id in sorted(datetimes_by_desk.keys()):
+                    if datetimes.issubset(datetimes_by_desk[available_desk_id]):
+                        desk = Desk.objects.get(id=available_desk_id)
+                        if available_desk is None:
+                            available_desk = desk
+                            available_desk_rate = 0
+                            for dt in datetimes:
+                                available_desk_rate += fill_rates[available_desk.agenda][dt.date()][
+                                    'fill_rate'
+                                ]
+                        else:
+                            for dt in datetimes:
+                                desk_rate = 0
+                                for dt in datetimes:
+                                    desk_rate += fill_rates[desk.agenda][dt.date()]['fill_rate']
+                            if desk_rate < available_desk_rate:
+                                available_desk = desk
+                                available_desk_rate = desk_rate
+
             else:
+                # meeting agenda
+                # search first desk where all requested slots are free
+                for available_desk_id in sorted(datetimes_by_desk.keys()):
+                    if datetimes.issubset(datetimes_by_desk[available_desk_id]):
+                        available_desk = Desk.objects.get(id=available_desk_id)
+                        break
+
+            if available_desk is None:
                 return Response(
                     {
                         'err': 1,
diff --git a/tests/test_api.py b/tests/test_api.py
index 394a90d..73ca3f6 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -2703,3 +2703,84 @@ def test_virtual_agendas_meetings_booking(app, mock_now, user):
     assert resp_booking.json['err'] == 1
     assert resp_booking.json['err_class'] == 'no more desk available'
     assert resp_booking.json['err_desc'] == 'no more desk available'
+
+
+def test_virtual_agendas_meetings_booking_default_policy(app, mock_now, user):
+    foo_agenda = Agenda.objects.create(
+        label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
+    )
+    MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
+    foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
+    foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2')
+    TimePeriod.objects.create(
+        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
+    )
+    TimePeriod.objects.create(
+        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_2,
+    )
+
+    bar_agenda = Agenda.objects.create(
+        label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
+    )
+    MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
+    bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1')
+    bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2')
+    bar_desk_3 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 3')
+    bar_desk_4 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 3')
+    TimePeriod.objects.create(
+        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
+    )
+    TimePeriod.objects.create(
+        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_2,
+    )
+    TimePeriod.objects.create(
+        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_3,
+    )
+    TimePeriod.objects.create(
+        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_4,
+    )
+
+    virt_agenda = Agenda.objects.create(
+        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
+    )
+    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
+    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
+    virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
+    # We are saturday and we can book for next monday and tuesday, 4 slots available each day
+    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
+    resp = app.get(api_url)
+    # We are saturday and we can book for next monday, 4 slots available each day
+    assert len(resp.json['data']) == 4
+
+    # there are 6 desks so we can make 6 bookings on the same slot
+    fillslot_url = resp.json['data'][0]['api']['fillslot_url']
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    for i in range(1, 7):
+        foo_num_bookings = Booking.objects.filter(event__desk__agenda=foo_agenda).count()
+        bar_num_bookings = Booking.objects.filter(event__desk__agenda=bar_agenda).count()
+        foo_fill_rate = foo_num_bookings / 2
+        bar_fill_rate = bar_num_bookings / 4
+        next_agenda = None
+        if i != 1:
+            if foo_fill_rate < bar_fill_rate:
+                next_agenda = foo_agenda
+            elif foo_fill_rate > bar_fill_rate:
+                next_agenda = bar_agenda
+            elif foo_fill_rate == bar_fill_rate:
+                next_agenda = None
+
+        resp_booking = app.post(fillslot_url)
+        assert Booking.objects.count() == i
+        booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
+        assert (
+            resp_booking.json['datetime']
+            == localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
+            == resp.json['data'][0]['datetime']
+        )
+        if next_agenda:
+            assert booking.event.agenda == next_agenda
+
+    foo_num_bookings = Booking.objects.filter(event__desk__agenda=foo_agenda).count()
+    bar_num_bookings = Booking.objects.filter(event__desk__agenda=bar_agenda).count()
+    assert foo_num_bookings == 2
+    assert bar_num_bookings == 4
-- 
2.20.1

