From 36bec97c1236d89023e17a730d8d9f1af74f5357 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 31 May 2022 11:40:43 +0200
Subject: [PATCH] api: new endpoint to get events and check status (#65770)

---
 chrono/api/serializers.py | 122 +++++---
 chrono/api/urls.py        |   5 +
 chrono/api/views.py       |  67 +++++
 tests/api/test_booking.py |   8 +-
 tests/api/test_event.py   | 587 +++++++++++++++++++++++++++++++++++++-
 5 files changed, 747 insertions(+), 42 deletions(-)

diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py
index 15adc9cd..27bab774 100644
--- a/chrono/api/serializers.py
+++ b/chrono/api/serializers.py
@@ -50,6 +50,22 @@ class CommaSeparatedStringField(serializers.ListField):
         return super().to_internal_value(data)
 
 
+class DateRangeMixin(metaclass=serializers.SerializerMetaclass):
+    datetime_formats = ['%Y-%m-%d', '%Y-%m-%d %H:%M', 'iso-8601']
+
+    date_start = serializers.DateTimeField(required=False, input_formats=datetime_formats)
+    date_end = serializers.DateTimeField(required=False, input_formats=datetime_formats)
+
+
+class AgendaSlugsMixin(metaclass=serializers.SerializerMetaclass):
+    agendas = CommaSeparatedStringField(
+        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
+    )
+
+    def get_agenda_qs(self):
+        return Agenda.objects.filter(kind='events').select_related('events_type')
+
+
 class SlotSerializer(serializers.Serializer):
     label = serializers.CharField(max_length=250, allow_blank=True)
     user_external_id = serializers.CharField(max_length=250, allow_blank=True)
@@ -122,6 +138,18 @@ class MultipleAgendasEventsSlotsSerializer(EventsSlotsSerializer):
         return value
 
 
+class MultipleAgendasEventsCheckStatusSerializer(AgendaSlugsMixin, DateRangeMixin, serializers.Serializer):
+    user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        for field in ['agendas', 'user_external_id', 'date_start', 'date_end']:
+            self.fields[field].required = True
+
+    def validate_agendas(self, value):
+        return get_objects_from_slugs(value, qs=self.get_agenda_qs())
+
+
 class RecurringFillslotsSerializer(MultipleAgendasEventsSlotsSerializer):
     include_booked_events_detail = serializers.BooleanField(default=False)
 
@@ -172,8 +200,16 @@ class BookingSerializer(serializers.ModelSerializer):
             'user_absence_reason',
             'user_presence_reason',
             'extra_data',
+            'creation_datetime',
+            'cancellation_datetime',
+        ]
+        read_only_fields = [
+            'id',
+            'in_waiting_list',
+            'extra_data',
+            'creation_datetime',
+            'cancellation_datetime',
         ]
-        read_only_fields = ['id', 'in_waiting_list', 'extra_data']
 
     def to_representation(self, instance):
         ret = super().to_representation(instance)
@@ -241,13 +277,6 @@ class StatisticsFiltersSerializer(serializers.Serializer):
     )
 
 
-class DateRangeMixin(metaclass=serializers.SerializerMetaclass):
-    datetime_formats = ['%Y-%m-%d', '%Y-%m-%d %H:%M', 'iso-8601']
-
-    date_start = serializers.DateTimeField(required=False, input_formats=datetime_formats)
-    date_end = serializers.DateTimeField(required=False, input_formats=datetime_formats)
-
-
 class DateRangeSerializer(DateRangeMixin, serializers.Serializer):
     pass
 
@@ -273,19 +302,13 @@ class DatetimesSerializer(DateRangeSerializer):
         return attrs
 
 
-class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
-    agendas = CommaSeparatedStringField(
-        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
-    )
+class AgendaOrSubscribedSlugsMixin(AgendaSlugsMixin):
     subscribed = CommaSeparatedStringField(
         required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
     )
     user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
     guardian_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
 
