Projet

Général

Profil

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

Lauréline Guérin, 02 juin 2022 09:30

Télécharger (37,1 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       |  69 +++++
 tests/api/test_booking.py |   8 +-
 tests/api/test_event.py   | 627 +++++++++++++++++++++++++++++++++++++-
 5 files changed, 789 insertions(+), 42 deletions(-)
chrono/api/serializers.py
51 51
        return super().to_internal_value(data)
52 52

  
53 53

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

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

  
60

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

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

  
69

  
54 70
class SlotSerializer(serializers.Serializer):
55 71
    label = serializers.CharField(max_length=250, allow_blank=True)
56 72
    user_external_id = serializers.CharField(max_length=250, allow_blank=True)
......
123 139
        return value
124 140

  
125 141

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

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

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

  
153

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

  
......
179 207
            'user_presence_reason',
180 208
            'color',
181 209
            'extra_data',
210
            'creation_datetime',
211
            'cancellation_datetime',
212
        ]
213
        read_only_fields = [
214
            'id',
215
            'in_waiting_list',
216
            'extra_data',
217
            'creation_datetime',
218
            'cancellation_datetime',
182 219
        ]
183
        read_only_fields = ['id', 'in_waiting_list', 'extra_data']
184 220

  
185 221
    def to_representation(self, instance):
186 222
        ret = super().to_representation(instance)
......
248 284
    )
249 285

  
250 286

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

  
254
    date_start = serializers.DateTimeField(required=False, input_formats=datetime_formats)
255
    date_end = serializers.DateTimeField(required=False, input_formats=datetime_formats)
256

  
257

  
258 287
class DateRangeSerializer(DateRangeMixin, serializers.Serializer):
259 288
    pass
260 289

  
......
280 309
        return attrs
281 310

  
282 311

  
283
class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
284
    agendas = CommaSeparatedStringField(
285
        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
286
    )
312
class AgendaOrSubscribedSlugsMixin(AgendaSlugsMixin):
287 313
    subscribed = CommaSeparatedStringField(
288 314
        required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
289 315
    )
290 316
    user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
291 317
    guardian_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
292 318

  
293
    def get_agenda_qs(self):
294
        return Agenda.objects.filter(kind='events').select_related('events_type')
295

  
296 319
    def validate(self, attrs):
297 320
        super().validate(attrs)
298 321
        if 'agendas' not in attrs and 'subscribed' not in attrs:
......
350 373
        return attrs
351 374

  
352 375

  
353
class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, serializers.Serializer):
376
class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, DateRangeMixin, serializers.Serializer):
354 377
    pass
355 378

  
356 379

  
......
363 386
    check_overlaps = serializers.BooleanField(default=False)
364 387

  
365 388

  
366
class AgendaSlugsSerializer(serializers.Serializer):
367
    agendas = CommaSeparatedStringField(
368
        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
369
    )
370

  
371

  
372 389
class EventSerializer(serializers.ModelSerializer):
373 390
    recurrence_days = StringOrListField(
374 391
        required=False, child=serializers.IntegerField(min_value=0, max_value=6)
375 392
    )
393
    primary_event = serializers.SlugRelatedField(read_only=True, slug_field='slug')
394
    agenda = serializers.SlugRelatedField(read_only=True, slug_field='slug')
376 395

  
377 396
    class Meta:
378 397
        model = Event
......
386 405
            'places',
387 406
            'waiting_list_places',
388 407
            'label',
408
            'slug',
389 409
            'description',
390 410
            'pricing',
391 411
            'url',
412
            'primary_event',
413
            'agenda',
392 414
        ]
415
        read_only_fields = ['slug']
393 416

  
394 417
    def __init__(self, *args, **kwargs):
395 418
        super().__init__(*args, **kwargs)
396 419

  
397
        if self.instance.agenda.events_type and not self.instance.primary_event:
398
            field_classes = {
399
                'text': serializers.CharField,
400
                'textarea': serializers.CharField,
401
                'bool': serializers.NullBooleanField,
402
            }
403
            field_options = {
404
                'text': {'allow_blank': True},
405
                'textarea': {'allow_blank': True},
406
            }
407
            for custom_field in self.instance.agenda.events_type.get_custom_fields():
408
                field_class = field_classes[custom_field['field_type']]
409
                field_name = 'custom_field_%s' % custom_field['varname']
410
                self.fields[field_name] = field_class(
411
                    required=False,
412
                    **(field_options.get(custom_field['field_type']) or {}),
413
                )
