From 7206f0e2f2d3bbc773dd791d92337acaf68256ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 25 Feb 2021 14:51:44 +0100
Subject: [PATCH] api: exclude slots already booked by user in fillslot
 (#51432)

---
 chrono/api/views.py |  17 ++++-
 tests/test_api.py   | 158 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 173 insertions(+), 2 deletions(-)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index 303a4c3..0daaf69 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -851,6 +851,7 @@ class SlotSerializer(serializers.Serializer):
     user_display_label = serializers.CharField(max_length=250, allow_blank=True)
     user_email = serializers.CharField(max_length=250, allow_blank=True)
     user_phone_number = serializers.CharField(max_length=16, allow_blank=True)
+    exclude_user = 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)
@@ -976,6 +977,8 @@ class Fillslots(APIView):
 
         available_desk = None
         color = None
+        user_external_id = payload.get('user_external_id') or None
+        exclude_user = payload.get('exclude_user')
 
         if agenda.accept_meetings():
             # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
@@ -1025,7 +1028,12 @@ class Fillslots(APIView):
                     http_status=status.HTTP_400_BAD_REQUEST,
                 )
             all_slots = sorted(
-                get_all_slots(agenda, meeting_type, resources=resources),
+                get_all_slots(
+                    agenda,
+                    meeting_type,
+                    resources=resources,
+                    excluded_user_external_id=user_external_id if exclude_user else None,
+                ),
                 key=lambda slot: slot.start_datetime,
             )
 
@@ -1118,7 +1126,7 @@ class Fillslots(APIView):
         else:
             # convert event recurrence identifiers to real event slugs
             for i, slot in enumerate(slots.copy()):
-                if not ':' in slot:
+                if ':' not in slot:
                     continue
                 event = get_event_recurrence(agenda, slot)
                 slots[i] = event.slug
@@ -1133,6 +1141,11 @@ class Fillslots(APIView):
                     raise APIError(_('event not bookable'), err_class='event not bookable')
                 if event.cancelled:
                     raise APIError(_('event is cancelled'), err_class='event is cancelled')
+                if exclude_user and user_external_id:
+                    if event.booking_set.filter(user_external_id=user_external_id).exists():
+                        raise APIError(
+                            _('event is already booked by user'), err_class='event is already booked by user'
+                        )
 
             if not events.count():
                 raise APIError(
diff --git a/tests/test_api.py b/tests/test_api.py
index d8e4d85..eeccda1 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1145,6 +1145,103 @@ def test_booking_api(app, some_data, user):
     resp = app.post('/api/agenda/0/fillslot/%s/' % event.id, status=404)
 
 
+@pytest.mark.freeze_time('2021-02-23')
+def test_booking_api_exclude_slots(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().replace(hour=10, minute=0),
+        places=5,
+        agenda=agenda,
+    )
+    Booking.objects.create(event=event, user_external_id='42')
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'user_external_id': '42'}
+    )
+    assert resp.json['err'] == 0
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
+        params={'user_external_id': '42', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event is already booked by user'
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'exclude_user': True})
+    assert resp.json['err'] == 0
+
+    event.delete()
+
+    # recurrent event
+    event = Event.objects.create(
+        slug='recurrent',
+        start_datetime=localtime().replace(hour=12, minute=0),
+        repeat='weekly',
+        places=2,
+        agenda=agenda,
+    )
+    first_recurrence = event.get_or_create_event_recurrence(event.start_datetime)
+    Booking.objects.create(event=first_recurrence, user_external_id='42')
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'user_external_id': '42'}
+    )
+    assert resp.json['err'] == 0
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
+        params={'user_external_id': '42', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'event is already booked by user'
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'exclude_user': True})
+    assert resp.json['err'] == 0
+
+
+@pytest.mark.freeze_time('2021-02-25')
+def test_booking_api_meetings_agenda_exclude_slots(app, user):
+    tomorrow = now() + datetime.timedelta(days=1)
+    agenda = Agenda.objects.create(
+        label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
+    )
+    desk = Desk.objects.create(agenda=agenda, slug='desk')
+    meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
+    TimePeriod.objects.create(
+        weekday=tomorrow.date().weekday(),
+        start_time=datetime.time(9, 0),
+        end_time=datetime.time(17, 00),
+        desk=desk,
+    )
+    desk.duplicate()
+    desk.duplicate()
+    event = Event.objects.create(
+        agenda=agenda,
+        meeting_type=meeting_type,
+        places=1,
+        start_datetime=localtime(tomorrow).replace(hour=9, minute=0),
+        desk=desk,
+    )
+    Booking.objects.create(event=event, user_external_id='42')
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (agenda.slug, meeting_type.slug),
+        params={'user_external_id': '42'},
+    )
+    assert resp.json['err'] == 0
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (agenda.slug, meeting_type.slug),
+        params={'user_external_id': '42', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'no more desk available'
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (agenda.slug, meeting_type.slug),
+        params={'exclude_user': True},
+    )
+    assert resp.json['err'] == 0
+
+
 def test_booking_ics(app, some_data, meetings_agenda, user):
     agenda = Agenda.objects.filter(label=u'Foo bar')[0]
     event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
@@ -4764,6 +4861,67 @@ def test_virtual_agendas_meetings_booking(app, mock_now, user):
     assert resp_booking.json['err_desc'] == 'no more desk available'
 
 
+@pytest.mark.freeze_time('2021-02-25')
+def test_virtual_agendas_meetings_booking_exclude_slots(app, user):
+    tomorrow = now() + datetime.timedelta(days=1)
+    agenda = Agenda.objects.create(
+        label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
+    )
+    desk = Desk.objects.create(agenda=agenda, slug='desk')
+    meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
+    TimePeriod.objects.create(
+        weekday=tomorrow.date().weekday(),
+        start_time=datetime.time(9, 0),
+        end_time=datetime.time(17, 00),
+        desk=desk,
+    )
+    agenda2 = agenda.duplicate()
+    agenda3 = agenda.duplicate()
+    virt_agenda = Agenda.objects.create(
+        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=10
+    )
+    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda)
+    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
+    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda3)
+
+    event = Event.objects.create(
+        agenda=agenda,
+        meeting_type=meeting_type,
+        places=1,
+        start_datetime=localtime(tomorrow).replace(hour=9, minute=0),
+        desk=desk,
+    )
+    Booking.objects.create(event=event, user_external_id='42')
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
+        params={'user_external_id': '42'},
+    )
+    assert resp.json['err'] == 0
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
+        params={'user_external_id': '42', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'no more desk available'
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
+        params={'exclude_user': True},
+    )
+    assert resp.json['err'] == 0
+
+    virt_agenda.minimal_booking_delay = None
+    virt_agenda.maximal_booking_delay = None
+    virt_agenda.save()
+    resp = app.post(
+        '/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
+        params={'user_external_id': '42', 'exclude_user': True},
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == '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
-- 
2.29.2

