0001-api-add-a-meta-dict-on-datetimes-endpoints-50278.patch
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, some_data): |
|
4823 |
agenda = Agenda.objects.filter(label=u'Foo bar')[0] |
|
4824 |
events = Event.objects.filter(agenda_id=agenda.id).exclude(start_datetime__lt=now()) |
|
4825 |
assert len(events) == 3 |
|
4826 |
api_url = '/api/agenda/%s/datetimes/' % agenda.slug |
|
4827 |
resp = app.get(api_url) |
|
4828 |
assert len(resp.json['data']) == 3 |
|
4829 |
assert resp.json['meta'] == { |
|
4830 |
'no_bookable_datetimes': False, |
|
4831 |
'bookable_datetimes_number_total': 3, |
|
4832 |
'bookable_datetimes_number_available': 3, |
|
4833 |
'bookable_datetimes_first': '2017-05-21 17:00:00', |
|
4834 |
} |
|
4835 | ||
4836 |
def simulate_booking(event, nb_places): |
|
4837 |
for i in range(nb_places): |
|
4838 |
Booking(event=event).save() |
|
4839 | ||
4840 |
simulate_booking(events[0], 10) |
|
4841 |
resp = app.get(api_url) |
|
4842 |
assert len(resp.json['data']) == 3 |
|
4843 |
assert resp.json['meta'] == { |
|
4844 |
'no_bookable_datetimes': False, |
|
4845 |
'bookable_datetimes_number_total': 3, |
|
4846 |
'bookable_datetimes_number_available': 3, |
|
4847 |
'bookable_datetimes_first': '2017-05-21 17:00:00', |
|
4848 |
} |
|
4849 | ||
4850 |
simulate_booking(events[0], 10) |
|
4851 |
resp = app.get(api_url) |
|
4852 |
assert len(resp.json['data']) == 3 |
|
4853 |
assert resp.json['meta'] == { |
|
4854 |
'no_bookable_datetimes': False, |
|
4855 |
'bookable_datetimes_number_total': 3, |
|
4856 |
'bookable_datetimes_number_available': 2, |
|
4857 |
'bookable_datetimes_first': '2017-05-22 17:00:00', |
|
4858 |
} |
|
4859 | ||
4860 |
simulate_booking(events[1], 20) |
|
4861 |
simulate_booking(events[2], 20) |
|
4862 |
resp = app.get(api_url) |
|
4863 |
assert len(resp.json['data']) == 3 |
|
4864 |
assert resp.json['meta'] == { |
|
4865 |
'no_bookable_datetimes': True, |
|
4866 |
'bookable_datetimes_number_total': 3, |
|
4867 |
'bookable_datetimes_number_available': 0, |
|
4868 |
'bookable_datetimes_first': None, |
|
4869 |
} |
|
4870 | ||
4871 | ||
4872 |
def test_datetimes_api_meetings_agenda_meta(app, mock_now): |
|
4873 |
meetings_agenda = Agenda.objects.create( |
|
4874 |
label=u'Foo bar Meeting', kind='meetings', maximal_booking_delay=3 |
|
4875 |
) |
|
4876 |
meeting_type = MeetingType(agenda=meetings_agenda, label='Blah', duration=30) |
|
4877 |
meeting_type.save() |
|
4878 | ||
4879 |
desk1 = Desk.objects.create(agenda=meetings_agenda, label='Desk 1') |
|
4880 |
desk2 = Desk.objects.create(agenda=meetings_agenda, label='Desk 2') |
|
4881 |
test_1st_weekday = (localtime(now()).weekday() + 2) % 7 |
|
4882 |
for desk in desk1, desk2: |
|
4883 |
TimePeriod( |
|
4884 |
weekday=test_1st_weekday, |
|
4885 |
start_time=datetime.time(10, 0), |
|
4886 |
end_time=datetime.time(12, 0), |
|
4887 |
desk=desk, |
|
4888 |
).save() |
|
4889 | ||
4890 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
|
4891 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug) |
|
4892 |
resp = app.get(api_url) |
|
4893 |
assert len(resp.json['data']) == 4 |
|
4894 |
assert resp.json['data'][2]['disabled'] is False |
|
4895 |
assert resp.json['meta'] == { |
|
4896 |
'no_bookable_datetimes': False, |
|
4897 |
'bookable_datetimes_number_total': 4, |
|
4898 |
'bookable_datetimes_number_available': 4, |
|
4899 |
'bookable_datetimes_first': '2017-05-22 10:00:00', |
|
4900 |
} |
|
4901 | ||
4902 |
def simulate_booking(slot, desk): |
|
4903 |
dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M') |
|
4904 |
ev = Event( |
|
4905 |
agenda=meetings_agenda, |
|
4906 |
meeting_type=meeting_type, |
|
4907 |
places=1, |
|
4908 |
full=False, |
|
4909 |
start_datetime=make_aware(dt), |
|
4910 |
desk=desk, |
|
4911 |
) |
|
4912 |
ev.save() |
|
4913 |
booking = Booking(event=ev) |
|
4914 |
booking.save() |
|
4915 | ||
4916 |
simulate_booking(resp.json['data'][0], desk1) |
|
4917 |
resp = app.get(api_url) |
|
4918 |
assert len(resp.json['data']) == 4 |
|
4919 |
assert resp.json['data'][0]['disabled'] is False |
|
4920 |
assert resp.json['meta'] == { |
|
4921 |
'no_bookable_datetimes': False, |
|
4922 |
'bookable_datetimes_number_total': 4, |
|
4923 |
'bookable_datetimes_number_available': 4, |
|
4924 |
'bookable_datetimes_first': '2017-05-22 10:00:00', |
|
4925 |
} |
|
4926 | ||
4927 |
simulate_booking(resp.json['data'][0], desk2) |
|
4928 |
resp = app.get(api_url) |
|
4929 |
assert len(resp.json['data']) == 4 |
|
4930 |
assert resp.json['data'][0]['disabled'] is True |
|
4931 |
assert resp.json['meta'] == { |
|
4932 |
'no_bookable_datetimes': False, |
|
4933 |
'bookable_datetimes_number_total': 4, |
|
4934 |
'bookable_datetimes_number_available': 3, |
|
4935 |
'bookable_datetimes_first': '2017-05-22 10:30:00', |
|
4936 |
} |
|
4937 | ||
4938 |
for idx in range(1, 4): |
|
4939 |
simulate_booking(resp.json['data'][idx], desk1) |
|
4940 |
simulate_booking(resp.json['data'][idx], desk2) |
|
4941 |
resp = app.get(api_url) |
|
4942 |
assert len(resp.json['data']) == 4 |
|
4943 |
assert resp.json['meta'] == { |
|
4944 |
'no_bookable_datetimes': True, |
|
4945 |
'bookable_datetimes_number_total': 4, |
|
4946 |
'bookable_datetimes_number_available': 0, |
|
4947 |
'bookable_datetimes_first': None, |
|
4948 |
} |
|
4949 | ||
4950 | ||
4951 |
def test_datetimes_api_virtual_meetings_agenda_meta(app, virtual_meetings_agenda): |
|
4952 |
meetings_agenda1 = Agenda.objects.create(label=u'Foo Meeting', kind='meetings', maximal_booking_delay=3) |
|
4953 |
meetings_agenda2 = Agenda.objects.create(label=u'Bar Meeting', kind='meetings', maximal_booking_delay=3) |
|
4954 |
virtual_agenda = Agenda.objects.create(label=u'Agenda Virtual', kind='virtual', maximal_booking_delay=3) |
|
4955 |
VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda1) |
|
4956 |
VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda2) |
|
4957 |
desk1 = Desk.objects.create(agenda=meetings_agenda1, label='Desk 1') |
|
4958 |
desk2 = Desk.objects.create(agenda=meetings_agenda2, label='Desk 2') |
|
4959 | ||
4960 |
test_1st_weekday = (localtime(now()).weekday() + 2) % 7 |
|
4961 |
for agenda, desk in zip((meetings_agenda1, meetings_agenda2), (desk1, desk2)): |
|
4962 |
meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30) |
|
4963 |
TimePeriod( |
|
4964 |
weekday=test_1st_weekday, |
|
4965 |
start_time=datetime.time(10, 0), |
|
4966 |
end_time=datetime.time(12, 0), |
|
4967 |
desk=desk, |
|
4968 |
).save() |
|
4969 | ||
4970 |
virt_meeting_type = virtual_agenda.iter_meetingtypes()[0] |
|
4971 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, virt_meeting_type.slug) |
|
4972 |
resp = app.get(api_url) |
|
4973 |
assert len(resp.json['data']) == 4 |
|
4974 |
assert resp.json['data'][2]['disabled'] is False |
|
4975 |
assert resp.json['meta'] == { |
|
4976 |
'no_bookable_datetimes': False, |
|
4977 |
'bookable_datetimes_number_total': 4, |
|
4978 |
'bookable_datetimes_number_available': 4, |
|
4979 |
'bookable_datetimes_first': '2017-05-22 10:00:00', |
|
4980 |
} |
|
4981 | ||
4982 |
def simulate_booking(slot, agenda, desk): |
|
4983 |
dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M') |
|
4984 |
ev = Event( |
|
4985 |
agenda=agenda, |
|
4986 |
meeting_type=meeting_type, |
|
4987 |
places=1, |
|
4988 |
full=False, |
|
4989 |
start_datetime=make_aware(dt), |
|
4990 |
desk=desk, |
|
4991 |
) |
|
4992 |
ev.save() |
|
4993 |
booking = Booking(event=ev) |
|
4994 |
booking.save() |
|
4995 | ||
4996 |
simulate_booking(resp.json['data'][0], meetings_agenda1, desk1) |
|
4997 |
resp = app.get(api_url) |
|
4998 |
assert len(resp.json['data']) == 4 |
|
4999 |
assert resp.json['data'][0]['disabled'] is False |
|
5000 |
assert resp.json['meta'] == { |
|
5001 |
'no_bookable_datetimes': False, |
|
5002 |
'bookable_datetimes_number_total': 4, |
|
5003 |
'bookable_datetimes_number_available': 4, |
|
5004 |
'bookable_datetimes_first': '2017-05-22 10:00:00', |
|
5005 |
} |
|
5006 | ||
5007 |
simulate_booking(resp.json['data'][0], meetings_agenda2, desk2) |
|
5008 |
resp = app.get(api_url) |
|
5009 |
assert len(resp.json['data']) == 4 |
|
5010 |
assert resp.json['data'][0]['disabled'] is True |
|
5011 |
assert resp.json['meta'] == { |
|
5012 |
'no_bookable_datetimes': False, |
|
5013 |
'bookable_datetimes_number_total': 4, |
|
5014 |
'bookable_datetimes_number_available': 3, |
|
5015 |
'bookable_datetimes_first': '2017-05-22 10:30:00', |
|
5016 |
} |
|
5017 | ||
5018 |
for idx in range(1, 4): |
|
5019 |
simulate_booking(resp.json['data'][idx], meetings_agenda1, desk1) |
|
5020 |
simulate_booking(resp.json['data'][idx], meetings_agenda2, desk2) |
|
5021 |
resp = app.get(api_url) |
|
5022 |
assert len(resp.json['data']) == 4 |
|
5023 |
assert resp.json['meta'] == { |
|
5024 |
'no_bookable_datetimes': True, |
|
5025 |
'bookable_datetimes_number_total': 4, |
|
5026 |
'bookable_datetimes_number_available': 0, |
|
5027 |
'bookable_datetimes_first': None, |
|
5028 |
} |
|
4820 |
- |