Projet

Général

Profil

0001-api-add-datetimes-for-multiple-events-agendas-55370.patch

Valentin Deniaud, 10 août 2021 16:13

Télécharger (12,2 ko)

Voir les différences:

Subject: [PATCH 1/2] api: add datetimes for multiple events agendas (#55370)

 chrono/api/serializers.py   |  15 +++++
 chrono/api/urls.py          |   1 +
 chrono/api/views.py         |  86 ++++++++++++++++++++++++++--
 tests/api/test_datetimes.py | 108 ++++++++++++++++++++++++++++++++++++
 4 files changed, 206 insertions(+), 4 deletions(-)
chrono/api/serializers.py
12 12
        return super().to_internal_value(data)
13 13

  
14 14

  
15
class CommaSeparatedStringField(serializers.ListField):
16
    def get_value(self, dictionary):
17
        return super(serializers.ListField, self).get_value(dictionary)
18

  
19
    def to_internal_value(self, data):
20
        data = [s.strip() for s in data.split(',') if s.strip()]
21
        return super().to_internal_value(data)
22

  
23

  
15 24
class SlotSerializer(serializers.Serializer):
16 25
    label = serializers.CharField(max_length=250, allow_blank=True)
17 26
    user_external_id = serializers.CharField(max_length=250, allow_blank=True)
......
118 127
                {'user_external_id': _('user_external_id and exclude_user_external_id have different values')}
119 128
            )
120 129
        return attrs
130

  
131

  
132
class MultipleAgendasDatetimesSerializer(DatetimesSerializer):
133
    agendas = CommaSeparatedStringField(
134
        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
135
    )
chrono/api/urls.py
20 20

  
21 21
urlpatterns = [
22 22
    url(r'^agenda/$', views.agendas),
23
    url(r'^agenda/datetimes/$', views.agendas_datetimes, name='api-agendas-datetimes'),
23 24
    url(r'^agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda_detail),
24 25
    url(r'^agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
25 26
    url(
chrono/api/views.py
449 449

  
450 450

  
451 451
def get_event_detail(
452
    request, event, agenda=None, min_places=1, booked_user_external_id=None, show_events=None
452
    request,
453
    event,
454
    agenda=None,
455
    min_places=1,
456
    booked_user_external_id=None,
457
    show_events=None,
458
    multiple_agendas=False,
453 459
):
454 460
    agenda = agenda or event.agenda
455 461
    details = {
456
        'id': event.slug,
462
        'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug,
457 463
        'slug': event.slug,  # kept for compatibility
458 464
        'text': get_event_text(event, agenda),
459 465
        'label': event.label or '',
......
502 508
    return details
503 509

  
504 510

  
505
def get_events_meta_detail(request, events, agenda=None, min_places=1, show_events=None):
511
def get_events_meta_detail(
512
    request, events, agenda=None, min_places=1, show_events=None, multiple_agendas=False
513
):
506 514
    bookable_datetimes_number_total = 0
507 515
    bookable_datetimes_number_available = 0
508 516
    first_bookable_slot = None
......
512 520
            bookable_datetimes_number_available += 1
513 521
            if not first_bookable_slot:
514 522
                first_bookable_slot = get_event_detail(
515
                    request, event, agenda=agenda, min_places=min_places, show_events=show_events
523
                    request,
524
                    event,
525
                    agenda=agenda,
526
                    min_places=min_places,
527
                    show_events=show_events,
528
                    multiple_agendas=multiple_agendas,
516 529
                )
517 530
    return {
518 531
        'no_bookable_datetimes': bool(bookable_datetimes_number_available == 0),
......
815 828
datetimes = Datetimes.as_view()
816 829

  
817 830

  
831
class MultipleAgendasDatetimes(APIView):
832
    permission_classes = ()
833
    serializer_class = serializers.MultipleAgendasDatetimesSerializer
834

  
835
    def get(self, request):
836
        serializer = self.serializer_class(data=request.query_params)
837
        if not serializer.is_valid():
838
            raise APIError(
839
                _('invalid payload'),
840
                err_class='invalid payload',
841
                errors=serializer.errors,
842
                http_status=status.HTTP_400_BAD_REQUEST,
843
            )
844
        payload = serializer.validated_data
845

  
846
        if 'events' in payload:
847
            raise APIError(
848
                _('events parameter is not supported'),
849
                err_class='events parameter is not supported',
850
                http_status=status.HTTP_400_BAD_REQUEST,
851
            )
852

  
853
        agenda_slugs = payload['agendas']
854
        agendas = Agenda.objects.filter(slug__in=agenda_slugs, kind='events')
855
        if not len(agendas) == len(agenda_slugs):
856
            not_found_slugs = sorted(set(agenda_slugs) - {agenda.slug for agenda in agendas})
857
            raise APIError(
858
                _('events agendas do not exist: %s') % ', '.join(not_found_slugs),
859
                err_class='events agendas do not exist',
860
                http_status=status.HTTP_404_NOT_FOUND,
861
            )
862

  
863
        user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
864
        entries = []
865
        for agenda in agendas:
866
            entries.extend(
867
                agenda.get_open_events(
868
                    annotate_queryset=True,
869
                    min_start=payload.get('date_start'),
870
                    max_start=payload.get('date_end'),
871
                    user_external_id=user_external_id,
872
                )
873
            )
874

  
875
        response = {
876
            'data': [
877
                get_event_detail(
878
                    request,
879
                    x,
880
                    min_places=payload['min_places'],
881
                    booked_user_external_id=payload.get('user_external_id'),
882
                    multiple_agendas=True,
883
                )
884
                for x in entries
885
            ],
886
            'meta': get_events_meta_detail(
887
                request, entries, min_places=payload['min_places'], multiple_agendas=True
888
            ),
889
        }
890
        return Response(response)
891

  
892

  
893
agendas_datetimes = MultipleAgendasDatetimes.as_view()
894

  
895

  
818 896
class MeetingDatetimes(APIView):
819 897
    permission_classes = ()
820 898

  
tests/api/test_datetimes.py
2 2
import urllib.parse as urlparse
3 3

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

  
8 10
from chrono.agendas.models import Agenda, Booking, Desk, Event, TimePeriodException
......
1344 1346
    freezer.move_to(event.recurrence_end_date)
1345 1347
    resp = app.get('/api/agenda/%s/recurring-events/' % agenda.slug)
1346 1348
    assert len(resp.json['data']) == 1
1349

  
1350

  
1351
@pytest.mark.freeze_time('2021-05-06 14:00')
1352
def test_datetimes_multiple_agendas(app):
1353
    first_agenda = Agenda.objects.create(label='First agenda', kind='events')
1354
    Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
1355
    event = Event.objects.create(
1356
        slug='event',
1357
        start_datetime=now() + datetime.timedelta(days=5),
1358
        places=5,
1359
        agenda=first_agenda,
1360
    )
1361
    second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
1362
    Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
1363
    event = Event.objects.create(
1364
        slug='event',
1365
        start_datetime=now() + datetime.timedelta(days=6),
1366
        places=5,
1367
        agenda=second_agenda,
1368
    )
1369
    Booking.objects.create(event=event)
1370

  
1371
    agenda_slugs = '%s,%s' % (first_agenda.slug, second_agenda.slug)
1372
    resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs})
1373
    assert len(resp.json['data']) == 2
1374
    assert resp.json['data'][0]['id'] == 'first-agenda@event'
1375
    assert resp.json['data'][0]['text'] == 'May 11, 2021, 4 p.m.'
1376
    assert resp.json['data'][0]['places']['available'] == 5
1377

  
1378
    assert resp.json['data'][1]['id'] == 'second-agenda@event'
1379
    assert resp.json['data'][1]['text'] == 'May 12, 2021, 4 p.m.'
1380
    assert resp.json['data'][1]['places']['available'] == 4
1381

  
1382
    # check user_external_id
1383
    Booking.objects.create(event=event, user_external_id='user')
1384
    resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'user_external_id': 'user'})
1385
    assert resp.json['data'][0]['places']['available'] == 5
1386
    assert 'booked_for_external_user' not in resp.json['data'][0]
1387
    assert resp.json['data'][0]['disabled'] is False
1388

  
1389
    assert resp.json['data'][1]['places']['available'] == 3
1390
    assert resp.json['data'][1]['booked_for_external_user'] == 'main-list'
1391
    assert resp.json['data'][1]['disabled'] is True
1392

  
1393
    # check exclude_user_external_id
1394
    resp = app.get(
1395
        '/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'exclude_user_external_id': 'user'}
1396
    )
1397
    assert 'booked_for_external_user' not in resp.json['data'][0]
1398
    assert resp.json['data'][0]['disabled'] is False
1399

  
1400
    assert 'booked_for_external_user' not in resp.json['data'][1]
1401
    assert resp.json['data'][1]['disabled'] is True
1402

  
1403
    # check min_places
1404
    resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4})
