Projet

Général

Profil

0001-api-add-a-meta-dict-on-datetimes-endpoints-50278.patch

Nicolas Roche, 21 janvier 2021 10:53

Télécharger (16,2 ko)

Voir les différences:

Subject: [PATCH] api: add a meta dict on datetimes endpoints (#50278)

 chrono/agendas/models.py |   4 +-
 chrono/api/views.py      |  69 ++++++++----
 tests/test_api.py        | 225 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 276 insertions(+), 22 deletions(-)
chrono/agendas/models.py
744 744
        Then we iterate, advancing by base_duration minutes each time.
745 745

  
746 746
        If we cross the end_time of the period or end of the current_day
747 747
        (means end_time is midnight), it advance time to self.start_time on
748 748
        the next week (same weekday, same start, one week in the future).
749 749

  
750 750
        When it crosses end_datetime it stops.
751 751

  
752
        Generated start_datetime MUST be in the local timezone as the API
753
        needs it to generate stable ids.
752
        Generated start_datetime MUST be in the local timezone, and the local
753
        timezone must not change, as the API needs it to generate stable ids.
754 754
        """
755 755
        meeting_duration = datetime.timedelta(minutes=meeting_duration)
756 756
        duration = datetime.timedelta(minutes=base_duration)
757 757

  
758 758
        real_min_datetime = min_datetime + datetime.timedelta(days=self.weekday - min_datetime.weekday())
759 759
        if real_min_datetime < min_datetime:
760 760
            real_min_datetime += datetime.timedelta(days=7)
761 761

  
chrono/api/views.py
393 393
                    kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
394 394
                )
395 395
            ),
396 396
        },
397 397
        'places': get_event_places(event),
398 398
    }
399 399

  
400 400

  
401
def get_events_meta_detail(events):
402
    bookable_datetimes_number_total = 0
403
    bookable_datetimes_number_available = 0
404
    bookable_datetimes_first = None
405
    for event in events:
406
        bookable_datetimes_number_total += 1
407
        if not bool(event.full):
408
            bookable_datetimes_number_available += 1
409
            if not bookable_datetimes_first:
410
                bookable_datetimes_first = format_response_datetime(event.start_datetime)
411
    return {
412
        'no_bookable_datetimes': bool(bookable_datetimes_number_available == 0),
413
        'bookable_datetimes_number_total': bookable_datetimes_number_total,
414
        'bookable_datetimes_number_available': bookable_datetimes_number_available,
415
        'bookable_datetimes_first': bookable_datetimes_first,
416
    }
417

  
418

  
401 419
def get_resources_from_request(request, agenda):
402 420
    if agenda.kind != 'meetings' or 'resources' not in request.GET:
403 421
        return []
404 422
    resources_slugs = [s for s in request.GET['resources'].split(',') if s]
405 423
    resources = list(agenda.resources.filter(slug__in=resources_slugs))
406 424
    if len(resources) != len(resources_slugs):
407 425
        unknown_slugs = set(resources_slugs) - set([r.slug for r in resources])
408 426
        unknown_slugs = sorted(list(unknown_slugs))
......
497 515

  
498 516
        if 'date_end' in request.GET:
499 517
            entries = entries.filter(
500 518
                start_datetime__lt=make_aware(
501 519
                    datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
502 520
                )
503 521
            )
504 522

  
505
        response = {'data': [get_event_detail(request, x, agenda=agenda) for x in entries]}
523
        response = {
524
            'data': [get_event_detail(request, x, agenda=agenda) for x in entries],
525
            'meta': get_events_meta_detail(entries),
526
        }
506 527
        return Response(response)
507 528

  
508 529

  
509 530
datetimes = Datetimes.as_view()
510 531

  
511 532

  
512 533
class MeetingDatetimes(APIView):
513 534
    permission_classes = ()
......
598 619
            reverse(
599 620
                'api-fillslot',
600 621
                kwargs={'agenda_identifier': agenda.slug, 'event_identifier': fake_event_identifier},
601 622
            )
602 623
        )
603 624
        if resources:
604 625
            fillslot_url += '?resources=%s' % ','.join(r.slug for r in resources)
605 626

  
606
        def make_id(start_datetime, meeting_type):
607
            """Make virtual id for a slot, combining meeting_type.id and
608
            iso-format of date and time.
609
            !!! The datetime must always be in the local timezone and the local
610
            timezone must not change if we want the id to be stable.
611
            It MUST be a garanty of SharedTimePeriod.get_time_slots(),
612
            !!!
613
            """
614
            return '%s:%s' % (
615
                meeting_type.slug,
616
                start_datetime.strftime('%Y-%m-%d-%H%M'),
617
            )
618

  
619
        response = {
620
            'data': [
627
        bookable_datetimes_number_total = 0
628
        bookable_datetimes_number_available = 0
629
        bookable_datetimes_first = None
630
        data = []
631
        for slot in generator_of_unique_slots:
632
            bookable_datetimes_number_total += 1
633
            if not bool(slot.full):
634
                bookable_datetimes_number_available += 1
635
                if not bookable_datetimes_first:
636
                    bookable_datetimes_first = format_response_datetime(slot.start_datetime)
637

  
638
            # Make virtual id for a slot, combining meeting_type.id and
639
            # iso-format of date and time.
640
            # (SharedTimePeriod.get_time_slots() generate datetime in fixed local timezone,
641
            # in order to make slot_id stable.)
642
            slot_id = '%s:%s' % (meeting_type.slug, slot.start_datetime.strftime('%Y-%m-%d-%H%M'))
643
            data.append(
621 644
                {
622 645
                    'id': slot_id,
623 646
                    'datetime': format_response_datetime(slot.start_datetime),
624 647
                    'text': date_format(slot.start_datetime, format='DATETIME_FORMAT'),
625 648
                    'disabled': bool(slot.full),
626 649
                    'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, slot_id)},
627 650
                }
628
                for slot in generator_of_unique_slots
629
                # we do not have the := operator, so we do that
630
                for slot_id in [make_id(slot.start_datetime, meeting_type)]
631
            ]
651
            )
652

  
653
        response = {
654
            'data': data,
655
            'meta': {
656
                'no_bookable_datetimes': bool(bookable_datetimes_number_available == 0),
657
                'bookable_datetimes_number_total': bookable_datetimes_number_total,
658
                'bookable_datetimes_number_available': bookable_datetimes_number_available,
659
                'bookable_datetimes_first': bookable_datetimes_first,
660
            },
632 661
        }
633 662
        return Response(response)
634 663

  
635 664

  
636 665
meeting_datetimes = MeetingDatetimes.as_view()
637 666

  
638 667

  
639 668
class MeetingList(APIView):
tests/test_api.py
4812 4812
    agenda_foo.save()
4813 4813
    resp = app.get(foo_api_url, params=params)
4814 4814
    assert len(resp.json['data']) == 16
4815 4815
    # also on virtual agenda
4816 4816
    virtual_agenda.maximal_booking_delay = 5
4817 4817
    virtual_agenda.save()
4818 4818
    resp = app.get(virtual_api_url, params=params)
4819 4819
    assert len(resp.json['data']) == 16
4820

  
4821

  
4822
def test_datetimes_api_meta(app, freezer):
4823
    # 2017-05-20 -> saturday
4824
    freezer.move_to(make_aware(datetime.datetime(year=2017, month=5, day=20, hour=1, minute=12)))
4825

  
4826
    agenda = Agenda.objects.create(label=u'Foo bar')
4827
    first_date = localtime(now()).replace(hour=17, minute=0, second=0, microsecond=0)
4828
    first_date += datetime.timedelta(days=1)
4829
    for i in range(3):
4830
        event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda)
4831
        event.save()
4832
    # a date in the past
4833
    event = Event(start_datetime=first_date - datetime.timedelta(days=10), places=10, agenda=agenda)
4834
    event.save()
4835

  
4836
    events = Event.objects.filter(agenda_id=agenda.id).exclude(start_datetime__lt=now())
4837
    assert len(events) == 3
4838
    api_url = '/api/agenda/%s/datetimes/' % agenda.slug
4839
    resp = app.get(api_url)
4840
    assert len(resp.json['data']) == 3
4841
    assert resp.json['meta'] == {
4842
        'no_bookable_datetimes': False,
4843
        'bookable_datetimes_number_total': 3,
4844
        'bookable_datetimes_number_available': 3,
4845
        'bookable_datetimes_first': '2017-05-21 17:00:00',
4846
    }
4847

  
4848
    def simulate_booking(event, nb_places):
4849
        for i in range(nb_places):
4850
            Booking(event=event).save()
4851

  
4852
    simulate_booking(events[0], 10)
4853
    resp = app.get(api_url)
4854
    assert len(resp.json['data']) == 3
4855
    assert resp.json['meta'] == {
4856
        'no_bookable_datetimes': False,
4857
        'bookable_datetimes_number_total': 3,
4858
        'bookable_datetimes_number_available': 3,
4859
        'bookable_datetimes_first': '2017-05-21 17:00:00',
4860
    }
4861

  
4862
    simulate_booking(events[0], 10)
4863
    resp = app.get(api_url)
4864
    assert len(resp.json['data']) == 3
4865
    assert resp.json['meta'] == {
4866
        'no_bookable_datetimes': False,
4867
        'bookable_datetimes_number_total': 3,
4868
        'bookable_datetimes_number_available': 2,
4869
        'bookable_datetimes_first': '2017-05-22 17:00:00',
4870
    }
4871

  
4872
    simulate_booking(events[1], 20)
4873
    simulate_booking(events[2], 20)
4874
    resp = app.get(api_url)
4875
    assert len(resp.json['data']) == 3
4876
    assert resp.json['meta'] == {
4877
        'no_bookable_datetimes': True,
4878
        'bookable_datetimes_number_total': 3,
4879
        'bookable_datetimes_number_available': 0,
4880
        'bookable_datetimes_first': None,
4881
    }
4882

  
4883

  
4884
def test_datetimes_api_meetings_agenda_meta(app, freezer):
4885
    # 2017-05-20 -> saturday
4886
    freezer.move_to(make_aware(datetime.datetime(year=2017, month=5, day=20, hour=1, minute=12)))
4887
    meetings_agenda = Agenda.objects.create(
4888
        label=u'Foo bar Meeting', kind='meetings', maximal_booking_delay=3
4889
    )
4890
    meeting_type = MeetingType(agenda=meetings_agenda, label='Blah', duration=30)
4891
    meeting_type.save()
4892

  
4893
    desk1 = Desk.objects.create(agenda=meetings_agenda, label='Desk 1')
4894
    desk2 = Desk.objects.create(agenda=meetings_agenda, label='Desk 2')
4895
    test_1st_weekday = (localtime(now()).weekday() + 2) % 7
4896
    for desk in desk1, desk2:
4897
        TimePeriod(
4898
            weekday=test_1st_weekday,
4899
            start_time=datetime.time(10, 0),
4900
            end_time=datetime.time(12, 0),
4901
            desk=desk,
4902
        ).save()
4903

  
4904
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
4905
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug)
4906
    resp = app.get(api_url)
4907
    assert len(resp.json['data']) == 4
4908
    assert resp.json['data'][2]['disabled'] is False
4909
    assert resp.json['meta'] == {
4910
        'no_bookable_datetimes': False,
4911
        'bookable_datetimes_number_total': 4,
4912
        'bookable_datetimes_number_available': 4,
4913
        'bookable_datetimes_first': '2017-05-22 10:00:00',
4914
    }
4915

  
4916
    def simulate_booking(slot, desk):
4917
        dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M')
4918
        ev = Event(
4919
            agenda=meetings_agenda,
4920
            meeting_type=meeting_type,
4921
            places=1,
4922
            full=False,
4923
            start_datetime=make_aware(dt),
4924
            desk=desk,
4925
        )
4926
        ev.save()
4927
        booking = Booking(event=ev)
4928
        booking.save()
4929

  
4930
    simulate_booking(resp.json['data'][0], desk1)
4931
    resp = app.get(api_url)
4932
    assert len(resp.json['data']) == 4
4933
    assert resp.json['data'][0]['disabled'] is False
4934
    assert resp.json['meta'] == {
4935
        'no_bookable_datetimes': False,
4936
        'bookable_datetimes_number_total': 4,
4937
        'bookable_datetimes_number_available': 4,
4938
        'bookable_datetimes_first': '2017-05-22 10:00:00',
4939
    }
4940

  
4941
    simulate_booking(resp.json['data'][0], desk2)
4942
    resp = app.get(api_url)
4943
    assert len(resp.json['data']) == 4
4944
    assert resp.json['data'][0]['disabled'] is True
4945
    assert resp.json['meta'] == {
4946
        'no_bookable_datetimes': False,
4947
        'bookable_datetimes_number_total': 4,
4948
        'bookable_datetimes_number_available': 3,
4949
        'bookable_datetimes_first': '2017-05-22 10:30:00',
4950
    }
4951

  
4952
    for idx in range(1, 4):
4953
        simulate_booking(resp.json['data'][idx], desk1)
4954
        simulate_booking(resp.json['data'][idx], desk2)
4955
    resp = app.get(api_url)
4956
    assert len(resp.json['data']) == 4
4957
    assert resp.json['meta'] == {
4958
        'no_bookable_datetimes': True,
4959
        'bookable_datetimes_number_total': 4,
4960
        'bookable_datetimes_number_available': 0,
4961
        'bookable_datetimes_first': None,
4962
    }
4963

  
4964

  
4965
def test_datetimes_api_virtual_meetings_agenda_meta(app, freezer):
4966
    # 2017-05-20 -> saturday
4967
    freezer.move_to(make_aware(datetime.datetime(year=2017, month=5, day=20, hour=1, minute=12)))
4968
    meetings_agenda1 = Agenda.objects.create(label=u'Foo Meeting', kind='meetings', maximal_booking_delay=3)
4969
    meetings_agenda2 = Agenda.objects.create(label=u'Bar Meeting', kind='meetings', maximal_booking_delay=3)
4970
    virtual_agenda = Agenda.objects.create(label=u'Agenda Virtual', kind='virtual', maximal_booking_delay=3)
4971
    VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda1)
4972
    VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda2)
4973
    desk1 = Desk.objects.create(agenda=meetings_agenda1, label='Desk 1')
4974
    desk2 = Desk.objects.create(agenda=meetings_agenda2, label='Desk 2')
4975

  
4976
    test_1st_weekday = (localtime(now()).weekday() + 2) % 7
4977
    for agenda, desk in zip((meetings_agenda1, meetings_agenda2), (desk1, desk2)):
4978
        meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
4979
        TimePeriod(
4980
            weekday=test_1st_weekday,
4981
            start_time=datetime.time(10, 0),
4982
            end_time=datetime.time(12, 0),
4983
            desk=desk,
4984
        ).save()
4985

  
4986
    virt_meeting_type = virtual_agenda.iter_meetingtypes()[0]
4987
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, virt_meeting_type.slug)
4988
    resp = app.get(api_url)
4989
    assert len(resp.json['data']) == 4
4990
    assert resp.json['data'][2]['disabled'] is False
4991
    assert resp.json['meta'] == {
4992
        'no_bookable_datetimes': False,
4993
        'bookable_datetimes_number_total': 4,
4994
        'bookable_datetimes_number_available': 4,
4995
        'bookable_datetimes_first': '2017-05-22 10:00:00',
4996
    }
4997

  
4998
    def simulate_booking(slot, agenda, desk):
4999
        dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M')
5000
        ev = Event(
5001
            agenda=agenda,
5002
            meeting_type=meeting_type,
5003
            places=1,
5004
            full=False,
5005
            start_datetime=make_aware(dt),
5006
            desk=desk,
5007
        )
5008
        ev.save()
5009
        booking = Booking(event=ev)
5010
        booking.save()
5011

  
5012
    simulate_booking(resp.json['data'][0], meetings_agenda1, desk1)
5013
    resp = app.get(api_url)
5014
    assert len(resp.json['data']) == 4
5015
    assert resp.json['data'][0]['disabled'] is False
5016
    assert resp.json['meta'] == {
5017
        'no_bookable_datetimes': False,
5018
        'bookable_datetimes_number_total': 4,
5019
        'bookable_datetimes_number_available': 4,
5020
        'bookable_datetimes_first': '2017-05-22 10:00:00',
5021
    }
5022

  
5023
    simulate_booking(resp.json['data'][0], meetings_agenda2, desk2)
5024
    resp = app.get(api_url)
5025
    assert len(resp.json['data']) == 4
5026
    assert resp.json['data'][0]['disabled'] is True
5027
    assert resp.json['meta'] == {
5028
        'no_bookable_datetimes': False,
5029
        'bookable_datetimes_number_total': 4,
5030
        'bookable_datetimes_number_available': 3,
5031
        'bookable_datetimes_first': '2017-05-22 10:30:00',
5032
    }
5033

  
5034
    for idx in range(1, 4):
5035
        simulate_booking(resp.json['data'][idx], meetings_agenda1, desk1)
5036
        simulate_booking(resp.json['data'][idx], meetings_agenda2, desk2)
5037
    resp = app.get(api_url)
5038
    assert len(resp.json['data']) == 4
5039
    assert resp.json['meta'] == {
5040
        'no_bookable_datetimes': True,
5041
        'bookable_datetimes_number_total': 4,
5042
        'bookable_datetimes_number_available': 0,
5043
        'bookable_datetimes_first': None,
5044
    }
4820
-