From 90dfb10afa828e8659ca6b8a4915306fb59dadb6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Wed, 6 Nov 2019 17:00:12 +0100
Subject: [PATCH 2/2] api: get event status and fillslot with event pk or slug
 (#15726)

---
 chrono/api/urls.py  |  4 ++--
 chrono/api/views.py | 37 ++++++++++++++++++++++++++-----------
 tests/test_api.py   | 20 +++++++++++++++-----
 3 files changed, 43 insertions(+), 18 deletions(-)

diff --git a/chrono/api/urls.py b/chrono/api/urls.py
index d595d41..79ee179 100644
--- a/chrono/api/urls.py
+++ b/chrono/api/urls.py
@@ -23,11 +23,11 @@ urlpatterns = [
     url(r'agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda_detail),
 
     url(r'agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
-    url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_pk>[\w:-]+)/$',
+    url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_identifier>[\w:-]+)/$',
         views.fillslot, name='api-fillslot'),
     url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$',
         views.fillslots, name='api-agenda-fillslots'),
-    url(r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_pk>\d+)/$', views.slot_status,
+    url(r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_identifier>[\w-]+)/$', views.slot_status,
         name='api-event-status'),
 
     url(r'agenda/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$',
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 15cb364..7465f1c 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -185,6 +185,7 @@ class Datetimes(APIView):
                 datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))))
 
         response = {'data': [{'id': x.id,
+                              'slug': x.slug,
                               'text': force_text(x),
                               'datetime': format_response_datetime(x.start_datetime),
                               'description': x.description,
@@ -194,13 +195,13 @@ class Datetimes(APIView):
                                       reverse('api-fillslot',
                                           kwargs={
                                               'agenda_identifier': agenda.slug,
-                                              'event_pk': x.id,
+                                              'event_identifier': x.slug,
                                       })),
                                   'status_url': request.build_absolute_uri(
                                       reverse('api-event-status',
                                           kwargs={
                                               'agenda_identifier': agenda.slug,
-                                              'event_pk': x.id,
+                                              'event_identifier': x.slug,
                                       }))
                                   },
                               } for x in entries]}
@@ -240,12 +241,12 @@ class MeetingDatetimes(APIView):
 
         # create fillslot API URL as a template, to avoid expensive calls
         # to request.build_absolute_uri()
-        fake_event_pk = '__event_id__'
+        fake_event_identifier = '__event_identifier__'
         fillslot_url = request.build_absolute_uri(
                 reverse('api-fillslot',
                     kwargs={
                         'agenda_identifier': agenda.slug,
-                        'event_pk': fake_event_pk,
+                        'event_identifier': fake_event_identifier,
                         }))
 
         response = {'data': [{'id': x.id,
@@ -253,7 +254,7 @@ class MeetingDatetimes(APIView):
                               'text': force_text(x),
                               'disabled': bool(x.full),
                               'api': {
-                                'fillslot_url': fillslot_url.replace(fake_event_pk, str(x.id)),
+                                'fillslot_url': fillslot_url.replace(fake_event_identifier, str(x.id)),
                               },
                              } for x in slots]}
         return Response(response)
@@ -333,7 +334,7 @@ class Fillslots(APIView):
     permission_classes = (permissions.IsAuthenticated,)
     serializer_class = SlotsSerializer
 
-    def post(self, request, agenda_identifier=None, event_pk=None, format=None):
+    def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
         return self.fillslot(request=request, agenda_identifier=agenda_identifier,
                              format=format)
 
@@ -467,7 +468,10 @@ class Fillslots(APIView):
                         full=False, places=1,
                         desk=available_desk))
         else:
-            events = Event.objects.filter(id__in=slots).order_by('start_datetime')
+            try:
+                events = Event.objects.filter(id__in=[int(s) for s in slots]).order_by('start_datetime')
+            except ValueError:
+                events = Event.objects.filter(slug__in=slots).order_by('start_datetime')
 
         # search free places. Switch to waiting list if necessary.
         in_waiting_list = False
@@ -538,10 +542,10 @@ fillslots = Fillslots.as_view()
 class Fillslot(Fillslots):
     serializer_class = SlotSerializer
 
-    def post(self, request, agenda_identifier=None, event_pk=None, format=None):
+    def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
         return self.fillslot(request=request,
                              agenda_identifier=agenda_identifier,
-                             slots=[event_pk],  # fill a "list on one slot"
+                             slots=[event_identifier],  # fill a "list on one slot"
                              format=format)
 
 fillslot = Fillslot.as_view()