1405
    assert resp.json['data'][0]['disabled'] is False
1406
    assert resp.json['data'][1]['disabled'] is True
1407

  
1408
    # check meta
1409
    resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4})
1410
    assert resp.json['meta']['bookable_datetimes_number_total'] == 2
1411
    assert resp.json['meta']['bookable_datetimes_number_available'] == 1
1412
    assert resp.json['meta']['first_bookable_slot'] == resp.json['data'][0]
1413

  
1414
    # check date_start
1415
    date_start = localtime() + datetime.timedelta(days=5, hours=1)
1416
    resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'date_start': date_start})
1417
    assert len(resp.json['data']) == 1
1418
    assert resp.json['data'][0]['id'] == 'second-agenda@event'
1419

  
1420
    # check date_end
1421
    date_end = localtime() + datetime.timedelta(days=5, hours=1)
1422
    resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'date_end': date_end})
1423
    assert len(resp.json['data']) == 1
1424
    assert resp.json['data'][0]['id'] == 'first-agenda@event'
1425

  
1426
    resp = app.get(
1427
        '/api/agenda/datetimes/',
1428
        params={'agendas': agenda_slugs, 'date_start': date_start, 'date_end': date_end},
1429
    )
1430
    assert len(resp.json['data']) == 0
1431

  
1432
    # invalid slugs
1433
    resp = app.get('/api/agenda/datetimes/', params={'agendas': 'xxx'}, status=404)
1434
    assert resp.json['err_desc'] == 'events agendas do not exist: xxx'
1435

  
1436
    resp = app.get('/api/agenda/datetimes/', params={'agendas': 'first-agenda,xxx,yyy'}, status=404)
1437
    assert resp.json['err_desc'] == 'events agendas do not exist: xxx, yyy'
1438

  
1439
    # no support for past events
1440
    resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'events': 'past'}, status=400)
1441

  
1442

  
1443
@pytest.mark.freeze_time('2021-05-06 14:00')
1444
def test_datetimes_multiple_agendas_queries(app):
1445
    for i in range(10):
1446
        agenda = Agenda.objects.create(label=str(i), kind='events')
1447
        Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
1448
        Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
1449
        Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
1450

  
1451
    with CaptureQueriesContext(connection) as ctx:
1452
        resp = app.get('/api/agenda/datetimes/', params={'agendas': ','.join(str(i) for i in range(10))})
1453
        assert len(resp.json['data']) == 20
1454
        assert len(ctx.captured_queries) == 21
1347
-