Projet

Général

Profil

0001-api-show_past_events-for-agendas-datetimes-endpoint-.patch

Lauréline Guérin, 24 septembre 2021 09:02

Télécharger (17,4 ko)

Voir les différences:

Subject: [PATCH] api: show_past_events for agendas/datetimes endpoint (#56615)

 chrono/agendas/models.py    | 39 +++++++++++++------
 chrono/api/serializers.py   |  1 +
 chrono/api/views.py         | 67 +++++++++++++++++++++-----------
 tests/api/test_datetimes.py | 76 +++++++++++++++++++++++++++++++++++--
 4 files changed, 146 insertions(+), 37 deletions(-)
chrono/agendas/models.py
653 653

  
654 654
        if prefetched_queryset:
655 655
            entries = self.prefetched_events
656
            # we may have past events
657
            entries = [e for e in entries if e.start_datetime >= localtime(now())]
656 658
        else:
657 659
            # recurring events are never opened
658 660
            entries = self.event_set.filter(recurrence_days__isnull=True)
......
701 703

  
702 704
    def get_past_events(
703 705
        self,
706
        prefetched_queryset=False,
704 707
        min_start=None,
705 708
        max_start=None,
706 709
        user_external_id=None,
707 710
    ):
708 711
        assert self.kind == 'events'
709 712

  
710
        # recurring events are never opened
711
        entries = self.event_set.filter(recurrence_days__isnull=True)
712
        # exclude canceled events except for event recurrences
713
        entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False))
714
        # we want only past events
715
        entries = entries.filter(start_datetime__lt=localtime(now()))
713
        if prefetched_queryset:
714
            entries = self.prefetched_events
715
            # we may have future events
716
            entries = [e for e in entries if e.start_datetime < localtime(now())]
717
        else:
718
            # recurring events are never opened
719
            entries = self.event_set.filter(recurrence_days__isnull=True)
720
            # exclude canceled events except for event recurrences
721
            entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False))
722
            # we want only past events
723
            entries = entries.filter(start_datetime__lt=localtime(now()))
716 724

  
717
        if min_start:
725
        if min_start and not prefetched_queryset:
718 726
            entries = entries.filter(start_datetime__gte=min_start)
719 727

  
720
        if max_start:
728
        if max_start and not prefetched_queryset:
721 729
            entries = entries.filter(start_datetime__lt=max_start)
722 730

  
723 731
        if user_external_id:
......
728 736
                entries,
729 737
                min_start,
730 738
                min(max_start or localtime(now()), localtime(now())),
739
                prefetched_queryset=prefetched_queryset,
731 740
            )
732 741

  
733 742
        return entries
......
869 878
            ]
870 879

  
871 880
    @staticmethod
872
    def prefetch_events_and_exceptions(qs, annotate_events=False, user_external_id=None):
881
    def prefetch_events_and_exceptions(
882
        qs, user_external_id=None, show_past_events=False, min_start=None, max_start=None
883
    ):
873 884
        event_queryset = Event.objects.filter(
874 885
            Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
886
            recurrence_days__isnull=True,
875 887
            cancelled=False,
876
            start_datetime__gte=localtime(now()),
877 888
        ).order_by()
878 889

  
879 890
        if user_external_id:
880 891
            event_queryset = Event.annotate_queryset_for_user(event_queryset, user_external_id)
881
        if annotate_events:
882
            event_queryset = Event.annotate_queryset(event_queryset)
892
        if not show_past_events:
893
            event_queryset = event_queryset.filter(start_datetime__gte=localtime(now()))
894
        if min_start:
895
            event_queryset = event_queryset.filter(start_datetime__gte=min_start)
896
        if max_start:
897
            event_queryset = event_queryset.filter(start_datetime__lt=max_start)
