Projet

Général

Profil

0003-api-make-recurring-events-fillslots-work-with-multip.patch

Valentin Deniaud, 21 octobre 2021 18:07

Télécharger (18,6 ko)

Voir les différences:

Subject: [PATCH 3/5] api: make recurring events fillslots work with multiple
 agendas (#57957)

 chrono/api/serializers.py  |  29 ++++++++
 chrono/api/urls.py         |   6 +-
 chrono/api/views.py        |  44 +++++--------
 tests/api/test_fillslot.py | 132 +++++++++++++++++++++++++++++++------
 4 files changed, 158 insertions(+), 53 deletions(-)
chrono/api/serializers.py
1
import collections
2

  
1 3
from django.contrib.auth.models import Group
2 4
from django.utils.translation import ugettext_lazy as _
3 5
from rest_framework import serializers
......
88 90
        return value
89 91

  
90 92

  
93
class RecurringFillslotsSerializer(MultipleAgendasEventsSlotsSerializer):
94
    def validate_slots(self, value):
95
        super().validate_slots(value)
96
        open_event_slugs = collections.defaultdict(set)
97
        for agenda in self.context['agendas']:
98
            for event in agenda.get_open_recurring_events():
99
                open_event_slugs[agenda.slug].add(event.slug)
100

  
101
        slots = collections.defaultdict(lambda: collections.defaultdict(list))
102
        for slot in value:
103
            try:
104
                slugs, day = slot.split(':')
105
                day = int(day)
106
            except ValueError:
107
                raise ValidationError(_('invalid slot: %s') % slot)
108

  
109
            agenda_slug, event_slug = slugs.split('@')
110
            if event_slug not in open_event_slugs[agenda_slug]:
111
                raise ValidationError(_('event %s of agenda %s is not bookable') % (event_slug, agenda_slug))
112

  
113
            # convert ISO day number to db lookup day number
114
            day = (day + 1) % 7 + 1
115
            slots[agenda_slug][event_slug].append(day)
116

  
117
        return slots
118

  
119

  
91 120
class BookingSerializer(serializers.ModelSerializer):
92 121
    user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
93 122

  
chrono/api/urls.py
22 22
    url(r'^agenda/$', views.agendas),
23 23
    url(r'^agendas/datetimes/$', views.agendas_datetimes, name='api-agendas-datetimes'),
24 24
    url(r'^agendas/recurring-events/$', views.recurring_events_list, name='api-agenda-recurring-events'),
25
    url(r'^agendas/recurring-events/fillslots/$', views.recurring_fillslots, name='api-recurring-fillslots'),
25 26
    url(
26 27
        r'^agendas/events/fillslots/$',
27 28
        views.agendas_events_fillslots,
......
40 41
        views.events_fillslots,
41 42
        name='api-agenda-events-fillslots',
42 43
    ),
43
    url(
44
        r'^agenda/(?P<agenda_identifier>[\w-]+)/recurring-events/fillslots/$',
45
        views.recurring_fillslots,
46
        name='api-recurring-fillslots',
47
    ),
48 44
    url(
49 45
        r'^agenda/(?P<agenda_identifier>[\w-]+)/event/$',
50 46
        views.events,
chrono/api/views.py
1583 1583

  
1584 1584
class RecurringFillslots(APIView):
1585 1585
    permission_classes = (permissions.IsAuthenticated,)
1586
    serializer_class = serializers.EventsSlotsSerializer
1586
    serializer_class = serializers.RecurringFillslotsSerializer
1587 1587

  
1588
    def post(self, request, agenda_identifier):
1588
    def post(self, request):
1589 1589
        if not settings.ENABLE_RECURRING_EVENT_BOOKING:
1590 1590
            raise Http404()
1591 1591

  
1592
        agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
1593 1592
        start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
1594 1593
        if not start_datetime or start_datetime < now():
1595 1594
            start_datetime = now()
1596 1595

  
1597
        serializer = self.serializer_class(data=request.data, partial=True)
1596
        agenda_slugs = get_agendas_from_request(request)
1597
        agendas = get_objects_from_slugs(agenda_slugs, qs=Agenda.objects.filter(kind='events'))
1598

  
1599
        context = {'allowed_agenda_slugs': agenda_slugs, 'agendas': agendas}
1600
        serializer = self.serializer_class(data=request.data, partial=True, context=context)
1598 1601
        if not serializer.is_valid():
1599 1602
            raise APIError(
1600 1603
                _('invalid payload'),
......
1605 1608
        payload = serializer.validated_data
1606 1609
        user_external_id = payload['user_external_id']
1607 1610

  
1608
        open_event_slugs = set(agenda.get_open_recurring_events().values_list('slug', flat=True))
1609
        slots = collections.defaultdict(list)
1610
        for slot in payload['slots']:
1611
            try:
1612
                slug, day = slot.split(':')
1613
                day = int(day)
1614
            except ValueError:
1615
                raise APIError(
1616
                    _('invalid slot: %s') % slot,
1617
                    err_class='invalid slot: %s' % slot,
1618
                    http_status=status.HTTP_400_BAD_REQUEST,
1619
                )
1620
            if slug not in open_event_slugs:
1621
                raise APIError(
1622
                    _('event %s is not bookable') % slug,
1623
                    err_class='event %s is not bookable' % slug,
1624
                    http_status=status.HTTP_400_BAD_REQUEST,
1625
                )
1626
            # convert ISO day number to db lookup day number
1627
            day = (day + 1) % 7 + 1
1628
            slots[slug].append(day)
1629

  
1630 1611
        event_filter = Q()
1631
        for slug, days in slots.items():
1632
            event_filter |= Q(agenda=agenda, primary_event__slug=slug, start_datetime__week_day__in=days)
1612
        for agenda_slug, days_by_event in payload['slots'].items():
1613
            for event_slug, days in days_by_event.items():
1614
                event_filter |= Q(
1615
                    agenda__slug=agenda_slug,
1616
                    primary_event__slug=event_slug,
1617
                    start_datetime__week_day__in=days,
1618
                )
1633 1619

  
1634 1620
        events_to_book = Event.objects.filter(event_filter) if event_filter else Event.objects.none()
1635 1621
        events_to_book = events_to_book.filter(start_datetime__gte=start_datetime, cancelled=False)
......
1666 1652
            'err': 0,
1667 1653
            'booking_count': len(bookings),
1668 1654
            'cancelled_booking_count': deleted_count,
1669
            'full_events': [get_event_detail(request, x, agenda=agenda) for x in full_events],
1655
            'full_events': [get_event_detail(request, x, multiple_agendas=True) for x in full_events],
1670 1656
        }
1671 1657
        return Response(response)
1672 1658

  
tests/api/test_fillslot.py
2110 2110
def test_recurring_events_api_fillslots(app, user, freezer):
2111 2111
    freezer.move_to('2021-09-06 12:00')
2112 2112
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
2113
    Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
2113 2114
    event = Event.objects.create(
2114 2115
        label='Event',
2115 2116
        start_datetime=now(),
......
2131 2132
    )
2132 2133
    sunday_event.create_all_recurrences()
2133 2134

  
2134
    resp = app.get('/api/agenda/%s/recurring-events/' % agenda.slug)
2135
    resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
2135 2136
    assert len(resp.json['data']) == 5
2136 2137

  
2137 2138
    app.authorization = ('Basic', ('john.doe', 'password'))
2138
    fillslots_url = '/api/agenda/%s/recurring-events/fillslots/' % agenda.slug
2139
    fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug
2139 2140
    params = {'user_external_id': 'user_id'}
2140 2141
    # Book Monday and Thursday of first event and Sunday of second event
2141
    params['slots'] = 'event:0,event:3,sunday-event:6'
2142
    params['slots'] = 'foo-bar@event:0,foo-bar@event:3,foo-bar@sunday-event:6'
2142 2143
    resp = app.post_json(fillslots_url, params=params)
2143 2144
    assert resp.json['booking_count'] == 156
2144 2145

  
......
2179 2180
    assert resp.json['booking_count'] == 0
2180 2181

  
2181 2182
    # no event in range
2182
    resp = app.post_json(fillslots_url + '?date_start=2020-10-06&date_end=2020-11-06', params=params)
2183
    resp = app.post_json(fillslots_url + '&date_start=2020-10-06&date_end=2020-11-06', params=params)
2183 2184
    assert resp.json['booking_count'] == 0
2184 2185

  
2185
    params['slots'] = 'event:1'
2186
    resp = app.post_json(fillslots_url + '?date_start=2021-10-06&date_end=2021-11-06', params=params)
2186
    params['slots'] = 'foo-bar@event:1'
2187
    resp = app.post_json(fillslots_url + '&date_start=2021-10-06&date_end=2021-11-06', params=params)
2187 2188
    assert resp.json['booking_count'] == 4
2188 2189
    assert Booking.objects.filter(user_external_id='user_id_4').count() == 4
2189 2190

  
2190
    resp = app.post_json(fillslots_url, params={'slots': 'event:0'}, status=400)
2191
    resp = app.post_json(fillslots_url, params={'slots': 'foo-bar@event:0'}, status=400)
2191 2192
    assert resp.json['err'] == 1
2192 2193
    assert resp.json['err_desc'] == 'invalid payload'
2193 2194
    assert resp.json['errors']['user_external_id'] == ['This field is required.']
......
2197 2198
    assert resp.json['err_desc'] == 'invalid payload'
2198 2199
    assert resp.json['errors']['slots'] == ['This field is required.']
2199 2200

  
2200
    resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'a:a'}, status=400)
