Projet

Général

Profil

0001-api-new-endpoint-to-get-events-and-check-status-6577.patch

Lauréline Guérin, 31 mai 2022 15:14

Télécharger (35,6 ko)

Voir les différences:

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(-)
chrono/api/serializers.py
50 50
        return super().to_internal_value(data)
51 51

  
52 52

  
53
class DateRangeMixin(metaclass=serializers.SerializerMetaclass):
54
    datetime_formats = ['%Y-%m-%d', '%Y-%m-%d %H:%M', 'iso-8601']
55

  
56
    date_start = serializers.DateTimeField(required=False, input_formats=datetime_formats)
57
    date_end = serializers.DateTimeField(required=False, input_formats=datetime_formats)
58

  
59

  
60
class AgendaSlugsMixin(metaclass=serializers.SerializerMetaclass):
61
    agendas = CommaSeparatedStringField(
62
        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
63
    )
64

  
65
    def get_agenda_qs(self):
66
        return Agenda.objects.filter(kind='events').select_related('events_type')
67

  
68

  
53 69
class SlotSerializer(serializers.Serializer):
54 70
    label = serializers.CharField(max_length=250, allow_blank=True)
55 71
    user_external_id = serializers.CharField(max_length=250, allow_blank=True)
......
122 138
        return value
123 139

  
124 140

  
141
class MultipleAgendasEventsCheckStatusSerializer(AgendaSlugsMixin, DateRangeMixin, serializers.Serializer):
142
    user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
143

  
144
    def __init__(self, *args, **kwargs):
145
        super().__init__(*args, **kwargs)
146
        for field in ['agendas', 'user_external_id', 'date_start', 'date_end']:
147
            self.fields[field].required = True
148

  
149
    def validate_agendas(self, value):
150
        return get_objects_from_slugs(value, qs=self.get_agenda_qs())
151

  
152

  
125 153
class RecurringFillslotsSerializer(MultipleAgendasEventsSlotsSerializer):
126 154
    include_booked_events_detail = serializers.BooleanField(default=False)
127 155

  
......
172 200
            'user_absence_reason',
173 201
            'user_presence_reason',
174 202
            'extra_data',
203
            'creation_datetime',
204
            'cancellation_datetime',
205
        ]
206
        read_only_fields = [
207
            'id',
208
            'in_waiting_list',
209
            'extra_data',
210
            'creation_datetime',
211
            'cancellation_datetime',
175 212
        ]
176
        read_only_fields = ['id', 'in_waiting_list', 'extra_data']
177 213

  
178 214
    def to_representation(self, instance):
179 215
        ret = super().to_representation(instance)
......
241 277
    )
242 278

  
243 279

  
244
class DateRangeMixin(metaclass=serializers.SerializerMetaclass):
245
    datetime_formats = ['%Y-%m-%d', '%Y-%m-%d %H:%M', 'iso-8601']
246

  
247
    date_start = serializers.DateTimeField(required=False, input_formats=datetime_formats)
248
    date_end = serializers.DateTimeField(required=False, input_formats=datetime_formats)
249

  
250

  
251 280
class DateRangeSerializer(DateRangeMixin, serializers.Serializer):
252 281
    pass
253 282

  
......
273 302
        return attrs
274 303

  
275 304

  
276
class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
277
    agendas = CommaSeparatedStringField(
278
        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
279
    )
305
class AgendaOrSubscribedSlugsMixin(AgendaSlugsMixin):
280 306
    subscribed = CommaSeparatedStringField(
281 307
        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
282 308
    )
283 309
    user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
284 310
    guardian_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
285 311

  
286
    def get_agenda_qs(self):
287
        return Agenda.objects.filter(kind='events').select_related('events_type')
288

  
289 312
    def validate(self, attrs):
290 313
        super().validate(attrs)
291 314
        if 'agendas' not in attrs and 'subscribed' not in attrs:
......
343 366
        return attrs
344 367

  
345 368

  
346
class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, serializers.Serializer):
369
class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, DateRangeMixin, serializers.Serializer):
347 370
    pass
348 371

  
349 372

  
......
356 379
    check_overlaps = serializers.BooleanField(default=False)
357 380

  
358 381

  
359
class AgendaSlugsSerializer(serializers.Serializer):
360
    agendas = CommaSeparatedStringField(
361
        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
362
    )
363

  
364

  
365 382
class EventSerializer(serializers.ModelSerializer):
366 383
    recurrence_days = StringOrListField(
367 384
        required=False, child=serializers.IntegerField(min_value=0, max_value=6)
368 385
    )
386
    primary_event = serializers.SlugRelatedField(read_only=True, slug_field='slug')