883 898

  
884 899
        recurring_event_queryset = Event.objects.filter(
885 900
            Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
chrono/api/serializers.py
133 133
    agendas = CommaSeparatedStringField(
134 134
        required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
135 135
    )
136
    show_past_events = serializers.BooleanField(default=False)
chrono/api/views.py
415 415
    return places
416 416

  
417 417

  
418
def is_event_disabled(event, min_places=1, disable_booked=True):
418
def is_event_disabled(event, min_places=1, disable_booked=True, bookable_events=None):
419 419
    if disable_booked and getattr(event, 'user_places_count', 0) > 0:
420 420
        return True
421 421
    if event.start_datetime < now():
422
        # event is past => not disabled (always ok to book a past event)
423
        return False
422
        # event is past
423
        if bookable_events in ['all', 'past']:
424
            # but we want to book past events, and it's always ok
425
            return False
426
        # we just want to show past events, but they are not bookable
427
        return True
424 428
    if event.remaining_places < min_places and event.remaining_waiting_list_places < min_places:
425 429
        return True
426 430
    return False
......
454 458
    agenda=None,
455 459
    min_places=1,
456 460
    booked_user_external_id=None,
457
    show_events=None,
461
    bookable_events=None,
458 462
    multiple_agendas=False,
459 463
    disable_booked=True,
460 464
):
......
470 474
        'pricing': event.pricing,
471 475
        'url': event.url,
472 476
        'duration': event.duration,
473
        'disabled': is_event_disabled(event, min_places=min_places, disable_booked=disable_booked),
477
        'disabled': is_event_disabled(
478
            event, min_places=min_places, disable_booked=disable_booked, bookable_events=bookable_events
479
        ),
474 480
        'api': {
475 481
            'bookings_url': request.build_absolute_uri(
476 482
                reverse(
......
499 505
        },
500 506
        'places': get_event_places(event),
501 507
    }
502
    if show_events is not None:
503
        details['api']['fillslot_url'] += '?events=%s' % show_events
508
    if bookable_events is not None:
509
        details['api']['fillslot_url'] += '?events=%s' % bookable_events
504 510
    if booked_user_external_id:
505 511
        if getattr(event, 'user_places_count', 0) > 0:
506 512
            details['booked_for_external_user'] = 'main-list'
......
511 517

  
512 518

  
513 519
def get_events_meta_detail(
514
    request, events, agenda=None, min_places=1, show_events=None, multiple_agendas=False
520
    request, events, agenda=None, min_places=1, bookable_events=None, multiple_agendas=False
515 521
):
516 522
    bookable_datetimes_number_total = 0
517 523
    bookable_datetimes_number_available = 0
518 524
    first_bookable_slot = None
519 525
    for event in events:
520 526
        bookable_datetimes_number_total += 1
521
        if not is_event_disabled(event, min_places=min_places):
527
        if not is_event_disabled(event, min_places=min_places, bookable_events=bookable_events):
522 528
            bookable_datetimes_number_available += 1
523 529
            if not first_bookable_slot:
524 530
                first_bookable_slot = get_event_detail(
......
526 532
                    event,
527 533
                    agenda=agenda,
528 534
                    min_places=min_places,
529
                    show_events=show_events,
535
                    bookable_events=bookable_events,
530 536
                    multiple_agendas=multiple_agendas,
531 537
                )
532 538
    return {
......
749 755

  
750 756
        user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
751 757
        disable_booked = bool(payload.get('exclude_user_external_id'))
752
        show_events_raw = payload.get('events')
753
        show_events = show_events_raw or 'future'
754
        show_past = show_events in ['all', 'past']
755
        show_future = show_events in ['all', 'future']
758
        bookable_events_raw = payload.get('events')
759
        bookable_events = bookable_events_raw or 'future'
760
        book_past = bookable_events in ['all', 'past']
761
        book_future = bookable_events in ['all', 'future']
756 762

  
757 763
        entries = []
758
        if show_past:
764
        if book_past:
759 765
            entries += agenda.get_past_events(
760 766
                min_start=payload.get('date_start'),
761 767
                max_start=payload.get('date_end'),
762 768
                user_external_id=user_external_id,
763 769
            )
764
        if show_future:
770
        if book_future:
765 771
            entries += agenda.get_open_events(
766 772
                min_start=payload.get('date_start'),
767 773
                max_start=payload.get('date_end'),
......
772 778
            entries = [
773 779
                e
774 780
                for e in entries
775
                if not is_event_disabled(e, payload['min_places'], disable_booked=disable_booked)
781
                if not is_event_disabled(
782
                    e, payload['min_places'], disable_booked=disable_booked, bookable_events=bookable_events
783
                )
776 784
            ]
777 785

  
778 786
        response = {
......
783 791
                    agenda=agenda,
784 792
                    min_places=payload['min_places'],
785 793
                    booked_user_external_id=payload.get('user_external_id'),
786
                    show_events=show_events_raw,
794
                    bookable_events=bookable_events_raw,
787 795
                    disable_booked=disable_booked,
788 796
                )
789 797
                for x in entries
790 798
            ],
791 799
            'meta': get_events_meta_detail(
792
                request, entries, agenda=agenda, min_places=payload['min_places'], show_events=show_events_raw
800
                request,
801
                entries,
802
                agenda=agenda,
803
                min_places=payload['min_places'],
804
                bookable_events=bookable_events_raw,
793 805
            ),
794 806
        }
795 807
        return Response(response)
......
825 837

  
826 838
        user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
827 839
        disable_booked = bool(payload.get('exclude_user_external_id'))
828
        agendas = Agenda.prefetch_events_and_exceptions(agendas, user_external_id=user_external_id)
840
        show_past_events = bool(payload.get('show_past_events'))
841
        agendas = Agenda.prefetch_events_and_exceptions(
842
            agendas,
843
            user_external_id=user_external_id,
844
            show_past_events=show_past_events,
845
            min_start=payload.get('date_start'),
846
            max_start=payload.get('date_end'),
847
        )
829 848

  
830 849
        entries = []
831 850
        for agenda in agendas:
851
            if show_past_events:
852
                entries.extend(
853
                    agenda.get_past_events(
854
                        prefetched_queryset=True,
855
                    )
856
                )
832 857
            entries.extend(
833 858
                agenda.get_open_events(
834 859
                    prefetched_queryset=True,
835
                    min_start=payload.get('date_start'),
836
                    max_start=payload.get('date_end'),
837 860
                )
838 861
            )
839 862

  
tests/api/test_datetimes.py
1376 1376
        places=5,
1377 1377
        agenda=first_agenda,
1378 1378
    )