420
        if not self.instance.agenda.events_type:
421
            return
422
        field_classes = {
423
            'text': serializers.CharField,
424
            'textarea': serializers.CharField,
425
            'bool': serializers.NullBooleanField,
426
        }
427
        field_options = {
428
            'text': {'allow_blank': True},
429
            'textarea': {'allow_blank': True},
430
        }
431
        for custom_field in self.instance.agenda.events_type.get_custom_fields():
432
            field_class = field_classes[custom_field['field_type']]
433
            field_name = 'custom_field_%s' % custom_field['varname']
434
            self.fields[field_name] = field_class(
435
                required=False,
436
                read_only=self.instance.primary_event is not None,
437
                **(field_options.get(custom_field['field_type']) or {}),
438
            )
414 439

  
415 440
    def validate(self, attrs):
416 441
        if not self.instance.agenda.events_type:
......
435 460
        attrs['custom_fields'] = custom_fields
436 461
        return attrs
437 462

  
463
    def to_representation(self, instance):
464
        ret = super().to_representation(instance)
465
        if not self.instance.agenda.events_type:
466
            return ret
467
        defaults = {
468
            'text': '',
469
            'textarea': '',
470
            'bool': None,
471
        }
472
        custom_fields = self.instance.custom_fields
473
        for custom_field in self.instance.agenda.events_type.get_custom_fields():
474
            varname = custom_field['varname']
475
            field_name = 'custom_field_%s' % varname
476
            value = defaults[custom_field['field_type']]
477
            if varname in custom_fields:
478
                value = custom_fields[varname]
479
            ret[field_name] = value
480
        return ret
481

  
438 482

  
439 483
class AgendaSerializer(serializers.ModelSerializer):
440 484
    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 event.checked:
2148
                check_status = {'status': 'error', 'error_reason': 'event-not-checked'}
2149
            elif not bookings_by_event_id[event.pk]:
2150
                check_status = {'status': 'not-booked'}
2151
            elif len(bookings_by_event_id[event.pk]) > 1:
2152
                check_status = {'status': 'error', 'error_reason': 'too-many-bookings-found'}
2153
            else:
2154
                booking = bookings_by_event_id[event.pk][0]
2155
                if booking.cancellation_datetime is not None:
2156
                    check_status = {'status': 'cancelled'}
2157
                elif booking.user_was_present is None:
2158
                    check_status = {'status': 'error', 'error_reason': 'booking-not-checked'}
2159
                else:
2160
                    check_status = {
2161
                        'status': 'presence' if booking.user_was_present else 'absence',
2162
                        'check_type': booking.user_check_type.slug if booking.user_check_type else '',
2163
                    }
2164
            data.append(
2165
                {
2166
                    'event': serializers.EventSerializer(event).data,
2167
                    'check_status': check_status,
2168
                    'booking': serializers.BookingSerializer(booking).data if booking else {},
2169
                }
2170
            )
2171

  
2172
        return Response({'err': 0, 'data': data})
2173

  
2174

  
2175
agendas_events_check_status = MultipleAgendasEventsCheckStatus.as_view()
2176

  
2177

  
2109 2178
class SubscriptionFilter(filters.FilterSet):
2110 2179
    date_start = filters.DateFilter(lookup_expr='gte')
2111 2180
    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,
......
155 155
            'user_presence_reason': '',
156 156
            'color': None,
157 157
            'extra_data': None,
158
            'cancellation_datetime': None,
159
            'creation_datetime': localtime(meetings_booking1.creation_datetime).isoformat(),
158 160
        },
159 161
        {
160 162
            'id': events_booking1.pk,
......
169 171
            'color': None,
170 172
            'extra_data': None,
171 173
            'event': resp.json['data'][1]['event'],
174
            'cancellation_datetime': None,
175
            'creation_datetime': localtime(events_booking1.creation_datetime).isoformat(),
172 176
        },
173 177
        {
174 178
            'id': events_booking2.pk,
......
183 187
            'color': None,
184 188
            'extra_data': None,
185 189
            'event': resp.json['data'][1]['event'],
190
            'cancellation_datetime': None,
191
            'creation_datetime': localtime(events_booking2.creation_datetime).isoformat(),
186 192
        },
187 193
    ]
188 194

  
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
        checked=True,
934
    )
935
    Subscription.objects.create(
936
        agenda=agenda,
937
        user_external_id='child:42',
938
        date_start=datetime.date(year=2021, month=9, day=1),
939
        date_end=datetime.date(year=2022, month=9, day=1),
940
    )
