From 8d327c0dfe2c1b8fa59e4238ed6127db18052d03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 21 Feb 2020 16:15:12 +0100
Subject: [PATCH 2/2] api: add a suspend action on Booking (#40018)

---
 chrono/agendas/models.py |  6 ++++++
 chrono/api/urls.py       |  1 +
 chrono/api/views.py      | 38 ++++++++++++++++++++++++++++++++++++++
 tests/test_api.py        | 35 +++++++++++++++++++++++++++++++++++
 4 files changed, 80 insertions(+)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 79ee1a8..c2135df 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -475,6 +475,12 @@ class Booking(models.Model):
             self.secondary_booking_set.update(in_waiting_list=False)
             self.save()
 
+    def suspend(self):
+        self.in_waiting_list = True
+        with transaction.atomic():
+            self.secondary_booking_set.update(in_waiting_list=True)
+            self.save()
+
     def get_ics(self, request=None):
         ics = vobject.iCalendar()
         ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
diff --git a/chrono/api/urls.py b/chrono/api/urls.py
index 0fd391c..119210a 100644
--- a/chrono/api/urls.py
+++ b/chrono/api/urls.py
@@ -48,5 +48,6 @@ urlpatterns = [
     url(r'^booking/(?P<booking_pk>\w+)/$', views.booking),
     url(r'^booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking, name='api-cancel-booking'),
     url(r'^booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking, name='api-accept-booking'),
+    url(r'^booking/(?P<booking_pk>\w+)/suspend/$', views.suspend_booking, name='api-suspend-booking'),
     url(r'^booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics, name='api-booking-ics'),
 ]
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 2eaec49..6a664c1 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -658,6 +658,10 @@ class Fillslots(APIView):
             response['api']['accept_url'] = request.build_absolute_uri(
                 reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id})
             )
+        else:
+            response['api']['suspend_url'] = request.build_absolute_uri(
+                reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
+            )
         if agenda.kind == 'meetings':
             response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
             response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
@@ -776,6 +780,40 @@ class AcceptBooking(APIView):
 accept_booking = AcceptBooking.as_view()
 
 
+class SuspendBooking(APIView):
+    '''
+    Suspend a accepted booking.
+
+    It will return error codes if the booking was cancelled before (code 1) and
+    if the bookingis already in waiting list (code 2).
+    '''
+
+    permission_classes = (permissions.IsAuthenticated,)
+
+    def post(self, request, booking_pk=None, format=None):
+        booking = get_object_or_404(Booking, pk=booking_pk)
+        if booking.cancellation_datetime:
+            response = {
+                'err': 1,
+                'err_class': 'booking is cancelled',
+                'err_desc': _('booking is cancelled'),
+            }
+            return Response(response)
+        if booking.in_waiting_list:
+            response = {
+                'err': 2,
+                'err_class': 'booking is already in waiting list',
+                'err_desc': _('booking is already in waiting list'),
+            }
+            return Response(response)
+        booking.suspend()
+        response = {'err': 0, 'booking_id': booking.pk}
+        return Response(response)
+
+
+suspend_booking = SuspendBooking.as_view()
+
+
 class SlotStatus(APIView):
     permission_classes = (permissions.IsAuthenticated,)
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 4155fb9..4555ed7 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -410,8 +410,10 @@ def test_booking_api(app, some_data, user):
     Booking.objects.get(id=resp.json['booking_id'])
     assert resp.json['datetime'] == localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
     assert 'accept_url' not in resp.json['api']
+    assert 'suspend_url' in resp.json['api']
     assert 'cancel_url' in resp.json['api']
     assert 'ics_url' in resp.json['api']
+    assert urlparse.urlparse(resp.json['api']['suspend_url']).netloc
     assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
     assert urlparse.urlparse(resp.json['api']['ics_url']).netloc
     assert Booking.objects.count() == 1
@@ -606,7 +608,9 @@ def test_booking_api_fillslots(app, some_data, user):
     Booking.objects.get(id=primary_booking_id)
     assert resp.json['datetime'] == localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
     assert 'accept_url' not in resp.json['api']
+    assert 'suspend_url' in resp.json['api']
     assert 'cancel_url' in resp.json['api']
+    assert urlparse.urlparse(resp.json['api']['suspend_url']).netloc
     assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
     assert Booking.objects.count() == 3
     # these 3 bookings are related, the first is the primary one
@@ -1201,6 +1205,7 @@ def test_waiting_list_booking(app, some_data, user):
     assert resp.json['err'] == 0
     assert resp.json['in_waiting_list'] is True
     assert 'accept_url' in resp.json['api']
+    assert 'suspend_url' not in resp.json['api']
     assert 'cancel_url' in resp.json['api']
     assert 'ics_url' in resp.json['api']
     assert urlparse.urlparse(resp.json['api']['accept_url']).netloc
@@ -1263,6 +1268,36 @@ def test_accept_booking(app, some_data, user):
     assert Booking.objects.filter(in_waiting_list=False).count() == 0
 
 
+def test_suspend_booking(app, some_data, user):
+    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
+    event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0]
+    event.waiting_list_places = 5
+    event.save()
+
+    # create a booking not on the waiting list
+    booking = Booking.objects.create(event=event, in_waiting_list=False)
+    assert booking.in_waiting_list is False
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post('/api/booking/%s/suspend/' % booking.pk)
+    booking.refresh_from_db()
+    assert booking.in_waiting_list is True
+
+    # suspend a booking that doesn't exist
+    resp = app.post('/api/booking/0/suspend/', status=404)
+
+    # suspend a booking that is in the waiting list
+    resp = app.post('/api/booking/%s/suspend/' % booking.pk, status=200)
+    assert resp.json['err'] == 2
+
+    # suspend a booking that was cancelled before
+    booking.in_waiting_list = False
+    booking.cancel()
+    resp = app.post('/api/booking/%s/suspend/' % booking.pk, status=200)
+    assert resp.json['err'] == 1
+    assert booking.in_waiting_list is False
+
+
 def test_multiple_booking_api(app, some_data, 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]
-- 
2.20.1