-    def get_agenda_qs(self):
-        return Agenda.objects.filter(kind='events').select_related('events_type')
-
     def validate(self, attrs):
         super().validate(attrs)
         if 'agendas' not in attrs and 'subscribed' not in attrs:
@@ -343,7 +366,7 @@ class MultipleAgendasDatetimesSerializer(AgendaOrSubscribedSlugsMixin, Datetimes
         return attrs
 
 
-class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, serializers.Serializer):
+class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, DateRangeMixin, serializers.Serializer):
     pass
 
 
@@ -356,16 +379,12 @@ class RecurringEventsListSerializer(AgendaOrSubscribedSlugsSerializer):
     check_overlaps = serializers.BooleanField(default=False)
 
 
-class AgendaSlugsSerializer(serializers.Serializer):
-    agendas = CommaSeparatedStringField(
-        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
-    )
-
-
 class EventSerializer(serializers.ModelSerializer):
     recurrence_days = StringOrListField(
         required=False, child=serializers.IntegerField(min_value=0, max_value=6)
     )
+    primary_event = serializers.SlugRelatedField(read_only=True, slug_field='slug')
+    agenda = serializers.SlugRelatedField(read_only=True, slug_field='slug')
 
     class Meta:
         model = Event
@@ -379,31 +398,37 @@ class EventSerializer(serializers.ModelSerializer):
             'places',
             'waiting_list_places',
             'label',
+            'slug',
             'description',
             'pricing',
             'url',
+            'primary_event',
+            'agenda',
         ]
+        read_only_fields = ['slug']
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        if self.instance.agenda.events_type and not self.instance.primary_event:
-            field_classes = {
-                'text': serializers.CharField,
-                'textarea': serializers.CharField,
-                'bool': serializers.NullBooleanField,
-            }
-            field_options = {
-                'text': {'allow_blank': True},
-                'textarea': {'allow_blank': True},
-            }
-            for custom_field in self.instance.agenda.events_type.get_custom_fields():
-                field_class = field_classes[custom_field['field_type']]
-                field_name = 'custom_field_%s' % custom_field['varname']
-                self.fields[field_name] = field_class(
-                    required=False,
-                    **(field_options.get(custom_field['field_type']) or {}),
-                )
+        if not self.instance.agenda.events_type:
+            return
+        field_classes = {
+            'text': serializers.CharField,
+            'textarea': serializers.CharField,
+            'bool': serializers.NullBooleanField,
+        }
+        field_options = {
+            'text': {'allow_blank': True},
+            'textarea': {'allow_blank': True},
+        }
+        for custom_field in self.instance.agenda.events_type.get_custom_fields():
+            field_class = field_classes[custom_field['field_type']]
+            field_name = 'custom_field_%s' % custom_field['varname']
+            self.fields[field_name] = field_class(
+                required=False,
+                read_only=self.instance.primary_event is not None,
+                **(field_options.get(custom_field['field_type']) or {}),
+            )
 
     def validate(self, attrs):
         if not self.instance.agenda.events_type:
@@ -428,6 +453,25 @@ class EventSerializer(serializers.ModelSerializer):
         attrs['custom_fields'] = custom_fields
         return attrs
 
+    def to_representation(self, instance):
+        ret = super().to_representation(instance)
+        if not self.instance.agenda.events_type:
+            return ret
+        defaults = {
+            'text': '',
+            'textarea': '',
+            'bool': None,
+        }
+        custom_fields = self.instance.custom_fields
+        for custom_field in self.instance.agenda.events_type.get_custom_fields():
+            varname = custom_field['varname']
+            field_name = 'custom_field_%s' % varname
+            value = defaults[custom_field['field_type']]
+            if varname in custom_fields:
+                value = custom_fields[varname]
+            ret[field_name] = value
+        return ret
+
 
 class AgendaSerializer(serializers.ModelSerializer):
     edit_role = serializers.CharField(required=False, max_length=150)
