From a710c7eb70409e240dc32de1ddc812926d66f47a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 18 Feb 2021 15:56:44 +0100
Subject: [PATCH] api: list bookings of somebody (#51187)

---
 chrono/api/urls.py  |   1 +
 chrono/api/views.py |  83 ++++++++++++++++---
 chrono/settings.py  |   1 +
 setup.py            |   1 +
 tests/test_api.py   | 190 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 266 insertions(+), 10 deletions(-)

diff --git a/chrono/api/urls.py b/chrono/api/urls.py
index 1150a8d..c181cc0 100644
--- a/chrono/api/urls.py
+++ b/chrono/api/urls.py
@@ -55,6 +55,7 @@ urlpatterns = [
         views.meeting_datetimes,
         name='api-agenda-meeting-datetimes',
     ),
+    url(r'^bookings/$', views.bookings, name='api-bookings'),
     url(r'^booking/(?P<booking_pk>\w+)/$', views.booking, name='api-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'),
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 9a35085..5752747 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -19,7 +19,6 @@ import datetime
 import itertools
 import uuid
 
-
 from django.db import transaction
 from django.db.models import Prefetch, Q
 from django.http import Http404, HttpResponse
@@ -32,8 +31,10 @@ from django.utils.timezone import now, make_aware, localtime
 from django.utils.translation import gettext_noop
 from django.utils.translation import ugettext_lazy as _
 
+from django_filters import rest_framework as filters
 from rest_framework import permissions, serializers, status
-
+from rest_framework.exceptions import ValidationError
+from rest_framework.generics import ListAPIView
 from rest_framework.views import APIView
 
 from chrono.api.utils import Response
@@ -1243,7 +1244,8 @@ class BookingSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Booking
-        fields = ['user_was_present', 'user_absence_reason']
+        fields = ['id', 'in_waiting_list', 'user_was_present', 'user_absence_reason']
+        read_only_fields = ['id', 'in_waiting_list']
 
     def validate_user_absence_reason(self, value):
         if value is None:
@@ -1251,6 +1253,66 @@ class BookingSerializer(serializers.ModelSerializer):
         return value
 
 
+class BookingFilter(filters.FilterSet):
+    agenda = filters.CharFilter(field_name='event__agenda__slug', lookup_expr='exact')
+    category = filters.CharFilter(field_name='event__agenda__category__slug', lookup_expr='exact')
+    date_start = filters.DateFilter(field_name='event__start_datetime', lookup_expr='gte')
+    date_end = filters.DateFilter(field_name='event__start_datetime', method='filter_date_end')
+
+    def filter_date_end(self, queryset, name, value):
+        # we want to include all events starting during the targeted day
+        lookup = '__'.join([name, 'lt'])
+        return queryset.filter(**{lookup: value + datetime.timedelta(days=1)})
+
+    class Meta:
+        model = Booking
+        fields = [
+            'user_external_id',
+            'agenda',
+            'category',
+            'date_start',
+            'date_end',
+            'user_was_present',
+            'user_absence_reason',
+        ]
+
+
+class BookingsAPI(ListAPIView):
+    filter_backends = (filters.DjangoFilterBackend,)
+    serializer_class = BookingSerializer
+    filterset_class = BookingFilter
+
+    def get(self, request, *args, **kwargs):
+        if not request.GET.get('user_external_id'):
+            response = {
+                'err': 1,
+                'err_class': 'missing param user_external_id',
+                'err_desc': _('missing param user_external_id'),
+            }
+            return Response(response)
+
+        try:
+            response = super().get(request, *args, **kwargs)
+        except ValidationError as e:
+            return Response(
+                {
+                    'err': 1,
+                    'err_class': 'invalid payload',
+                    'err_desc': _('invalid payload'),
+                    'errors': e.detail,
+                },
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+
+        return Response({'err': 0, 'data': response.data})
+
+    def get_queryset(self):
+        return Booking.objects.all().order_by('pk')
+
+
+bookings = BookingsAPI.as_view()
+
+
 class BookingAPI(APIView):
     permission_classes = (permissions.IsAuthenticated,)
     serializer_class = BookingSerializer
@@ -1281,13 +1343,14 @@ class BookingAPI(APIView):
         if response:
             return response
 
-        response = {
-            'err': 0,
-            'booking_id': self.booking.pk,
-            'in_waiting_list': self.booking.in_waiting_list,
-            'user_was_present': self.booking.user_was_present,
-            'user_absence_reason': self.booking.user_absence_reason,
-        }
+        serializer = self.serializer_class(self.booking)
+        response = serializer.data
+        response.update(
+            {
+                'err': 0,
+                'booking_id': self.booking.pk,
+            }
+        )
         return Response(response)
 
     def patch(self, request, *args, **kwargs):
diff --git a/chrono/settings.py b/chrono/settings.py
index 68d2531..132575f 100644
--- a/chrono/settings.py
+++ b/chrono/settings.py
@@ -56,6 +56,7 @@ INSTALLED_APPS = (
     'chrono.api',
     'chrono.manager',
     'rest_framework',
+    'django_filters',
 )
 
 MIDDLEWARE = (
diff --git a/setup.py b/setup.py
index a21d780..2412e9d 100644
--- a/setup.py
+++ b/setup.py
@@ -164,6 +164,7 @@ setup(
         'django>=1.11, <2.3',
         'gadjo',
         'djangorestframework>=3.4',
+        'django-filter',
         'django-jsonfield >= 0.9.3',
         'vobject',
         'python-dateutil',
diff --git a/tests/test_api.py b/tests/test_api.py
index 3c31d07..761825f 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1935,6 +1935,196 @@ def test_booking_api_with_cancel_booking(app, some_data, user):
     assert Booking.objects.count() == 6
 
 
+def test_bookings_api(app, user):
+    events_agenda = Agenda.objects.create(kind='events')
+    events_event = Event.objects.create(agenda=events_agenda, start_datetime=now(), places=10)
+    events_booking1 = Booking.objects.create(event=events_event, user_external_id='enfant-1234')
+    events_booking2 = Booking.objects.create(event=events_event, user_external_id='enfant-1234')
+    Booking.objects.create(event=events_event, user_external_id='foobar')
+    Booking.objects.create(event=events_event)
+
+    meetings_agenda = Agenda.objects.create(kind='meetings')
+    meetings_event = Event.objects.create(agenda=meetings_agenda, start_datetime=now(), places=1)
+    meetings_booking1 = Booking.objects.create(event=meetings_event, user_external_id='enfant-1234')
+    Booking.objects.create(event=meetings_event, user_external_id='baz')
+    Booking.objects.create(event=meetings_event, user_external_id='baz')
+    Booking.objects.create(event=meetings_event)
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/bookings/')
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'missing param user_external_id'
+    assert resp.json['err_desc'] == 'missing param user_external_id'
+
+    resp = app.get('/api/bookings/', params={'user_external_id': ''})
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == 'missing param user_external_id'
+    assert resp.json['err_desc'] == 'missing param user_external_id'
+
+    resp = app.get('/api/bookings/', params={'user_external_id': 'enfant-1234'})
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [
+        {
+            'id': events_booking1.pk,
+            'in_waiting_list': False,
+            'user_was_present': None,
+            'user_absence_reason': '',
+        },
+        {
+            'id': events_booking2.pk,
+            'in_waiting_list': False,
+            'user_was_present': None,
+            'user_absence_reason': '',
+        },
+        {
+            'id': meetings_booking1.pk,
+            'in_waiting_list': False,
+            'user_was_present': None,
+            'user_absence_reason': '',
+        },
+    ]
+
+
+def test_bookings_api_filter_agenda(app, user):
+    bookings = []
+    for label in ['foobar', 'foobaz']:
+        agenda = Agenda.objects.create(label='foobar')
+        event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+        bookings.append(Booking.objects.create(event=event, user_external_id='42'))
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'agenda': 'foo'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == []
+
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'agenda': 'foobar'})
+    assert [b['id'] for b in resp.json['data']] == [bookings[0].pk]
+
+
+def test_bookings_api_filter_category(app, user):
+    bookings = []
+    for label in ['foobar', 'foobaz']:
+        category = Category.objects.create(label=label)
+        agenda = Agenda.objects.create(category=category)
+        event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+        bookings.append(Booking.objects.create(event=event, user_external_id='42'))
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'category': 'foo'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == []
+
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'category': 'foobar'})
+    assert [b['id'] for b in resp.json['data']] == [bookings[0].pk]
+
+
+def test_bookings_api_filter_date_start(app, user):
+    agenda = Agenda.objects.create()
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
+    )
+    booking = Booking.objects.create(event=event, user_external_id='42')
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': 'foo'}, status=400)
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == "invalid payload"
+    assert resp.json['err_desc'] == "invalid payload"
+
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': '2017-05-21'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': '2017-05-22'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': '2017-05-23'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == []
+
+    event.start_datetime = make_aware(datetime.datetime(2017, 5, 22, 23, 59))
+    event.save()
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': '2017-05-21'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': '2017-05-22'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': '2017-05-23'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == []
+
+
+def test_bookings_api_filter_date_end(app, user):
+    agenda = Agenda.objects.create()
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
+    )
+    booking = Booking.objects.create(event=event, user_external_id='42')
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': 'foo'}, status=400)
+    assert resp.json['err'] == 1
+    assert resp.json['err_class'] == "invalid payload"
+    assert resp.json['err_desc'] == "invalid payload"
+
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': '2017-05-21'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == []
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': '2017-05-22'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': '2017-05-23'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+
+    event.start_datetime = make_aware(datetime.datetime(2017, 5, 22, 23, 59))
+    event.save()
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': '2017-05-21'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == []
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': '2017-05-22'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': '2017-05-23'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking.pk]
+
+
+def test_bookings_api_filter_user_was_present(app, user):
+    agenda = Agenda.objects.create()
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
+    )
+    Booking.objects.create(event=event, user_external_id='42')
+    booking2 = Booking.objects.create(event=event, user_external_id='42', user_was_present=True)
+    booking3 = Booking.objects.create(event=event, user_external_id='42', user_was_present=False)
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_was_present': True})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking2.pk]
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_was_present': False})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking3.pk]
+
+
+def test_bookings_api_filter_user_absence_reason(app, user):
+    agenda = Agenda.objects.create()
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
+    )
+    Booking.objects.create(event=event, user_external_id='42')
+    booking2 = Booking.objects.create(event=event, user_external_id='42', user_absence_reason='foo-bar')
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'foo'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == []
+    resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'foo-bar'})
+    assert resp.json['err'] == 0
+    assert [b['id'] for b in resp.json['data']] == [booking2.pk]
+
+
 @pytest.mark.parametrize('flag', [True, False, None])
 def test_booking_api_present(app, user, flag):
     agenda = Agenda.objects.create(kind='events')
-- 
2.29.2