387
    agenda = serializers.SlugRelatedField(read_only=True, slug_field='slug')
369 388

  
370 389
    class Meta:
371 390
        model = Event
......
379 398
            'places',
380 399
            'waiting_list_places',
381 400
            'label',
401
            'slug',
382 402
            'description',
383 403
            'pricing',
384 404
            'url',
405
            'primary_event',
406
            'agenda',
385 407
        ]
408
        read_only_fields = ['slug']
386 409

  
387 410
    def __init__(self, *args, **kwargs):
388 411
        super().__init__(*args, **kwargs)
389 412

  
390
        if self.instance.agenda.events_type and not self.instance.primary_event:
391
            field_classes = {
392
                'text': serializers.CharField,
393
                'textarea': serializers.CharField,
394
                'bool': serializers.NullBooleanField,
395
            }
396
            field_options = {
397
                'text': {'allow_blank': True},
398
                'textarea': {'allow_blank': True},
399
            }
400
            for custom_field in self.instance.agenda.events_type.get_custom_fields():
401
                field_class = field_classes[custom_field['field_type']]
402
                field_name = 'custom_field_%s' % custom_field['varname']
403
                self.fields[field_name] = field_class(
404
                    required=False,
405
                    **(field_options.get(custom_field['field_type']) or {}),
406
                )
413
        if not self.instance.agenda.events_type:
414
            return
415
        field_classes = {
416
            'text': serializers.CharField,
417
            'textarea': serializers.CharField,
418
            'bool': serializers.NullBooleanField,
419
        }
420
        field_options = {
421
            'text': {'allow_blank': True},
422
            'textarea': {'allow_blank': True},
423
        }
424
        for custom_field in self.instance.agenda.events_type.get_custom_fields():
425
            field_class = field_classes[custom_field['field_type']]
426
            field_name = 'custom_field_%s' % custom_field['varname']
427
            self.fields[field_name] = field_class(
428
                required=False,
429
                read_only=self.instance.primary_event is not None,
430
                **(field_options.get(custom_field['field_type']) or {}),
431
            )
407 432

  
408 433
    def validate(self, attrs):
409 434
        if not self.instance.agenda.events_type:
......
428 453
        attrs['custom_fields'] = custom_fields
429 454
        return attrs
430 455

  
456
    def to_representation(self, instance):
457
        ret = super().to_representation(instance)
458
        if not self.instance.agenda.events_type:
459
            return ret
460
        defaults = {
461
            'text': '',
462
            'textarea': '',
463
            'bool': None,
464
        }
465
        custom_fields = self.instance.custom_fields
466
        for custom_field in self.instance.agenda.events_type.get_custom_fields():
467
            varname = custom_field['varname']
468
            field_name = 'custom_field_%s' % varname
469
            value = defaults[custom_field['field_type']]
470
            if varname in custom_fields:
471
                value = custom_fields[varname]
472
            ret[field_name] = value
473
        return ret
474

  
431 475

  
432 476
class AgendaSerializer(serializers.ModelSerializer):
433 477
    edit_role = serializers.CharField(required=False, max_length=150)
chrono/api/urls.py
28 28
        views.agendas_events_fillslots,
29 29
        name='api-agendas-events-fillslots',
30 30
    ),
31
    url(
32
        r'^agendas/events/check-status/$',
33
        views.agendas_events_check_status,
34
        name='api-agendas-events-check-status',
35
    ),