diff --git a/chrono/api/urls.py b/chrono/api/urls.py
index 9f0e2dc5..2d7cd2ac 100644
--- a/chrono/api/urls.py
+++ b/chrono/api/urls.py
@@ -28,6 +28,11 @@ urlpatterns = [
         views.agendas_events_fillslots,
         name='api-agendas-events-fillslots',
     ),
+    url(
+        r'^agendas/events/check-status/$',
+        views.agendas_events_check_status,
+        name='api-agendas-events-check-status',
+    ),
     url(r'^agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda),
     url(r'^agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
     url(
diff --git a/chrono/api/views.py b/chrono/api/views.py
index f13163e5..617f6eec 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -2106,6 +2106,73 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
 agendas_events_fillslots = MultipleAgendasEventsFillslots.as_view()
 
 
+class MultipleAgendasEventsCheckStatus(APIView):
+    permission_classes = (permissions.IsAuthenticated,)
+    serializer_class = serializers.MultipleAgendasEventsCheckStatusSerializer
+
+    def get(self, request):
+        serializer = self.serializer_class(data=request.query_params)
+
+        if not serializer.is_valid():
+            raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors, err=1)
+        agendas = serializer.validated_data['agendas']
+        agendas_by_id = {a.pk: a for a in agendas}
+        user_external_id = serializer.validated_data['user_external_id']
+        date_start = serializer.validated_data['date_start']
+        date_end = serializer.validated_data['date_end']
+
+        events = Event.objects.filter(
+            agenda__in=agendas,
+            agenda__subscriptions__user_external_id=user_external_id,
+            agenda__subscriptions__date_start__lte=F('start_datetime'),
+            agenda__subscriptions__date_end__gt=F('start_datetime'),
+            recurrence_days__isnull=True,
+            cancelled=False,
+            start_datetime__gte=date_start,
+            start_datetime__lt=date_end,
+        ).prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
+        booking_queryset = Booking.objects.filter(
+            event__in=events,
+            user_external_id=user_external_id,
+        ).select_related('user_check_type')
+        bookings_by_event_id = collections.defaultdict(list)
+        for booking in booking_queryset:
+            bookings_by_event_id[booking.event_id].append(booking)
+
+        data = []
+        for event in events:
+            event.agenda = agendas_by_id[event.agenda_id]  # agenda is already fetched, reuse it
+            check_status = {}
+            booking = None
+            if not bookings_by_event_id[event.pk]:
+                check_status = {'status': 'not-booked'}
+            elif len(bookings_by_event_id[event.pk]) > 1:
+                check_status = {'status': 'error', 'error_reason': 'too-many-bookings-found'}
+            else:
+                booking = bookings_by_event_id[event.pk][0]
+                if booking.cancellation_datetime is not None:
+                    check_status = {'status': 'cancelled'}
+                elif booking.user_was_present is None:
+                    check_status = {'status': 'error', 'error_reason': 'booking-not-checked'}
+                else:
+                    check_status = {
+                        'status': 'presence' if booking.user_was_present else 'absence',
+                        'check_type': booking.user_check_type.slug if booking.user_check_type else '',
+                    }
+            data.append(
+                {
+                    'event': serializers.EventSerializer(event).data,
+                    'check_status': check_status,
+                    'booking': serializers.BookingSerializer(booking).data if booking else {},
+                }
+            )
+
+        return Response({'err': 0, 'data': data})
+
+
+agendas_events_check_status = MultipleAgendasEventsCheckStatus.as_view()
+
+
 class SubscriptionFilter(filters.FilterSet):
     date_start = filters.DateFilter(lookup_expr='gte')
     date_end = filters.DateFilter(lookup_expr='lt')
diff --git a/tests/api/test_booking.py b/tests/api/test_booking.py
index 07ed8df3..138935ff 100644
--- a/tests/api/test_booking.py
+++ b/tests/api/test_booking.py
@@ -4,7 +4,7 @@ from unittest import mock
 import pytest
 from django.db import connection
 from django.test.utils import CaptureQueriesContext
-from django.utils.timezone import make_aware, now
+from django.utils.timezone import localtime, make_aware, now
 
 from chrono.agendas.models import (
     Agenda,
@@ -153,6 +153,8 @@ def test_bookings_api(app, user):
             'user_absence_reason': '',
             'user_presence_reason': '',
             'extra_data': None,
+            'cancellation_datetime': None,
+            'creation_datetime': localtime(meetings_booking1.creation_datetime).isoformat(),
         },
         {
             'id': events_booking1.pk,
@@ -166,6 +168,8 @@ def test_bookings_api(app, user):
             'user_presence_reason': '',
             'extra_data': None,
             'event': resp.json['data'][1]['event'],
+            'cancellation_datetime': None,
+            'creation_datetime': localtime(events_booking1.creation_datetime).isoformat(),
         },
         {
             'id': events_booking2.pk,
@@ -179,6 +183,8 @@ def test_bookings_api(app, user):
             'user_presence_reason': '',
             'extra_data': None,
             'event': resp.json['data'][1]['event'],
+            'cancellation_datetime': None,
+            'creation_datetime': localtime(events_booking2.creation_datetime).isoformat(),
         },
     ]
 
diff --git a/tests/api/test_event.py b/tests/api/test_event.py
index 7e76dd7b..98e27bfe 100644
--- a/tests/api/test_event.py
+++ b/tests/api/test_event.py
@@ -1,9 +1,11 @@
 import datetime
 
 import pytest
-from django.utils.timezone import localtime, now
+from django.db import connection
+from django.test.utils import CaptureQueriesContext
+from django.utils.timezone import localtime, make_aware, now
 
-from chrono.agendas.models import Agenda, Booking, Event, EventsType
+from chrono.agendas.models import Agenda, Booking, CheckType, CheckTypeGroup, Event, EventsType, Subscription
 
 pytestmark = pytest.mark.django_db
 
@@ -702,6 +704,43 @@ def test_update_event(app, user):
     }
 
 