2201
    resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:a'}, status=400)
2201 2202
    assert resp.json['err'] == 1
2202
    assert resp.json['err_desc'] == 'invalid slot: a:a'
2203
    assert resp.json['errors']['slots'] == ['invalid slot: foo-bar@a:a']
2203 2204

  
2204
    resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'a:1'}, status=400)
2205
    resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:1'}, status=400)
2205 2206
    assert resp.json['err'] == 1
2206
    assert resp.json['err_desc'] == 'event a is not bookable'
2207
    assert resp.json['errors']['slots'] == ['event a of agenda foo-bar is not bookable']
2208

  
2209
    resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar'}, status=400)
2210
    assert resp.json['err'] == 1
2211
    assert resp.json['errors']['slots'] == ['Invalid format for slot foo-bar']
2207 2212

  
2208 2213

  
2209 2214
def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
2210 2215
    freezer.move_to('2021-09-06 12:00')
2211 2216
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
2217
    Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
2212 2218
    event = Event.objects.create(
2213 2219
        label='Event',
2214 2220
        start_datetime=now(),
......
2228 2234
    assert events.filter(booked_waiting_list_places=1).count() == 5
2229 2235

  
2230 2236
    # check that new bookings are put in waiting list despite free slots on main list
2231
    params = {'user_external_id': 'user_id', 'slots': 'event:0'}
2232
    resp = app.post_json('/api/agenda/%s/recurring-events/fillslots/' % agenda.slug, params=params)
2237
    params = {'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'}
2238
    resp = app.post_json('/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug, params=params)
2233 2239
    assert resp.json['booking_count'] == 5
2234 2240
    assert events.filter(booked_waiting_list_places=2).count() == 5
2235 2241

  
......
2237 2243
def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
2238 2244
    freezer.move_to('2021-09-06 12:00')
2239 2245
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
2246
    Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
2240 2247
    event = Event.objects.create(
2241 2248
        label='Event',
2242 2249
        start_datetime=now(),
......
2249 2256
    event.create_all_recurrences()
2250 2257

  
2251 2258
    app.authorization = ('Basic', ('john.doe', 'password'))
2252
    fillslots_url = '/api/agenda/%s/recurring-events/fillslots/' % agenda.slug
2259
    fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug
2253 2260
    params = {'user_external_id': 'user_id'}
2254 2261
    # Book Monday and Thursday
2255
    params['slots'] = 'event:0,event:3'
2262
    params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
2256 2263
    resp = app.post_json(fillslots_url, params=params)
2257 2264
    assert resp.json['booking_count'] == 104
2258 2265
    assert resp.json['cancelled_booking_count'] == 0
......
2261 2268
    assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52
2262 2269

  
2263 2270
    # Change booking to Monday and Tuesday
2264
    params['slots'] = 'event:0,event:1'
2271
    params['slots'] = 'foo-bar@event:0,foo-bar@event:1'
2265 2272
    resp = app.post_json(fillslots_url, params=params)
2266 2273
    assert resp.json['booking_count'] == 52
2267 2274
    assert resp.json['cancelled_booking_count'] == 52
......
2276 2283
    assert Booking.objects.count() == 104
2277 2284

  
2278 2285
    params = {'user_external_id': 'user_id_2'}
2279
    params['slots'] = 'event:0,event:3'
2286
    params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
2280 2287
    resp = app.post_json(fillslots_url, params=params)
2281 2288
    assert resp.json['booking_count'] == 104
2282 2289
    assert resp.json['cancelled_booking_count'] == 0
......
2287 2294
    assert events.filter(booked_places=1).count() == 156
2288 2295
    assert events.filter(booked_waiting_list_places=1).count() == 52
2289 2296

  
2290
    params['slots'] = 'event:1,event:4'
2297
    params['slots'] = 'foo-bar@event:1,foo-bar@event:4'
2291 2298
    resp = app.post_json(fillslots_url, params=params)
2292 2299
    assert resp.json['booking_count'] == 104
2293 2300
    assert resp.json['cancelled_booking_count'] == 104
......
2309 2316
        start_datetime=now() + datetime.timedelta(days=1), places=2, agenda=agenda
2310 2317
    )
2311 2318
    Booking.objects.create(event=normal_event, user_external_id='user_id')
2312
    resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event:0'})
2319
    resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'})
2313 2320
    assert resp.json['cancelled_booking_count'] == 52
2314 2321
    assert Booking.objects.filter(user_external_id='user_id', event=normal_event).count() == 1
2315 2322

  
2316 2323

  
2324
@pytest.mark.freeze_time('2021-09-06 12:00')
2325
def test_recurring_events_api_fillslots_multiple_agendas(app, user):
2326
    agenda = Agenda.objects.create(label='First Agenda', kind='events')
2327
    Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
2328
    start, end = now(), now() + datetime.timedelta(days=30)
2329
    event_a = Event.objects.create(
2330
        label='A',
2331
        start_datetime=start,
2332
        places=2,
2333
        recurrence_end_date=end,
2334
        recurrence_days=[0, 2, 5],
2335
        agenda=agenda,
2336
    )
2337
    event_a.create_all_recurrences()
2338
    event_b = Event.objects.create(
2339
        label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1], agenda=agenda
2340
    )
2341
    event_b.create_all_recurrences()
2342
    agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
2343
    Desk.objects.create(agenda=agenda2, slug='_exceptions_holder')
2344
    event_c = Event.objects.create(
2345
        label='C',
2346
        start_datetime=start,
2347
        places=2,
2348
        recurrence_end_date=end,
2349
        recurrence_days=[2, 3],
2350
        agenda=agenda2,
2351
    )
2352
    event_c.create_all_recurrences()
2353

  
2354
    resp = app.get('/api/agendas/recurring-events/?agendas=first-agenda,second-agenda')
2355
    assert len(resp.json['data']) == 6
2356

  
2357
    app.authorization = ('Basic', ('john.doe', 'password'))
2358
    fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s'
2359
    params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:0,first-agenda@a:5,second-agenda@c:3'}
2360
    resp = app.post_json(fillslots_url % 'first-agenda,second-agenda', params=params)
2361
    assert resp.json['booking_count'] == 13
2362

  
2363
    assert Booking.objects.count() == 13
2364
    assert Booking.objects.filter(event__primary_event=event_a).count() == 9
2365
    assert Booking.objects.filter(event__primary_event=event_b).count() == 0
2366
    assert Booking.objects.filter(event__primary_event=event_c).count() == 4
2367

  
2368
    # update bookings
2369
    params = {'user_external_id': 'user_id', 'slots': 'first-agenda@b:1'}
2370
    resp = app.post_json(fillslots_url % 'first-agenda,second-agenda', params=params)
2371

  
2372
    assert resp.json['booking_count'] == 5
2373
    assert resp.json['cancelled_booking_count'] == 13
2374
    assert Booking.objects.filter(event__primary_event=event_a).count() == 0
2375
    assert Booking.objects.filter(event__primary_event=event_b).count() == 5
2376
    assert Booking.objects.filter(event__primary_event=event_c).count() == 0
2377

  
2378
    # error if slot's agenda is not in querystring
2379
    resp = app.post_json(fillslots_url % 'second-agenda', params=params, status=400)
2380
    assert resp.json['err'] == 1
2381
    assert resp.json['errors']['slots'] == [
2382
        'Some events belong to agendas that are not present in querystring: first-agenda'
2383
    ]
2384

  
2385

  
2386
@pytest.mark.freeze_time('2021-09-06 12:00')
2387
def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
2388
    for i in range(20):
2389
        agenda = Agenda.objects.create(slug=f'{i}', kind='events')
2390
        Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
2391
        start, end = now(), now() + datetime.timedelta(days=30)
2392
        event = Event.objects.create(
2393
            start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1, 2], agenda=agenda
2394
        )
2395
        event.create_all_recurrences()
2396

  
2397
    agenda_slugs = ','.join(str(i) for i in range(20))
2398
    resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda_slugs)
2399
    events_to_book = [x['id'] for x in resp.json['data']]
2400

  
2401
    app.authorization = ('Basic', ('john.doe', 'password'))
2402
    with CaptureQueriesContext(connection) as ctx:
2403
        resp = app.post_json(
2404
            '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda_slugs,
2405
            params={'slots': events_to_book, 'user_external_id': 'user'},
2406
        )
2407
        assert resp.json['booking_count'] == 180
2408
        assert len(ctx.captured_queries) == 36
2409

  
2410

  
2317 2411
@pytest.mark.freeze_time('2021-09-06 12:00')
2318 2412
def test_api_events_fillslots(app, user):
2319 2413
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
2320
-