31 36
    url(r'^agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda),
32 37
    url(r'^agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
33 38
    url(
chrono/api/views.py
2106 2106
agendas_events_fillslots = MultipleAgendasEventsFillslots.as_view()
2107 2107

  
2108 2108

  
2109
class MultipleAgendasEventsCheckStatus(APIView):
2110
    permission_classes = (permissions.IsAuthenticated,)
2111
    serializer_class = serializers.MultipleAgendasEventsCheckStatusSerializer
2112

  
2113
    def get(self, request):
2114
        serializer = self.serializer_class(data=request.query_params)
2115

  
2116
        if not serializer.is_valid():
2117
            raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors, err=1)
2118
        agendas = serializer.validated_data['agendas']
2119
        agendas_by_id = {a.pk: a for a in agendas}
2120
        user_external_id = serializer.validated_data['user_external_id']
2121
        date_start = serializer.validated_data['date_start']
2122
        date_end = serializer.validated_data['date_end']
2123

  
2124
        events = Event.objects.filter(
2125
            agenda__in=agendas,
2126
            agenda__subscriptions__user_external_id=user_external_id,
2127
            agenda__subscriptions__date_start__lte=F('start_datetime'),
2128
            agenda__subscriptions__date_end__gt=F('start_datetime'),
2129
            recurrence_days__isnull=True,
2130
            cancelled=False,
2131
            start_datetime__gte=date_start,
2132
            start_datetime__lt=date_end,
2133
        ).prefetch_related(Prefetch('primary_event', queryset=Event.objects.all().order_by()))
2134
        booking_queryset = Booking.objects.filter(
2135
            event__in=events,
2136
            user_external_id=user_external_id,
2137
        ).select_related('user_check_type')
2138
        bookings_by_event_id = collections.defaultdict(list)
2139
        for booking in booking_queryset:
2140
            bookings_by_event_id[booking.event_id].append(booking)
2141

  
2142
        data = []
2143
        for event in events:
2144
            event.agenda = agendas_by_id[event.agenda_id]  # agenda is already fetched, reuse it
2145
            check_status = {}
2146
            booking = None
2147
            if not bookings_by_event_id[event.pk]:
2148
                check_status = {'status': 'not-booked'}
2149
            elif len(bookings_by_event_id[event.pk]) > 1:
2150
                check_status = {'status': 'error', 'error_reason': 'too-many-bookings-found'}
2151
            else:
2152
                booking = bookings_by_event_id[event.pk][0]
2153
                if booking.cancellation_datetime is not None:
2154
                    check_status = {'status': 'cancelled'}
2155
                elif booking.user_was_present is None:
2156
                    check_status = {'status': 'error', 'error_reason': 'booking-not-checked'}
2157
                else:
2158
                    check_status = {
2159
                        'status': 'presence' if booking.user_was_present else 'absence',
2160
                        'check_type': booking.user_check_type.slug if booking.user_check_type else '',
2161
                    }
2162
            data.append(
2163
                {
2164
                    'event': serializers.EventSerializer(event).data,
2165
                    'check_status': check_status,
2166
                    'booking': serializers.BookingSerializer(booking).data if booking else {},
2167
                }
2168
            )
2169

  
2170
        return Response({'err': 0, 'data': data})
2171

  
2172

  
2173
agendas_events_check_status = MultipleAgendasEventsCheckStatus.as_view()
2174

  
2175

  
2109 2176
class SubscriptionFilter(filters.FilterSet):
2110 2177
    date_start = filters.DateFilter(lookup_expr='gte')
2111 2178
    date_end = filters.DateFilter(lookup_expr='lt')
tests/api/test_booking.py
4 4
import pytest
5 5
from django.db import connection
6 6
from django.test.utils import CaptureQueriesContext
7
from django.utils.timezone import make_aware, now
7
from django.utils.timezone import localtime, make_aware, now
8 8

  
9 9
from chrono.agendas.models import (
10 10
    Agenda,
......
153 153
            'user_absence_reason': '',
154 154
            'user_presence_reason': '',
155 155
            'extra_data': None,
156
            'cancellation_datetime': None,
157
            'creation_datetime': localtime(meetings_booking1.creation_datetime).isoformat(),
156 158
        },
157 159
        {
158 160
            'id': events_booking1.pk,
......
166 168
            'user_presence_reason': '',
167 169
            'extra_data': None,
168 170
            'event': resp.json['data'][1]['event'],
171
            'cancellation_datetime': None,
172
            'creation_datetime': localtime(events_booking1.creation_datetime).isoformat(),
169 173
        },
170 174
        {
171 175
            'id': events_booking2.pk,
......
179 183
            'user_presence_reason': '',
180 184
            'extra_data': None,
181 185
            'event': resp.json['data'][1]['event'],
186
            'cancellation_datetime': None,
187
            'creation_datetime': localtime(events_booking2.creation_datetime).isoformat(),
182 188
        },
183 189
    ]
184 190

  
tests/api/test_event.py
1 1
import datetime
2 2

  
3 3
import pytest
4
from django.utils.timezone import localtime, now
4
from django.db import connection
5
from django.test.utils import CaptureQueriesContext
6
from django.utils.timezone import localtime, make_aware, now
5 7

  
6
from chrono.agendas.models import Agenda, Booking, Event, EventsType
8
from chrono.agendas.models import Agenda, Booking, CheckType, CheckTypeGroup, Event, EventsType, Subscription
7 9

  
8 10
pytestmark = pytest.mark.django_db
9 11

  
......
702 704
    }
703 705

  
704 706

  
707
def test_event_read_only_fields(app, user):
708
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
709
    agenda2 = Agenda.objects.create(label='Foo bar 2', kind='events')
710
    event = Event.objects.create(
711
        slug='event', start_datetime=now() + datetime.timedelta(days=5), places=1, agenda=agenda
712
    )
713

  
714
    app.authorization = ('Basic', ('john.doe', 'password'))