@@ -610,8 +614,18 @@ accept_booking = AcceptBooking.as_view()
 class SlotStatus(APIView):
     permission_classes = (permissions.IsAuthenticated,)
 
-    def get(self, request, agenda_identifier=None, event_pk=None, format=None):
-        event = get_object_or_404(Event, id=event_pk)
+    def get_object(self, event_identifier):
+        try:
+            return Event.objects.get(slug=event_identifier)
+        except Event.DoesNotExist:
+            try:
+                # legacy access by event id
+                return Event.objects.get(pk=int(event_identifier))
+            except (ValueError, Event.DoesNotExist):
+                raise Http404()
+
+    def get(self, request, agenda_identifier=None, event_identifier=None, format=None):
+        event = self.get_object(event_identifier)
         response = {
             'err': 0,
             'places': {
@@ -626,6 +640,7 @@ class SlotStatus(APIView):
             response['places']['waiting_list_available'] = (event.waiting_list_places - event.waiting_list)
         return Response(response)
 
+
 slot_status = SlotStatus.as_view()
 
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 78e6d40..973ef75 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -225,7 +225,7 @@ def test_datetime_api_status_url(app, some_data):
     agenda = Agenda.objects.get(label=u'Foo bar2')
     resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
     for datum in resp.json['data']:
-        assert urlparse.urlparse(datum['api']['status_url']).path == '/api/agenda/%s/status/%s/' % (agenda.slug, datum['id'])
+        assert urlparse.urlparse(datum['api']['status_url']).path == '/api/agenda/%s/status/%s/' % (agenda.slug, datum['slug'])
 
 def test_datetimes_api_meetings_agenda(app, meetings_agenda):
     meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
@@ -333,7 +333,7 @@ def test_booking_api(app, some_data, user):
     for agenda_key in (agenda.slug, agenda.id):  # acces datetimes via agenda slug or id (legacy)
         resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
         event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api']['fillslot_url']
-        assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id)
+        assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug)
 
     app.authorization = ('Basic', ('john.doe', 'password'))
     resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id))
@@ -346,7 +346,8 @@ def test_booking_api(app, some_data, user):
     assert urlparse.urlparse(resp.json['api']['ics_url']).netloc
     assert Booking.objects.count() == 1
 
-    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id))
+    # access by slug
+    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.slug))
     assert Booking.objects.count() == 2
     assert Booking.objects.filter(event__agenda=agenda).count() == 2
 
@@ -467,6 +468,7 @@ def test_booking_ics(app, some_data, meetings_agenda, user):
 def test_booking_api_fillslots(app, some_data, user):
     agenda = Agenda.objects.filter(label=u'Foo bar')[0]
     events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
+    events_slugs = [x.slug for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
     assert len(events_ids) == 3
     event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]  # first event
 
@@ -495,7 +497,8 @@ def test_booking_api_fillslots(app, some_data, user):
     assert bookings[1].primary_booking.id == bookings[0].id == primary_booking_id
     assert bookings[2].primary_booking.id == bookings[0].id == primary_booking_id
 
-    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_ids})
+    # access by slug
+    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_slugs})
     primary_booking_id_2 = resp.json['booking_id']
     assert Booking.objects.count() == 6
     assert Booking.objects.filter(event__agenda=agenda).count() == 6
@@ -915,6 +918,12 @@ def test_status(app, some_data, user):
     assert resp.json['places']['waiting_list_available'] == 4
     assert resp.json['places']['waiting_list_reserved'] == 1
 
+    # access by slug
+    resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, event.slug))
+    # not found event
+    resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, 'unknown'), status=404)
+
+
 def test_waiting_list_datetimes(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]
@@ -1343,7 +1352,8 @@ def test_agenda_meeting_api_multiple_desk(app, meetings_agenda, user):
         booking_url = event_data['api']['fillslot_url']
         with CaptureQueriesContext(connection) as ctx:
             app.post(booking_url)
-            assert len(ctx.captured_queries) == queries_count_fillslot1
+            # 2 + idx: because of slug unicity
+            assert len(ctx.captured_queries) == queries_count_fillslot1 + 2 + idx
 
     with CaptureQueriesContext(connection) as ctx:
         app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
-- 
2.20.1