1379
    Event.objects.create(  # not visible in datetimes api
1380
        slug='recurring',
1381
        start_datetime=now() + datetime.timedelta(days=5),
1382
        recurrence_days=[localtime().weekday()],
1383
        recurrence_end_date=now() + datetime.timedelta(days=5),
1384
        places=5,
1385
        agenda=first_agenda,
1386
    )
1379 1387
    second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
1380 1388
    Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
1381 1389
    event = Event.objects.create(
......
1454 1462
    resp = app.get('/api/agendas/datetimes/', params={'agendas': 'first-agenda,xxx,yyy'}, status=400)
1455 1463
    assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
1456 1464

  
1457
    # no support for past events
1465
    # no support for past events booking (they are never bookable)
1458 1466
    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'events': 'past'}, status=400)
1459 1467

  
1468
    # but it's possible to show past events
1469
    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
1470
    assert len(resp.json['data']) == 2
1471
    assert resp.json['data'][0]['id'] == 'first-agenda@event'
1472
    assert resp.json['data'][1]['id'] == 'second-agenda@event'
1473

  
1474
    Event.objects.create(
1475
        slug='event-in-past',
1476
        start_datetime=now() - datetime.timedelta(days=5),
1477
        places=5,
1478
        agenda=first_agenda,
1479
    )
1480
    Event.objects.create(  # not visible in datetimes api
1481
        slug='recurring-in-past',
1482
        start_datetime=now() - datetime.timedelta(days=5),
1483
        recurrence_days=[localtime().weekday()],
1484
        recurrence_end_date=now() - datetime.timedelta(days=5),
1485
        places=5,
1486
        agenda=first_agenda,
1487
    )
1488

  
1489
    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
1490
    assert len(resp.json['data']) == 3
1491
    assert resp.json['data'][0]['id'] == 'first-agenda@event-in-past'
1492
    assert resp.json['data'][0]['disabled'] is True
1493
    assert resp.json['data'][1]['id'] == 'first-agenda@event'