941

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

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

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

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

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

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

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

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

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

  
1073

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

  
1146
    app.authorization = ('Basic', ('john.doe', 'password'))
1147
    url = '/api/agendas/events/check-status/'
1148
    params = {
1149
        'user_external_id': 'child:42',
1150
        'agendas': 'foo',
1151
        'date_start': '2022-05-01',
1152
        'date_end': '2022-06-01',
1153
    }
1154
    with CaptureQueriesContext(connection) as ctx:
1155
        resp = app.get(url, params=params)
1156
        assert len(ctx.captured_queries) == 5
1157
    assert resp.json['err'] == 0
1158
    assert resp.json['data'] == [
1159
        {
1160
            'event': {
1161
                'description': None,
1162
                'duration': None,
1163
                'label': 'Not Checked Event Label',
1164
                'slug': 'notchecked-event-slug',
1165
                'places': 10,
1166
                'pricing': None,
1167
                'publication_datetime': None,
1168
                'recurrence_days': None,
1169
                'recurrence_end_date': None,
1170
                'recurrence_week_interval': 1,
1171
                'start_datetime': localtime(notchecked_event.start_datetime).isoformat(),
1172
                'url': None,
1173
                'waiting_list_places': 0,
1174
                'agenda': agenda.slug,
1175
                'primary_event': None,
1176
                'custom_field_bool': None,
1177
                'custom_field_text': '',
1178
                'custom_field_textarea': '',
1179
            },
1180
            'check_status': {'error_reason': 'event-not-checked', 'status': 'error'},
1181
            'booking': {},
1182
        },
1183
        {
1184
            'event': {
1185
                'description': None,
1186
                'duration': None,
1187
                'label': 'Event Label',
1188
                'slug': 'event-slug',
1189
                'places': 10,
1190
                'pricing': None,
1191
                'publication_datetime': None,
1192
                'recurrence_days': None,
1193
                'recurrence_end_date': None,
1194
                'recurrence_week_interval': 1,
1195
                'start_datetime': localtime(event.start_datetime).isoformat(),
1196
                'url': None,
1197
                'waiting_list_places': 0,
1198
                'agenda': agenda.slug,
1199
                'primary_event': None,
1200
                'custom_field_bool': None,
1201
                'custom_field_text': '',
1202
                'custom_field_textarea': '',
1203
            },
1204
            'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
1205
            'booking': {
1206
                'cancellation_datetime': None,
1207
                'color': None,
1208
                'creation_datetime': localtime(now()).isoformat(),
1209
                'extra_data': None,
1210
                'id': booking2.pk,
1211
                'in_waiting_list': False,
1212
                'user_absence_reason': '',
1213
                'user_email': '',
1214
                'user_first_name': '',
1215
                'user_last_name': '',
1216
                'user_phone_number': '',
1217
                'user_presence_reason': check_type.slug,
1218
                'user_was_present': True,
1219
            },
1220
        },
1221
        {
1222
            'event': {
1223
                'description': None,
1224
                'duration': None,
1225
                'label': 'Recurring Event Label',
1226
                'slug': 'recurring-event-slug--2022-05-30-1600',
1227
                'places': 10,
1228
                'pricing': None,
1229
                'publication_datetime': None,
1230
                'recurrence_days': None,
1231
                'recurrence_end_date': None,
1232
                'recurrence_week_interval': 1,
1233
                'start_datetime': localtime(first_event.start_datetime).isoformat(),
1234
                'url': None,
1235
                'waiting_list_places': 0,
1236
                'agenda': agenda.slug,
1237
                'primary_event': recurring_event.slug,
1238
                'custom_field_text': 'foo',
1239
                'custom_field_textarea': 'foo bar',
1240
                'custom_field_bool': True,
1241
            },
1242
            'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
1243
            'booking': {
1244
                'cancellation_datetime': None,
1245
                'color': None,
1246
                'creation_datetime': localtime(now()).isoformat(),
1247
                'extra_data': None,
1248
                'id': booking1.pk,
1249
                'in_waiting_list': False,
1250
                'user_absence_reason': '',
1251
                'user_email': '',
1252
                'user_first_name': '',
1253
                'user_last_name': '',
1254
                'user_phone_number': '',
1255
                'user_presence_reason': check_type.slug,
1256
                'user_was_present': True,
1257
            },
1258
        },
1259
    ]
1260

  
1261

  
1262
@pytest.mark.freeze_time('2022-05-30 14:00')
1263
def test_events_check_status_agendas_filter(app, user):
1264
    agenda1 = Agenda.objects.create(label='Foo')