715
    api_url = '/api/agenda/%s/event/' % agenda.slug
716
    params = {
717
        'slug': 'slug',
718
        'agenda': agenda2.slug,
719
        'primary_event': event.slug,
720
        'start_datetime': now().isoformat(),
721
        'places': 42,
722
    }
723
    resp = app.post(api_url, params=params)
724
    assert resp.json['err'] == 0
725
    new_event = Event.objects.latest('pk')
726
    assert new_event.slug == 'foo-bar-event'
727
    assert new_event.agenda == agenda
728
    assert new_event.primary_event is None
729

  
730
    api_url = '/api/agenda/%s/event/%s/' % (agenda.slug, new_event.slug)
731
    params = {
732
        'slug': 'slug',
733
        'agenda': agenda2.slug,
734
        'primary_event': event.slug,
735
    }
736
    resp = app.patch(api_url, params=params)
737
    assert resp.json['err'] == 0
738
    new_event.refresh_from_db()
739
    assert new_event.slug == 'foo-bar-event'
740
    assert new_event.agenda == agenda
741
    assert new_event.primary_event is None
742

  
743

  
705 744
@pytest.mark.freeze_time('2021-11-01 10:00')
706 745
def test_delete_event(app, user):
707 746
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
......
777 816
    resp = app.delete('/api/agenda/%s/event/%s/' % (agenda.slug, event.slug))
778 817
    assert resp.json['err'] == 0
779 818
    assert not Event.objects.exists()
819

  
820

  
821
def test_events_check_status_params(app, user):
822
    app.authorization = ('Basic', ('john.doe', 'password'))
823

  
824
    # missing user_external_id
825
    resp = app.get(
826
        '/api/agendas/events/check-status/',
827
        params={'agendas': 'foo', 'date_start': '2022-05-01', 'date_end': '2022-06-01'},
828
        status=400,
829
    )
830
    assert resp.json['err'] == 1
831
    assert resp.json['err_desc'] == 'invalid payload'
832
    assert resp.json['errors']['user_external_id'] == ['This field is required.']
833

  
834
    # missing agendas
835
    resp = app.get(
836
        '/api/agendas/events/check-status/',
837
        params={'user_external_id': 'child:42', 'date_start': '2022-05-01', 'date_end': '2022-06-01'},
838
        status=400,
839
    )
840
    assert resp.json['err'] == 1
841
    assert resp.json['err_desc'] == 'invalid payload'
842
    assert resp.json['errors']['agendas'] == ['This field is required.']
843

  
844
    # unknown agenda
845
    resp = app.get(
846
        '/api/agendas/events/check-status/',
847
        params={
848
            'user_external_id': 'child:42',
849
            'agendas': 'foo, bar',
850
            'date_start': '2022-05-01',
851
            'date_end': '2022-06-01',
852
        },
853
        status=400,
854
    )
855
    assert resp.json['err'] == 1
856
    assert resp.json['err_desc'] == 'invalid payload'
857
    assert resp.json['errors']['agendas'] == ['invalid slugs: bar, foo']
858
    Agenda.objects.create(label='Foo')
859
    resp = app.get(
860
        '/api/agendas/events/check-status/',
861
        params={
862
            'user_external_id': 'child:42',
863
            'agendas': 'foo, bar',
864
            'date_start': '2022-05-01',
865
            'date_end': '2022-06-01',
866
        },
867
        status=400,
868
    )
869
    assert resp.json['err'] == 1
870
    assert resp.json['err_desc'] == 'invalid payload'
871
    assert resp.json['errors']['agendas'] == ['invalid slugs: bar']
872

  
873
    # wrong kind
874
    wrong_agenda = Agenda.objects.create(label='Bar')
875
    for kind in ['meetings', 'virtual']:
876
        wrong_agenda.kind = kind
877
        wrong_agenda.save()
878
        resp = app.get(
879
            '/api/agendas/events/check-status/',
880
            params={
881
                'user_external_id': 'child:42',
882
                'agendas': 'foo, bar',
883
                'date_start': '2022-05-01',
884
                'date_end': '2022-06-01',
885
            },
886
            status=400,
887
        )
888
        assert resp.json['err'] == 1
889
        assert resp.json['err_desc'] == 'invalid payload'
890
        assert resp.json['errors']['agendas'] == ['invalid slugs: bar']
891

  
892
    # missing date_start
893
    resp = app.get(
894
        '/api/agendas/events/check-status/',
895
        params={'user_external_id': 'child:42', 'agendas': 'foo', 'date_end': '2022-06-01'},
896
        status=400,
897
    )
898
    assert resp.json['err'] == 1
899
    assert resp.json['err_desc'] == 'invalid payload'