1494
    assert resp.json['data'][1]['disabled'] is False
1495
    assert resp.json['data'][2]['id'] == 'second-agenda@event'
1496
    assert resp.json['data'][2]['disabled'] is False
1497

  
1498
    date_start = localtime() - datetime.timedelta(days=4)
1499
    resp = app.get(
1500
        '/api/agendas/datetimes/',
1501
        params={'agendas': agenda_slugs, 'date_start': date_start, 'show_past_events': True},
1502
    )
1503
    assert len(resp.json['data']) == 2
1504
    assert resp.json['data'][0]['id'] == 'first-agenda@event'
1505
    assert resp.json['data'][1]['id'] == 'second-agenda@event'
1506

  
1507
    date_end = localtime() + datetime.timedelta(days=5, hours=1)
1508
    resp = app.get(
1509
        '/api/agendas/datetimes/',
1510
        params={'agendas': agenda_slugs, 'date_end': date_end, 'show_past_events': True},
1511
    )
1512
    assert len(resp.json['data']) == 2
1513
    assert resp.json['data'][0]['id'] == 'first-agenda@event-in-past'
1514
    assert resp.json['data'][1]['id'] == 'first-agenda@event'
1515

  
1460 1516

  
1461 1517
@pytest.mark.freeze_time('2021-05-06 14:00')
1462 1518
def test_datetimes_multiple_agendas_sort(app):
......
1466 1522
    second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
1467 1523
    Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
1468 1524
    Event.objects.create(label='09-05', start_datetime=now().replace(day=9), places=5, agenda=second_agenda)
1525
    Event.objects.create(label='04-05', start_datetime=now().replace(day=4), places=5, agenda=second_agenda)
1469 1526
    third_agenda = Agenda.objects.create(label='Third agenda', kind='events')
1470 1527
    Desk.objects.create(agenda=third_agenda, slug='_exceptions_holder')
1471 1528
    Event.objects.create(label='09-05', start_datetime=now().replace(day=9), places=5, agenda=third_agenda)
1529
    Event.objects.create(label='04-05', start_datetime=now().replace(day=4), places=5, agenda=third_agenda)
1472 1530

  
1473 1531
    # check events are ordered by start_datetime and then by agenda order in querystring
1474 1532
    agenda_slugs = ','.join((first_agenda.slug, third_agenda.slug, second_agenda.slug))
......
1478 1536
    assert resp.json['data'][1]['id'] == 'second-agenda@09-05'
1479 1537
    assert resp.json['data'][2]['id'] == 'first-agenda@10-05'
1480 1538

  
1539
    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
1540
    assert len(resp.json['data']) == 5
1541
    assert resp.json['data'][0]['id'] == 'third-agenda@04-05'
1542
    assert resp.json['data'][1]['id'] == 'second-agenda@04-05'
1543
    assert resp.json['data'][2]['id'] == 'third-agenda@09-05'
1544
    assert resp.json['data'][3]['id'] == 'second-agenda@09-05'
1545
    assert resp.json['data'][4]['id'] == 'first-agenda@10-05'
1546

  
1481 1547

  
1482 1548
@pytest.mark.freeze_time('2021-05-06 14:00')
1483 1549
def test_datetimes_multiple_agendas_queries(app):
1484 1550
    for i in range(10):
1485 1551
        agenda = Agenda.objects.create(label=str(i), kind='events')
1486 1552
        Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
1553
        Event.objects.create(start_datetime=now() - datetime.timedelta(days=5), places=5, agenda=agenda)
1487 1554
        Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
1488 1555
        Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
1489 1556

  
1490 1557
    with CaptureQueriesContext(connection) as ctx:
1491
        resp = app.get('/api/agendas/datetimes/', params={'agendas': ','.join(str(i) for i in range(10))})
1492
        assert len(resp.json['data']) == 20
1558
        resp = app.get(
1559
            '/api/agendas/datetimes/',
1560
            params={'agendas': ','.join(str(i) for i in range(10)), 'show_past_events': True},
1561
        )
1562
        assert len(resp.json['data']) == 30
1493 1563
        assert len(ctx.captured_queries) == 7
1494
-