1265
    agenda2 = Agenda.objects.create(label='Foo 2')
1266
    Event.objects.create(
1267
        slug='event-1',
1268
        label='Event 1',
1269
        start_datetime=now(),
1270
        places=10,
1271
        agenda=agenda1,
1272
    )
1273
    Event.objects.create(
1274
        slug='event-2',
1275
        label='Event 2',
1276
        start_datetime=now(),
1277
        places=10,
1278
        agenda=agenda2,
1279
    )
1280
    Subscription.objects.create(
1281
        agenda=agenda1,
1282
        user_external_id='child:42',
1283
        date_start=datetime.date(year=2021, month=9, day=1),
1284
        date_end=datetime.date(year=2022, month=9, day=1),
1285
    )
1286
    Subscription.objects.create(
1287
        agenda=agenda2,
1288
        user_external_id='child:42',
1289
        date_start=datetime.date(year=2021, month=9, day=1),
1290
        date_end=datetime.date(year=2022, month=9, day=1),
1291
    )
1292

  
1293
    app.authorization = ('Basic', ('john.doe', 'password'))
1294
    url = '/api/agendas/events/check-status/'
1295
    params = {
1296
        'user_external_id': 'child:42',
1297
        'agendas': 'foo, foo-2',
1298
        'date_start': '2022-05-01',
1299
        'date_end': '2022-06-01',
1300
    }
1301
    resp = app.get(url, params=params)
1302
    assert len(resp.json['data']) == 2
1303
    assert resp.json['data'][0]['event']['slug'] == 'event-1'
1304
    assert resp.json['data'][1]['event']['slug'] == 'event-2'
1305

  
1306
    params['agendas'] = 'foo'
1307
    resp = app.get(url, params=params)
1308
    assert len(resp.json['data']) == 1
1309
    assert resp.json['data'][0]['event']['slug'] == 'event-1'
1310

  
1311
    params['agendas'] = 'foo-2'
1312
    resp = app.get(url, params=params)
1313
    assert len(resp.json['data']) == 1
1314
    assert resp.json['data'][0]['event']['slug'] == 'event-2'
1315

  
1316

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

  
1346
    app.authorization = ('Basic', ('john.doe', 'password'))
1347
    url = '/api/agendas/events/check-status/'
1348
    params = {
1349
        'user_external_id': 'child:42',
1350
        'agendas': 'foo',
1351
        'date_start': '2022-05-01',
1352
        'date_end': '2022-06-01',
1353
    }
1354
    resp = app.get(url, params=params)
1355
    assert len(resp.json['data']) == int(expected)
1356

  
1357

  
1358
@pytest.mark.parametrize(
1359
    'event_date, expected',
1360
    [
1361
        # just before first day
1362
        ((2022, 4, 30, 12, 0), False),
1363
        # first day
1364
        ((2022, 5, 1, 12, 0), True),
1365
        # last day
1366
        ((2022, 5, 31, 12, 0), True),
1367
        # just after last day
1368
        ((2022, 6, 1, 12, 0), False),
1369
    ],
1370
)
1371
def test_events_check_status_subscription_filter(app, user, freezer, event_date, expected):
1372
    agenda = Agenda.objects.create(label='Foo')
1373
    Event.objects.create(
1374
        slug='event',
1375
        label='Event',
1376
        start_datetime=make_aware(datetime.datetime(*event_date)),
1377
        places=10,
1378
        agenda=agenda,
1379
    )
1380
    Subscription.objects.create(
1381
        agenda=agenda,
1382
        user_external_id='child:42',
1383
        date_start=datetime.date(year=2022, month=5, day=1),
1384
        date_end=datetime.date(year=2022, month=6, day=1),
1385
    )
1386
    Subscription.objects.create(
1387
        agenda=agenda,
1388
        user_external_id='other',
1389
        date_start=datetime.date(year=2022, month=4, day=1),
1390
        date_end=datetime.date(year=2022, month=7, day=1),
1391
    )
1392

  
1393
    app.authorization = ('Basic', ('john.doe', 'password'))
1394
    url = '/api/agendas/events/check-status/'
1395
    params = {
1396
        'user_external_id': 'child:42',
1397
        'agendas': 'foo',
1398
        'date_start': '2022-04-01',
1399
        'date_end': '2022-07-01',
1400
    }
1401
    resp = app.get(url, params=params)
1402
    assert len(resp.json['data']) == int(expected)
780
-