900
    assert resp.json['errors']['date_start'] == ['This field is required.']
901

  
902
    # missing date_end
903
    resp = app.get(
904
        '/api/agendas/events/check-status/',
905
        params={'user_external_id': 'child:42', 'agendas': 'foo', 'date_start': '2022-05-01'},
906
        status=400,
907
    )
908
    assert resp.json['err'] == 1
909
    assert resp.json['err_desc'] == 'invalid payload'
910
    assert resp.json['errors']['date_end'] == ['This field is required.']
911

  
912
    # bad date format
913
    resp = app.get(
914
        '/api/agendas/events/check-status/',
915
        params={'user_external_id': 'child:42', 'agendas': 'foo', 'date_start': 'wrong', 'date_end': 'wrong'},
916
        status=400,
917
    )
918
    assert resp.json['err'] == 1
919
    assert resp.json['err_desc'] == 'invalid payload'
920
    assert 'wrong format' in resp.json['errors']['date_start'][0]
921
    assert 'wrong format' in resp.json['errors']['date_end'][0]
922

  
923

  
924
@pytest.mark.freeze_time('2022-05-30 14:00')
925
def test_events_check_status(app, user):
926
    agenda = Agenda.objects.create(label='Foo')
927
    event = Event.objects.create(
928
        slug='event-slug',
929
        label='Event Label',
930
        start_datetime=now(),
931
        places=10,
932
        agenda=agenda,
933
    )
934
    Subscription.objects.create(
935
        agenda=agenda,
936
        user_external_id='child:42',
937
        date_start=datetime.date(year=2021, month=9, day=1),
938
        date_end=datetime.date(year=2022, month=9, day=1),
939
    )
940

  
941
    app.authorization = ('Basic', ('john.doe', 'password'))
942
    url = '/api/agendas/events/check-status/'
943
    params = {
944
        'user_external_id': 'child:42',
945
        'agendas': 'foo',
946
        'date_start': '2022-05-01',
947
        'date_end': '2022-06-01',
948
    }
949

  
950
    # not booked
951
    resp = app.get(url, params=params)
952
    assert resp.json['err'] == 0
953
    assert len(resp.json['data']) == 1
954
    assert resp.json['data'][0]['check_status'] == {
955
        'status': 'not-booked',
956
    }
957
    assert resp.json['data'][0]['booking'] == {}
958

  
959
    # 2 bookings found, error
960
    booking = Booking.objects.create(event=event, user_external_id='child:42')
961
    booking2 = Booking.objects.create(event=event, user_external_id='child:42')
962
    Booking.objects.create(event=event, user_external_id='other')
963
    resp = app.get(url, params=params)
964
    assert resp.json['err'] == 0
965
    assert len(resp.json['data']) == 1
966
    assert resp.json['data'][0]['check_status'] == {
967
        'status': 'error',
968
        'error_reason': 'too-many-bookings-found',
969
    }
970
    assert resp.json['data'][0]['booking'] == {}
971

  
972
    # booking cancelled
973
    booking2.delete()
974
    booking.cancellation_datetime = now()
975
    booking.save()
976
    resp = app.get(url, params=params)
977
    assert resp.json['err'] == 0
978
    assert len(resp.json['data']) == 1
979
    assert resp.json['data'][0]['check_status'] == {
980
        'status': 'cancelled',
981
    }
982
    assert list(resp.json['data'][0]['booking'].keys()) == [
983
        'id',
984
        'in_waiting_list',
985
        'user_first_name',
986
        'user_last_name',
987
        'user_email',
988
        'user_phone_number',
989
        'user_was_present',
990
        'user_absence_reason',
991
        'user_presence_reason',
992
        'extra_data',
993
        'creation_datetime',
994
        'cancellation_datetime',
995
    ]
996
    assert resp.json['data'][0]['booking']['cancellation_datetime'] == localtime(now()).isoformat()
997

  
998
    # booking not checked
999
    booking.cancellation_datetime = None
1000
    booking.save()
1001
    resp = app.get(url, params=params)
1002
    assert resp.json['err'] == 0
1003
    assert len(resp.json['data']) == 1
1004
    assert resp.json['data'][0]['check_status'] == {
1005
        'status': 'error',
1006
        'error_reason': 'booking-not-checked',
1007
    }
1008
    assert resp.json['data'][0]['booking']['cancellation_datetime'] is None
1009

  
1010
    # absence
1011
    booking.user_was_present = False
1012
    booking.save()
1013
    resp = app.get(url, params=params)
1014
    assert resp.json['err'] == 0
1015
    assert len(resp.json['data']) == 1
1016
    assert resp.json['data'][0]['check_status'] == {
1017
        'status': 'absence',
1018
        'check_type': '',
1019
    }