+def test_event_read_only_fields(app, user):
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    agenda2 = Agenda.objects.create(label='Foo bar 2', kind='events')
+    event = Event.objects.create(
+        slug='event', start_datetime=now() + datetime.timedelta(days=5), places=1, agenda=agenda
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    api_url = '/api/agenda/%s/event/' % agenda.slug
+    params = {
+        'slug': 'slug',
+        'agenda': agenda2.slug,
+        'primary_event': event.slug,
+        'start_datetime': now().isoformat(),
+        'places': 42,
+    }
+    resp = app.post(api_url, params=params)
+    assert resp.json['err'] == 0
+    new_event = Event.objects.latest('pk')
+    assert new_event.slug == 'foo-bar-event'
+    assert new_event.agenda == agenda
+    assert new_event.primary_event is None
+
+    api_url = '/api/agenda/%s/event/%s/' % (agenda.slug, new_event.slug)
+    params = {
+        'slug': 'slug',
+        'agenda': agenda2.slug,
+        'primary_event': event.slug,
+    }
+    resp = app.patch(api_url, params=params)
+    assert resp.json['err'] == 0
+    new_event.refresh_from_db()
+    assert new_event.slug == 'foo-bar-event'
+    assert new_event.agenda == agenda
+    assert new_event.primary_event is None
+
+
 @pytest.mark.freeze_time('2021-11-01 10:00')
 def test_delete_event(app, user):
     agenda = Agenda.objects.create(label='Foo bar', kind='events')
@@ -777,3 +816,547 @@ def test_delete_recurring_event_forbidden(app, user):
     resp = app.delete('/api/agenda/%s/event/%s/' % (agenda.slug, event.slug))
     assert resp.json['err'] == 0
     assert not Event.objects.exists()
+
+
+def test_events_check_status_params(app, user):
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    # missing user_external_id
+    resp = app.get(
+        '/api/agendas/events/check-status/',
+        params={'agendas': 'foo', 'date_start': '2022-05-01', 'date_end': '2022-06-01'},
+        status=400,
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert resp.json['errors']['user_external_id'] == ['This field is required.']
+
+    # missing agendas
+    resp = app.get(
+        '/api/agendas/events/check-status/',
+        params={'user_external_id': 'child:42', 'date_start': '2022-05-01', 'date_end': '2022-06-01'},
+        status=400,
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert resp.json['errors']['agendas'] == ['This field is required.']
+
+    # unknown agenda
+    resp = app.get(
+        '/api/agendas/events/check-status/',
+        params={
+            'user_external_id': 'child:42',
+            'agendas': 'foo, bar',
+            'date_start': '2022-05-01',
+            'date_end': '2022-06-01',
+        },
+        status=400,
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert resp.json['errors']['agendas'] == ['invalid slugs: bar, foo']
+    Agenda.objects.create(label='Foo')
+    resp = app.get(
+        '/api/agendas/events/check-status/',
+        params={
+            'user_external_id': 'child:42',
+            'agendas': 'foo, bar',
+            'date_start': '2022-05-01',
+            'date_end': '2022-06-01',
+        },
+        status=400,
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert resp.json['errors']['agendas'] == ['invalid slugs: bar']
+
+    # wrong kind
+    wrong_agenda = Agenda.objects.create(label='Bar')
+    for kind in ['meetings', 'virtual']:
+        wrong_agenda.kind = kind
+        wrong_agenda.save()
+        resp = app.get(
+            '/api/agendas/events/check-status/',
+            params={
+                'user_external_id': 'child:42',
+                'agendas': 'foo, bar',
+                'date_start': '2022-05-01',
+                'date_end': '2022-06-01',
+            },
+            status=400,
+        )
+        assert resp.json['err'] == 1
+        assert resp.json['err_desc'] == 'invalid payload'
+        assert resp.json['errors']['agendas'] == ['invalid slugs: bar']
+
+    # missing date_start
+    resp = app.get(
+        '/api/agendas/events/check-status/',
+        params={'user_external_id': 'child:42', 'agendas': 'foo', 'date_end': '2022-06-01'},
+        status=400,
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert resp.json['errors']['date_start'] == ['This field is required.']
+
+    # missing date_end
+    resp = app.get(
+        '/api/agendas/events/check-status/',
+        params={'user_external_id': 'child:42', 'agendas': 'foo', 'date_start': '2022-05-01'},
+        status=400,
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert resp.json['errors']['date_end'] == ['This field is required.']
+
+    # bad date format
+    resp = app.get(
+        '/api/agendas/events/check-status/',
+        params={'user_external_id': 'child:42', 'agendas': 'foo', 'date_start': 'wrong', 'date_end': 'wrong'},
+        status=400,
+    )
+    assert resp.json['err'] == 1
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert 'wrong format' in resp.json['errors']['date_start'][0]
+    assert 'wrong format' in resp.json['errors']['date_end'][0]
+
+
+@pytest.mark.freeze_time('2022-05-30 14:00')
+def test_events_check_status(app, user):
+    agenda = Agenda.objects.create(label='Foo')
+    event = Event.objects.create(
+        slug='event-slug',
+        label='Event Label',
+        start_datetime=now(),
+        places=10,
+        agenda=agenda,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='child:42',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2022, month=9, day=1),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    url = '/api/agendas/events/check-status/'
+    params = {
+        'user_external_id': 'child:42',
+        'agendas': 'foo',
+        'date_start': '2022-05-01',
+        'date_end': '2022-06-01',
+    }
+
+    # not booked
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'not-booked',
+    }
+    assert resp.json['data'][0]['booking'] == {}
+
+    # 2 bookings found, error
+    booking = Booking.objects.create(event=event, user_external_id='child:42')
+    booking2 = Booking.objects.create(event=event, user_external_id='child:42')
+    Booking.objects.create(event=event, user_external_id='other')
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'error',
+        'error_reason': 'too-many-bookings-found',
+    }
+    assert resp.json['data'][0]['booking'] == {}
+
+    # booking cancelled
+    booking2.delete()
+    booking.cancellation_datetime = now()
+    booking.save()
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'cancelled',
+    }
+    assert list(resp.json['data'][0]['booking'].keys()) == [
+        'id',
+        'in_waiting_list',
+        'user_first_name',
+        'user_last_name',
+        'user_email',
+        'user_phone_number',
+        'user_was_present',
+        'user_absence_reason',
+        'user_presence_reason',
+        'extra_data',
+        'creation_datetime',
+        'cancellation_datetime',
+    ]
+    assert resp.json['data'][0]['booking']['cancellation_datetime'] == localtime(now()).isoformat()
+
+    # booking not checked
+    booking.cancellation_datetime = None
+    booking.save()
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'error',
+        'error_reason': 'booking-not-checked',
+    }
+    assert resp.json['data'][0]['booking']['cancellation_datetime'] is None
+
+    # absence
+    booking.user_was_present = False
+    booking.save()
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'absence',
+        'check_type': '',
+    }
+    assert resp.json['data'][0]['booking']['user_was_present'] is False
+    assert resp.json['data'][0]['booking']['user_absence_reason'] == ''
+    assert resp.json['data'][0]['booking']['user_presence_reason'] == ''
+
+    # absence with check type
+    group = CheckTypeGroup.objects.create(label='Foo bar')
+    check_type = CheckType.objects.create(label='Foo reason', group=group, kind='absence')
+    booking.user_check_type = check_type
+    booking.save()
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'absence',
+        'check_type': 'foo-reason',
+    }
+    assert resp.json['data'][0]['booking']['user_was_present'] is False
+    assert resp.json['data'][0]['booking']['user_absence_reason'] == 'foo-reason'
+    assert resp.json['data'][0]['booking']['user_presence_reason'] == ''
+
+    # presence
+    booking.user_check_type = None
+    booking.user_was_present = True
+    booking.save()
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'presence',
+        'check_type': '',
+    }
+    assert resp.json['data'][0]['booking']['user_was_present'] is True
+    assert resp.json['data'][0]['booking']['user_absence_reason'] == ''
+    assert resp.json['data'][0]['booking']['user_presence_reason'] == ''
+
+    # presence with check type
+    check_type.kind = 'presence'
+    check_type.save()
+    booking.user_check_type = check_type
+    booking.save()
+    resp = app.get(url, params=params)
+    assert resp.json['err'] == 0
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['check_status'] == {
+        'status': 'presence',
+        'check_type': 'foo-reason',
+    }
+    assert resp.json['data'][0]['booking']['user_was_present'] is True
+    assert resp.json['data'][0]['booking']['user_absence_reason'] == ''
+    assert resp.json['data'][0]['booking']['user_presence_reason'] == 'foo-reason'
+
+
+@pytest.mark.freeze_time('2022-05-30 14:00')
+def test_events_check_status_events(app, user):
+    events_type = EventsType.objects.create(
+        label='Foo',
+        custom_fields=[
+            {'varname': 'text', 'label': 'Text', 'field_type': 'text'},
+            {'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
+            {'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
+        ],
+    )
+    group = CheckTypeGroup.objects.create(label='Foo bar')
+    check_type = CheckType.objects.create(label='Foo reason', group=group, kind='absence')
+    agenda = Agenda.objects.create(label='Foo', events_type=events_type)
+    start_datetime = now()
+    # recurring event
+    recurring_event = Event.objects.create(
+        slug='recurring-event-slug',
+        label='Recurring Event Label',
+        start_datetime=start_datetime,
+        recurrence_days=[start_datetime.weekday()],
+        recurrence_end_date=start_datetime + datetime.timedelta(days=7),
+        places=10,
+        agenda=agenda,
+        custom_fields={
+            'text': 'foo',
+            'textarea': 'foo bar',
+            'bool': True,
+        },
+    )
+    recurring_event.create_all_recurrences()
+    first_event = recurring_event.recurrences.get()
+    event = Event.objects.create(
+        slug='event-slug',
+        label='Event Label',
+        start_datetime=start_datetime - datetime.timedelta(days=1),
+        places=10,
+        agenda=agenda,
+    )
+    # cancelled event, not returned
+    Event.objects.create(
+        slug='cancelled',
+        label='Cancelled',
+        start_datetime=start_datetime,
+        places=10,
+        agenda=agenda,
+        cancelled=True,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='child:42',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2022, month=9, day=1),
+    )
+    booking1 = Booking.objects.create(
+        event=first_event, user_external_id='child:42', user_was_present=True, user_check_type=check_type
+    )
+    booking2 = Booking.objects.create(
+        event=event, user_external_id='child:42', user_was_present=True, user_check_type=check_type
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    url = '/api/agendas/events/check-status/'
+    params = {
+        'user_external_id': 'child:42',
+        'agendas': 'foo',
+        'date_start': '2022-05-01',
+        'date_end': '2022-06-01',
+    }
+    with CaptureQueriesContext(connection) as ctx:
+        resp = app.get(url, params=params)
+        assert len(ctx.captured_queries) == 5
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == [
+        {
+            'event': {
+                'description': None,
+                'duration': None,
+                'label': 'Event Label',
+                'slug': 'event-slug',
+                'places': 10,
+                'pricing': None,
+                'publication_datetime': None,
+                'recurrence_days': None,
+                'recurrence_end_date': None,
+                'recurrence_week_interval': 1,
+                'start_datetime': localtime(event.start_datetime).isoformat(),
+                'url': None,
+                'waiting_list_places': 0,
+                'agenda': agenda.slug,
+                'primary_event': None,
+                'custom_field_bool': None,
+                'custom_field_text': '',
+                'custom_field_textarea': '',
+            },
+            'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
+            'booking': {
+                'cancellation_datetime': None,
+                'creation_datetime': localtime(now()).isoformat(),
+                'extra_data': None,
+                'id': booking2.pk,
+                'in_waiting_list': False,
+                'user_absence_reason': '',
+                'user_email': '',
+                'user_first_name': '',
+                'user_last_name': '',
+                'user_phone_number': '',
+                'user_presence_reason': check_type.slug,
+                'user_was_present': True,
+            },
+        },
+        {
+            'event': {
+                'description': None,
+                'duration': None,
+                'label': 'Recurring Event Label',
+                'slug': 'recurring-event-slug--2022-05-30-1600',
+                'places': 10,
+                'pricing': None,
+                'publication_datetime': None,
+                'recurrence_days': None,
+                'recurrence_end_date': None,
+                'recurrence_week_interval': 1,
+                'start_datetime': localtime(first_event.start_datetime).isoformat(),
+                'url': None,
+                'waiting_list_places': 0,
+                'agenda': agenda.slug,
+                'primary_event': recurring_event.slug,
+                'custom_field_text': 'foo',
+                'custom_field_textarea': 'foo bar',
+                'custom_field_bool': True,
+            },
+            'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
+            'booking': {
+                'cancellation_datetime': None,
+                'creation_datetime': localtime(now()).isoformat(),
+                'extra_data': None,
+                'id': booking1.pk,
+                'in_waiting_list': False,
+                'user_absence_reason': '',
+                'user_email': '',
+                'user_first_name': '',
+                'user_last_name': '',
+                'user_phone_number': '',
+                'user_presence_reason': check_type.slug,
+                'user_was_present': True,
+            },
+        },
+    ]
+
+
+@pytest.mark.freeze_time('2022-05-30 14:00')
+def test_events_check_status_agendas_filter(app, user):
+    agenda1 = Agenda.objects.create(label='Foo')
+    agenda2 = Agenda.objects.create(label='Foo 2')
+    Event.objects.create(
+        slug='event-1',
+        label='Event 1',
+        start_datetime=now(),
+        places=10,
+        agenda=agenda1,
+    )
+    Event.objects.create(
+        slug='event-2',
+        label='Event 2',
+        start_datetime=now(),
+        places=10,
+        agenda=agenda2,
+    )
+    Subscription.objects.create(
+        agenda=agenda1,
+        user_external_id='child:42',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2022, month=9, day=1),
+    )
+    Subscription.objects.create(
+        agenda=agenda2,
+        user_external_id='child:42',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2022, month=9, day=1),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    url = '/api/agendas/events/check-status/'
+    params = {
+        'user_external_id': 'child:42',
+        'agendas': 'foo, foo-2',
+        'date_start': '2022-05-01',
+        'date_end': '2022-06-01',
+    }
+    resp = app.get(url, params=params)
+    assert len(resp.json['data']) == 2
+    assert resp.json['data'][0]['event']['slug'] == 'event-1'
+    assert resp.json['data'][1]['event']['slug'] == 'event-2'
+
+    params['agendas'] = 'foo'
+    resp = app.get(url, params=params)
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['event']['slug'] == 'event-1'
+
+    params['agendas'] = 'foo-2'
+    resp = app.get(url, params=params)
+    assert len(resp.json['data']) == 1
+    assert resp.json['data'][0]['event']['slug'] == 'event-2'
+
+
+@pytest.mark.parametrize(
+    'event_date, expected',
+    [
+        # just before first day
+        ((2022, 4, 30, 12, 0), False),
+        # first day
+        ((2022, 5, 1, 12, 0), True),
+        # last day
+        ((2022, 5, 31, 12, 0), True),
+        # just after last day
+        ((2022, 6, 1, 12, 0), False),
+    ],
+)
+def test_events_check_status_date_filter(app, user, event_date, expected):
+    agenda = Agenda.objects.create(label='Foo')
+    Event.objects.create(
+        slug='event',
+        label='Event',
+        start_datetime=make_aware(datetime.datetime(*event_date)),
+        places=10,
+        agenda=agenda,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='child:42',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2022, month=9, day=1),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    url = '/api/agendas/events/check-status/'
+    params = {
+        'user_external_id': 'child:42',
+        'agendas': 'foo',
+        'date_start': '2022-05-01',
+        'date_end': '2022-06-01',
+    }
+    resp = app.get(url, params=params)
+    assert len(resp.json['data']) == int(expected)
+
+
+@pytest.mark.parametrize(
+    'event_date, expected',
+    [
+        # just before first day
+        ((2022, 4, 30, 12, 0), False),
+        # first day
+        ((2022, 5, 1, 12, 0), True),
+        # last day
+        ((2022, 5, 31, 12, 0), True),
+        # just after last day
+        ((2022, 6, 1, 12, 0), False),
+    ],
+)
+def test_events_check_status_subscription_filter(app, user, freezer, event_date, expected):
+    agenda = Agenda.objects.create(label='Foo')
+    Event.objects.create(
+        slug='event',
+        label='Event',
+        start_datetime=make_aware(datetime.datetime(*event_date)),
+        places=10,
+        agenda=agenda,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='child:42',
+        date_start=datetime.date(year=2022, month=5, day=1),
+        date_end=datetime.date(year=2022, month=6, day=1),
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='other',
+        date_start=datetime.date(year=2022, month=4, day=1),
+        date_end=datetime.date(year=2022, month=7, day=1),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    url = '/api/agendas/events/check-status/'
+    params = {
+        'user_external_id': 'child:42',
+        'agendas': 'foo',
+        'date_start': '2022-04-01',
+        'date_end': '2022-07-01',
+    }
+    resp = app.get(url, params=params)
+    assert len(resp.json['data']) == int(expected)
-- 
2.35.1