1020
    assert resp.json['data'][0]['booking']['user_was_present'] is False
1021
    assert resp.json['data'][0]['booking']['user_absence_reason'] == ''
1022
    assert resp.json['data'][0]['booking']['user_presence_reason'] == ''
1023

  
1024
    # absence with check type
1025
    group = CheckTypeGroup.objects.create(label='Foo bar')
1026
    check_type = CheckType.objects.create(label='Foo reason', group=group, kind='absence')
1027
    booking.user_check_type = check_type
1028
    booking.save()
1029
    resp = app.get(url, params=params)
1030
    assert resp.json['err'] == 0
1031
    assert len(resp.json['data']) == 1
1032
    assert resp.json['data'][0]['check_status'] == {
1033
        'status': 'absence',
1034
        'check_type': 'foo-reason',
1035
    }
1036
    assert resp.json['data'][0]['booking']['user_was_present'] is False
1037
    assert resp.json['data'][0]['booking']['user_absence_reason'] == 'foo-reason'
1038
    assert resp.json['data'][0]['booking']['user_presence_reason'] == ''
1039

  
1040
    # presence
1041
    booking.user_check_type = None
1042
    booking.user_was_present = True
1043
    booking.save()
1044
    resp = app.get(url, params=params)
1045
    assert resp.json['err'] == 0
1046
    assert len(resp.json['data']) == 1
1047
    assert resp.json['data'][0]['check_status'] == {
1048
        'status': 'presence',
1049
        'check_type': '',
1050
    }
1051
    assert resp.json['data'][0]['booking']['user_was_present'] is True
1052
    assert resp.json['data'][0]['booking']['user_absence_reason'] == ''
1053
    assert resp.json['data'][0]['booking']['user_presence_reason'] == ''
1054

  
1055
    # presence with check type
1056
    check_type.kind = 'presence'
1057
    check_type.save()
1058
    booking.user_check_type = check_type
1059
    booking.save()
1060
    resp = app.get(url, params=params)
1061
    assert resp.json['err'] == 0
1062
    assert len(resp.json['data']) == 1
1063
    assert resp.json['data'][0]['check_status'] == {
1064
        'status': 'presence',
1065
        'check_type': 'foo-reason',
1066
    }
1067
    assert resp.json['data'][0]['booking']['user_was_present'] is True
1068
    assert resp.json['data'][0]['booking']['user_absence_reason'] == ''
1069
    assert resp.json['data'][0]['booking']['user_presence_reason'] == 'foo-reason'
1070

  
1071

  
1072
@pytest.mark.freeze_time('2022-05-30 14:00')
1073
def test_events_check_status_events(app, user):
1074
    events_type = EventsType.objects.create(
1075
        label='Foo',
1076
        custom_fields=[
1077
            {'varname': 'text', 'label': 'Text', 'field_type': 'text'},
1078
            {'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
1079
            {'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
1080
        ],
1081
    )
1082
    group = CheckTypeGroup.objects.create(label='Foo bar')
1083
    check_type = CheckType.objects.create(label='Foo reason', group=group, kind='absence')
1084
    agenda = Agenda.objects.create(label='Foo', events_type=events_type)
1085
    start_datetime = now()
1086
    # recurring event
1087
    recurring_event = Event.objects.create(
1088
        slug='recurring-event-slug',
1089
        label='Recurring Event Label',
1090
        start_datetime=start_datetime,
1091
        recurrence_days=[start_datetime.weekday()],
1092
        recurrence_end_date=start_datetime + datetime.timedelta(days=7),
1093
        places=10,
1094
        agenda=agenda,
1095
        custom_fields={
1096
            'text': 'foo',
1097
            'textarea': 'foo bar',
1098
            'bool': True,
1099
        },
1100
    )
1101
    recurring_event.create_all_recurrences()
1102
    first_event = recurring_event.recurrences.get()
1103
    event = Event.objects.create(
1104
        slug='event-slug',
1105
        label='Event Label',
1106
        start_datetime=start_datetime - datetime.timedelta(days=1),
1107
        places=10,
1108
        agenda=agenda,
1109
    )
1110
    # cancelled event, not returned
1111
    Event.objects.create(
1112
        slug='cancelled',
1113
        label='Cancelled',
1114
        start_datetime=start_datetime,
1115
        places=10,
1116
        agenda=agenda,
1117
        cancelled=True,
1118
    )
1119
    Subscription.objects.create(
1120
        agenda=agenda,
1121
        user_external_id='child:42',
1122
        date_start=datetime.date(year=2021, month=9, day=1),
1123
        date_end=datetime.date(year=2022, month=9, day=1),
1124
    )
1125
    booking1 = Booking.objects.create(
1126
        event=first_event, user_external_id='child:42', user_was_present=True, user_check_type=check_type
1127
    )
1128
    booking2 = Booking.objects.create(
1129
        event=event, user_external_id='child:42', user_was_present=True, user_check_type=check_type
1130
    )
1131

  
1132
    app.authorization = ('Basic', ('john.doe', 'password'))
1133
    url = '/api/agendas/events/check-status/'
1134
    params = {
1135
        'user_external_id': 'child:42',
1136
        'agendas': 'foo',
1137
        'date_start': '2022-05-01',
1138
        'date_end': '2022-06-01',
1139
    }
1140
    with CaptureQueriesContext(connection) as ctx:
1141
        resp = app.get(url, params=params)
1142
        assert len(ctx.captured_queries) == 5
1143
    assert resp.json['err'] == 0
1144
    assert resp.json['data'] == [
1145
        {
1146
            'event': {
1147
                'description': None,
1148
                'duration': None,
1149
                'label': 'Event Label',
1150
                'slug': 'event-slug',
1151
                'places': 10,
1152
                'pricing': None,
1153
                'publication_datetime': None,
1154
                'recurrence_days': None,
1155
                'recurrence_end_date': None,
1156
                'recurrence_week_interval': 1,
1157
                'start_datetime': localtime(event.start_datetime).isoformat(),
1158
                'url': None,
1159
                'waiting_list_places': 0,
1160
                'agenda': agenda.slug,
1161
                'primary_event': None,
1162
                'custom_field_bool': None,
1163
                'custom_field_text': '',
1164
                'custom_field_textarea': '',
1165
            },
1166
            'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
1167
            'booking': {
1168
                'cancellation_datetime': None,
1169
                'creation_datetime': localtime(now()).isoformat(),
1170
                'extra_data': None,
1171
                'id': booking2.pk,
1172
                'in_waiting_list': False,
1173
                'user_absence_reason': '',
1174
                'user_email': '',
1175
                'user_first_name': '',
1176
                'user_last_name': '',
1177
                'user_phone_number': '',
1178
                'user_presence_reason': check_type.slug,
1179
                'user_was_present': True,
1180
            },
1181
        },
1182
        {
1183
            'event': {
1184
                'description': None,
1185
                'duration': None,
1186
                'label': 'Recurring Event Label',
1187
                'slug': 'recurring-event-slug--2022-05-30-1600',
1188
                'places': 10,
1189
                'pricing': None,
1190
                'publication_datetime': None,
1191
                'recurrence_days': None,
1192
                'recurrence_end_date': None,
1193
                'recurrence_week_interval': 1,
1194
                'start_datetime': localtime(first_event.start_datetime).isoformat(),
1195
                'url': None,
1196
                'waiting_list_places': 0,
1197
                'agenda': agenda.slug,
1198
                'primary_event': recurring_event.slug,
1199
                'custom_field_text': 'foo',
1200
                'custom_field_textarea': 'foo bar',
1201
                'custom_field_bool': True,
1202
            },
1203
            'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
1204
            'booking': {
1205
                'cancellation_datetime': None,
1206
                'creation_datetime': localtime(now()).isoformat(),
1207
                'extra_data': None,
1208
                'id': booking1.pk,
1209
                'in_waiting_list': False,
1210
                'user_absence_reason': '',
1211
                'user_email': '',
1212
                'user_first_name': '',
1213
                'user_last_name': '',
1214
                'user_phone_number': '',
1215
                'user_presence_reason': check_type.slug,
1216
                'user_was_present': True,
1217
            },
1218
        },
1219
    ]
1220

  
1221

  
1222
@pytest.mark.freeze_time('2022-05-30 14:00')
1223
def test_events_check_status_agendas_filter(app, user):
1224
    agenda1 = Agenda.objects.create(label='Foo')
1225
    agenda2 = Agenda.objects.create(label='Foo 2')
1226
    Event.objects.create(
1227
        slug='event-1',
1228
        label='Event 1',
1229
        start_datetime=now(),
1230
        places=10,
1231
        agenda=agenda1,
1232
    )
1233
    Event.objects.create(
1234
        slug='event-2',
1235
        label='Event 2',
1236
        start_datetime=now(),
1237
        places=10,
1238
        agenda=agenda2,
1239
    )
1240
    Subscription.objects.create(
1241
        agenda=agenda1,
1242
        user_external_id='child:42',
1243
        date_start=datetime.date(year=2021, month=9, day=1),
1244
        date_end=datetime.date(year=2022, month=9, day=1),
1245
    )
1246
    Subscription.objects.create(
1247
        agenda=agenda2,
1248
        user_external_id='child:42',
1249
        date_start=datetime.date(year=2021, month=9, day=1),
1250
        date_end=datetime.date(year=2022, month=9, day=1),
1251
    )
1252

  
1253
    app.authorization = ('Basic', ('john.doe', 'password'))
1254
    url = '/api/agendas/events/check-status/'
1255
    params = {
1256
        'user_external_id': 'child:42',
1257
        'agendas': 'foo, foo-2',
1258
        'date_start': '2022-05-01',
1259
        'date_end': '2022-06-01',
1260
    }
1261
    resp = app.get(url, params=params)
1262
    assert len(resp.json['data']) == 2
1263
    assert resp.json['data'][0]['event']['slug'] == 'event-1'
1264
    assert resp.json['data'][1]['event']['slug'] == 'event-2'
1265

  
1266
    params['agendas'] = 'foo'
1267
    resp = app.get(url, params=params)
1268
    assert len(resp.json['data']) == 1
1269
    assert resp.json['data'][0]['event']['slug'] == 'event-1'
1270

  
1271
    params['agendas'] = 'foo-2'
1272
    resp = app.get(url, params=params)
1273
    assert len(resp.json['data']) == 1
1274
    assert resp.json['data'][0]['event']['slug'] == 'event-2'
1275

  
1276

  
1277
@pytest.mark.parametrize(
1278
    'event_date, expected',
1279
    [
1280
        # just before first day
1281
        ((2022, 4, 30, 12, 0), False),
1282
        # first day
1283
        ((2022, 5, 1, 12, 0), True),
1284
        # last day
1285
        ((2022, 5, 31, 12, 0), True),
1286
        # just after last day
1287
        ((2022, 6, 1, 12, 0), False),
1288
    ],
1289
)
1290
def test_events_check_status_date_filter(app, user, event_date, expected):
1291
    agenda = Agenda.objects.create(label='Foo')
1292
    Event.objects.create(
1293
        slug='event',
1294
        label='Event',
1295
        start_datetime=make_aware(datetime.datetime(*event_date)),
1296
        places=10,
1297
        agenda=agenda,
1298
    )
1299
    Subscription.objects.create(
1300
        agenda=agenda,
1301
        user_external_id='child:42',
1302
        date_start=datetime.date(year=2021, month=9, day=1),
1303
        date_end=datetime.date(year=2022, month=9, day=1),
1304
    )
1305

  
1306
    app.authorization = ('Basic', ('john.doe', 'password'))
1307
    url = '/api/agendas/events/check-status/'
1308
    params = {
1309
        'user_external_id': 'child:42',
1310
        'agendas': 'foo',
1311
        'date_start': '2022-05-01',
1312
        'date_end': '2022-06-01',
1313
    }
1314
    resp = app.get(url, params=params)
1315
    assert len(resp.json['data']) == int(expected)
1316

  
1317

  
1318
@pytest.mark.parametrize(
1319
    'event_date, expected',
1320
    [
1321
        # just before first day
1322
        ((2022, 4, 30, 12, 0), False),
1323
        # first day
1324
        ((2022, 5, 1, 12, 0), True),
1325
        # last day
1326
        ((2022, 5, 31, 12, 0), True),
1327
        # just after last day
1328
        ((2022, 6, 1, 12, 0), False),
1329
    ],
1330
)
1331
def test_events_check_status_subscription_filter(app, user, freezer, event_date, expected):
1332
    agenda = Agenda.objects.create(label='Foo')
1333
    Event.objects.create(
1334
        slug='event',
1335
        label='Event',
1336
        start_datetime=make_aware(datetime.datetime(*event_date)),
1337
        places=10,
1338
        agenda=agenda,
1339
    )
1340
    Subscription.objects.create(
1341
        agenda=agenda,
1342
        user_external_id='child:42',
1343
        date_start=datetime.date(year=2022, month=5, day=1),
1344
        date_end=datetime.date(year=2022, month=6, day=1),
1345
    )
1346
    Subscription.objects.create(
1347
        agenda=agenda,
1348
        user_external_id='other',
1349
        date_start=datetime.date(year=2022, month=4, day=1),
1350
        date_end=datetime.date(year=2022, month=7, day=1),
1351
    )
1352

  
1353
    app.authorization = ('Basic', ('john.doe', 'password'))
1354
    url = '/api/agendas/events/check-status/'
1355
    params = {
1356
        'user_external_id': 'child:42',
1357
        'agendas': 'foo',
1358
        'date_start': '2022-04-01',
1359
        'date_end': '2022-07-01',
1360
    }
1361
    resp = app.get(url, params=params)
1362
    assert len(resp.json['data']) == int(expected)
780
-