diff --git a/chrono/api/views.py b/chrono/api/views.py index e34b6b4b..a44df7db 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -801,25 +801,55 @@ class MeetingDatetimes(APIView): # slot from the list if there is still a bookable slot on a desk at the # same time. # The generator also remove slots starting before the current time. - def unique_slots(): - last_slot = None - all_slots = list( - agenda.get_all_slots( - meeting_type, - resources=resources, - unique=True, - start_datetime=start_datetime, - end_datetime=end_datetime, - user_external_id=booked_user_external_id or excluded_user_external_id, + if agenda.kind == 'meetings': + + def unique_slots(): + last_slot = None + all_slots = list( + agenda.get_all_slots( + meeting_type, + resources=resources, + unique=True, + start_datetime=start_datetime, + end_datetime=end_datetime, + user_external_id=booked_user_external_id or excluded_user_external_id, + ) ) - ) - for slot in sorted(all_slots, key=lambda slot: slot[:3]): - if slot.start_datetime < now_datetime: - continue - if last_slot and last_slot[:2] == slot[:2]: - continue - last_slot = slot - yield slot + for slot in sorted(all_slots, key=lambda slot: slot[:3]): + if slot.start_datetime < now_datetime: + continue + if last_slot and last_slot[:2] == slot[:2]: + continue + last_slot = slot + yield slot, None + + elif agenda.kind == 'virtual': + + def unique_slots(): + all_slots = list( + agenda.get_all_slots( + meeting_type, + resources=resources, + unique=False, + start_datetime=start_datetime, + end_datetime=end_datetime, + user_external_id=booked_user_external_id or excluded_user_external_id, + ) + ) + last_slot, slot_agendas = None, set() + for slot in sorted(all_slots, key=lambda slot: slot[:3]): + if slot.start_datetime < now_datetime: + continue + if last_slot is None: + last_slot = slot + elif last_slot[:2] != slot[:2]: + yield last_slot, slot_agendas + last_slot = slot + slot_agendas = set() + if not slot.full: + slot_agendas.add(slot.desk.agenda) + if last_slot: + yield last_slot, slot_agendas generator_of_unique_slots = unique_slots() @@ -839,7 +869,7 @@ class MeetingDatetimes(APIView): bookable_datetimes_number_available = 0 first_bookable_slot = None data = [] - for slot in generator_of_unique_slots: + for slot, slot_agendas in generator_of_unique_slots: if request.GET.get('hide_disabled') and slot.full: continue if minutes and slot.start_datetime.minute not in minutes: @@ -861,6 +891,23 @@ class MeetingDatetimes(APIView): } if booked_user_external_id and slot.booked_for_external_user: slot_data['booked_for_external_user'] = True + if slot_agendas is not None: + slot_data['agendas'] = [ + { + 'id': agenda.id, + 'text': agenda.label, + 'slug': agenda.slug, + 'api': { + 'fillslot_url': request.build_absolute_uri( + reverse( + 'api-fillslot', + kwargs={'agenda_identifier': agenda.slug, 'event_identifier': slot_id}, + ) + ) + }, + } + for agenda in sorted(slot_agendas, key=lambda a: a.label) + ] data.append(slot_data) bookable_datetimes_number_total += 1 diff --git a/tests/api/datetimes/test_meetings.py b/tests/api/datetimes/test_meetings.py index ccf3aa69..822d7ff3 100644 --- a/tests/api/datetimes/test_meetings.py +++ b/tests/api/datetimes/test_meetings.py @@ -14,7 +14,6 @@ from chrono.agendas.models import ( TimePeriod, TimePeriodException, UnavailabilityCalendar, - VirtualMember, ) from chrono.utils.timezone import localtime, make_aware, now @@ -1054,647 +1053,6 @@ def test_datetimes_api_meetings_agenda_start_hour_change(app, meetings_agenda): assert len([x for x in resp.json['data'] if x['disabled']]) == 2 -def test_virtual_agendas_meetings_datetimes_api(app, virtual_meetings_agenda): - real_agenda = virtual_meetings_agenda.real_agendas.first() - meeting_type = real_agenda.meetingtype_set.first() - default_desk = real_agenda.desk_set.first() - # Unkown meeting - app.get('/api/agenda/%s/meetings/xxx/datetimes/' % virtual_meetings_agenda.slug, status=404) - - virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_meetings_agenda.slug, virt_meeting_type.slug) - resp = app.get(api_url) - assert len(resp.json['data']) == 144 - - # cover completely to test limit condition in get_all_slots() - full_coverage = TimePeriodException.objects.create( - desk=default_desk, - start_datetime=make_aware(datetime.datetime(2017, 1, 1, 0, 0)), - end_datetime=make_aware(datetime.datetime(2018, 1, 1, 0, 0)), - ) - resp = app.get(api_url) - assert len(resp.json['data']) == 0 - full_coverage.delete() - - virtual_meetings_agenda.minimal_booking_delay = 7 - virtual_meetings_agenda.maximal_booking_delay = 28 - virtual_meetings_agenda.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 54 - - virtual_meetings_agenda.minimal_booking_delay = 1 - virtual_meetings_agenda.maximal_booking_delay = 56 - virtual_meetings_agenda.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 144 - - resp = app.get(api_url) - dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') - ev = Event( - agenda=real_agenda, - meeting_type=meeting_type, - places=1, - full=False, - start_datetime=make_aware(dt), - desk=default_desk, - ) - ev.save() - booking = Booking(event=ev) - booking.save() - resp2 = app.get(api_url) - assert len(resp2.json['data']) == 144 - assert resp.json['data'][0] == resp2.json['data'][0] - assert resp.json['data'][1] == resp2.json['data'][1] - assert resp.json['data'][2] != resp2.json['data'][2] - assert resp.json['data'][2]['disabled'] is False - assert resp2.json['data'][2]['disabled'] is True - assert resp.json['data'][3] == resp2.json['data'][3] - - # test with a timeperiod overlapping current moment, it should get one - # datetime for the current timeperiod + two from the next week. - if localtime(now()).time().hour == 23: - # skip this part of the test as it would require support for events - # crossing midnight - return - - TimePeriod.objects.filter(desk=default_desk).delete() - start_time = localtime(now()) - datetime.timedelta(minutes=10) - time_period = TimePeriod( - weekday=localtime(now()).weekday(), - start_time=start_time, - end_time=start_time + datetime.timedelta(hours=1), - desk=default_desk, - ) - time_period.save() - virtual_meetings_agenda.minimal_booking_delay = 0 - virtual_meetings_agenda.maximal_booking_delay = 10 - virtual_meetings_agenda.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 3 - - -def test_virtual_agendas_meetings_datetimes_api_with_similar_desk(app): - agenda_foo = Agenda.objects.create( - label='Agenda Foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=4 - ) - MeetingType.objects.create(agenda=agenda_foo, label='Meeting Type', duration=30) - test_1st_weekday = (localtime(now()).weekday() + 1) % 7 - test_2nd_weekday = (localtime(now()).weekday() + 2) % 7 - test_3rd_weekday = (localtime(now()).weekday() + 3) % 7 - - desk_foo = Desk.objects.create(agenda=agenda_foo, label='Desk 1') - TimePeriod.objects.create( - weekday=test_1st_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk_foo, - ) - TimePeriod.objects.create( - weekday=test_2nd_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk_foo, - ) - TimePeriod.objects.create( - weekday=test_3rd_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk_foo, - ) - - agenda_bar = Agenda.objects.create( - label='Agenda Bar', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=4 - ) - meeting_type_bar = MeetingType.objects.create(agenda=agenda_bar, label='Meeting Type', duration=30) - desk_bar = Desk.objects.create(agenda=agenda_bar, label='Desk 1') - TimePeriod.objects.create( - weekday=test_1st_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk_bar, - ) - TimePeriod.objects.create( - weekday=test_2nd_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk_bar, - ) - TimePeriod.objects.create( - weekday=test_3rd_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk_bar, - ) - - virtual_agenda = Agenda.objects.create( - label='Agenda Virtual', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=4 - ) - VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_foo) - VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_bar) - - # 4 slots each day * 3 days - foo_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda_foo.slug, meeting_type_bar.slug) - resp = app.get(foo_api_url) - assert len(resp.json['data']) == 12 - # same thing bar agenda - bar_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda_foo.slug, meeting_type_bar.slug) - resp = app.get(bar_api_url) - assert len(resp.json['data']) == 12 - # same thing on the virtual agenda - virtual_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type_bar.slug) - resp = app.get(virtual_api_url) - assert len(resp.json['data']) == 12 - - # exclude first day - start = (localtime(now()) + datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - end = (localtime(now()) + datetime.timedelta(days=1)).replace( - hour=23, minute=59, second=59, microsecond=0 - ) - TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_foo) - TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_bar) - # exclude second day - start = (localtime(now()) + datetime.timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) - end = (localtime(now()) + datetime.timedelta(days=2)).replace( - hour=23, minute=59, second=59, microsecond=0 - ) - TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_foo) - TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_bar) - - # 4 slots each day * 1 day - resp = app.get(foo_api_url) - assert len(resp.json['data']) == 4 - # same thing bar agenda - resp = app.get(bar_api_url) - assert len(resp.json['data']) == 4 - # same thing on the virtual agenda - resp = app.get(virtual_api_url) - assert len(resp.json['data']) == 4 - - -def test_virtual_agendas_meetings_datetimes_delays_api(app, mock_now): - foo_agenda = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=7) - MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) - foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') - TimePeriod.objects.create( - weekday=0, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_1, - ) - TimePeriod.objects.create( - weekday=1, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_1, - ) - - bar_agenda = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=7) - MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) - bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') - TimePeriod.objects.create( - weekday=2, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=bar_desk_1, - ) - TimePeriod.objects.create( - weekday=3, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=bar_desk_1, - ) - - virt_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') - - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) - virt_meeting_type = virt_agenda.iter_meetingtypes()[0] - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) - resp = app.get(api_url) - # 8 slots for m each agenda - assert len(resp.json['data']) == 16 - - # restrict foo's minimal_booking_delay : only bar's slots are left - foo_agenda.minimal_booking_delay = 6 - foo_agenda.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 8 - - # restrict bar's maximal_booking_delay : only half of bar's slots are left - bar_agenda.maximal_booking_delay = 5 - bar_agenda.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 4 - - # put back very slots from foo - foo_agenda.minimal_booking_delay = 1 - foo_agenda.maximal_booking_delay = 7 - foo_agenda.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 12 - - -def test_virtual_agendas_meetings_datetimes_exluded_periods(app, mock_now): - foo_agenda = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=7) - MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) - foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') - TimePeriod.objects.create( - weekday=0, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_1, - ) - TimePeriod.objects.create( - weekday=1, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_1, - ) - virt_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) - - api_url = '/api/agenda/%s/meetings/meeting-type/datetimes/' % (virt_agenda.slug) - resp = app.get(api_url) - # 8 slots - data = resp.json['data'] - assert len(data) == 8 - assert data[0]['datetime'] == '2017-05-22 10:00:00' - assert data[1]['datetime'] == '2017-05-22 10:30:00' - assert data[2]['datetime'] == '2017-05-22 11:00:00' - - # exclude one hour the first day - tp1 = TimePeriod.objects.create( - weekday=0, start_time=datetime.time(11, 0), end_time=datetime.time(12, 0), agenda=virt_agenda - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 6 - assert data[0]['datetime'] == '2017-05-22 10:00:00' - assert data[1]['datetime'] == '2017-05-22 10:30:00' - # no more slots the 22 thanks to the exclusion period - assert data[2]['datetime'] == '2017-05-23 10:00:00' - - # exclude the second day - tp2 = TimePeriod.objects.create( - weekday=1, start_time=datetime.time(9, 0), end_time=datetime.time(18, 0), agenda=virt_agenda - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 2 - assert data[0]['datetime'] == '2017-05-22 10:00:00' - assert data[1]['datetime'] == '2017-05-22 10:30:00' - - # go back to no restriction - tp1.delete() - tp2.delete() - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 8 - - # excluded period applies to every desk - foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2') - TimePeriod.objects.create( - weekday=3, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_2, - ) - TimePeriod.objects.create( - weekday=4, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_2, - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 16 - - # exclude one hour the first day - tp1 = TimePeriod.objects.create( - weekday=0, start_time=datetime.time(11, 0), end_time=datetime.time(12, 0), agenda=virt_agenda - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 14 - - # exclude one hour the last day - tp2 = TimePeriod.objects.create( - weekday=4, start_time=datetime.time(11, 0), end_time=datetime.time(12, 0), agenda=virt_agenda - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 12 - - # go back to no restriction - tp1.delete() - tp2.delete() - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 16 - - # add a second real agenda - bar_agenda = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=7) - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) - MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) - bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') - bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') - TimePeriod.objects.create( - weekday=0, - start_time=datetime.time(14, 0), - end_time=datetime.time(16, 0), - desk=bar_desk_1, - ) - TimePeriod.objects.create( - weekday=1, - start_time=datetime.time(14, 0), - end_time=datetime.time(16, 0), - desk=bar_desk_1, - ) - TimePeriod.objects.create( - weekday=2, - start_time=datetime.time(14, 0), - end_time=datetime.time(16, 0), - desk=bar_desk_2, - ) - TimePeriod.objects.create( - weekday=3, - start_time=datetime.time(14, 0), - end_time=datetime.time(16, 0), - desk=bar_desk_2, - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 32 - - # exclude the first day, 11 to 15 : 4 slots - tp1 = TimePeriod.objects.create( - weekday=0, start_time=datetime.time(11, 0), end_time=datetime.time(15, 0), agenda=virt_agenda - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 28 - - -def test_virtual_agendas_meetings_exception(app, user, virtual_meetings_agenda): - app.authorization = ('Basic', ('john.doe', 'password')) - real_agenda = virtual_meetings_agenda.real_agendas.first() - desk = real_agenda.desk_set.first() - virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] - datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % ( - virtual_meetings_agenda.slug, - virt_meeting_type.slug, - ) - resp = app.get(datetimes_url) - - # test exception at the lowest limit - excp1 = TimePeriodException.objects.create( - desk=desk, - start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), - end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), - ) - resp2 = app.get(datetimes_url) - assert len(resp.json['data']) == len(resp2.json['data']) + 4 - - # test exception at the highest limit - excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 22, 11, 0)) - excp1.save() - resp2 = app.get(datetimes_url) - assert len(resp.json['data']) == len(resp2.json['data']) + 2 - - # add an exception with an end datetime less than excp1 end datetime - # and make sure that excp1 end datetime preveil - excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 23, 11, 0)) - excp1.save() - - TimePeriodException.objects.create( - desk=excp1.desk, - start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)), - end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), - ) - - resp2 = app.get(datetimes_url) - assert len(resp.json['data']) == len(resp2.json['data']) + 6 - - # with a second desk - desk2 = Desk.objects.create(label='Desk 2', agenda=real_agenda) - time_period = desk.timeperiod_set.first() - TimePeriod.objects.create( - desk=desk2, - start_time=time_period.start_time, - end_time=time_period.end_time, - weekday=time_period.weekday, - ) - resp3 = app.get(datetimes_url) - assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed - - -def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, mock_now): - foo_agenda = Agenda.objects.create( - label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 - ) - foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) - foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') - - test_1st_weekday = (localtime(now()).weekday() + 2) % 7 - test_2nd_weekday = (localtime(now()).weekday() + 3) % 7 - test_3rd_weekday = (localtime(now()).weekday() + 4) % 7 - test_4th_weekday = (localtime(now()).weekday() + 5) % 7 - - def create_time_perdiods(desk, end=12): - TimePeriod.objects.create( - weekday=test_1st_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(end, 0), - desk=desk, - ) - TimePeriod.objects.create( - weekday=test_2nd_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(end, 0), - desk=desk, - ) - - create_time_perdiods(foo_desk_1) - virt_agenda = Agenda.objects.create( - label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=6 - ) - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) - virt_meeting_type = virt_agenda.iter_meetingtypes()[0] - - # We are saturday and we can book for next monday and tuesday, 4 slots available each day - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) - resp = app.get(api_url) - assert len(resp.json['data']) == 8 - assert resp.json['data'][0]['id'] == 'meeting-type:2017-05-22-1000' - - virt_agenda.maximal_booking_delay = 10 # another monday comes in - virt_agenda.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 12 - - # Back to next monday and tuesday restriction - virt_agenda.maximal_booking_delay = 6 - virt_agenda.save() - - # Add another agenda - bar_agenda = Agenda.objects.create( - label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 - ) - bar_meeting_type = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) - bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') - create_time_perdiods(bar_desk_1, end=13) # bar_agenda has two more slots each day - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) - with CaptureQueriesContext(connection) as ctx: - resp = app.get(api_url) - assert len(resp.json['data']) == 12 - assert len(ctx.captured_queries) == 10 - - # simulate booking - dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') - ev = Event.objects.create( - agenda=foo_agenda, - meeting_type=foo_meeting_type, - places=1, - full=False, - start_datetime=make_aware(dt), - desk=foo_desk_1, - ) - booking1 = Booking.objects.create(event=ev) - - resp = app.get(api_url) - assert len(resp.json['data']) == 12 - # No disabled slot, because the booked slot is still available in second agenda - for slot in resp.json['data']: - assert slot['disabled'] is False - - ev = Event.objects.create( - agenda=bar_agenda, - meeting_type=bar_meeting_type, - places=1, - full=False, - start_datetime=make_aware(dt), - desk=bar_desk_1, - ) - booking2 = Booking.objects.create(event=ev) - - resp = app.get(api_url) - assert len(resp.json['data']) == 12 - # now one slot is disabled - for i, slot in enumerate(resp.json['data']): - if i == 2: - assert slot['disabled'] - else: - assert slot['disabled'] is False - - # Cancel booking, every slot available - booking1.cancel() - booking2.cancel() - resp = app.get(api_url) - assert len(resp.json['data']) == 12 - for slot in resp.json['data']: - assert slot['disabled'] is False - - # Add new desk on foo_agenda, open on wednesday - foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2') - TimePeriod.objects.create( - weekday=test_3rd_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_2, - ) - resp = app.get(api_url) - assert len(resp.json['data']) == 16 - - # Add new desk on bar_agenda, open on thursday - bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') - TimePeriod.objects.create( - weekday=test_4th_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=bar_desk_2, - ) - resp = app.get(api_url) - assert len(resp.json['data']) == 20 - - -@pytest.mark.freeze_time('2021-02-25') -def test_virtual_agendas_meetings_datetimes_exclude_slots(app): - tomorrow = now() + datetime.timedelta(days=1) - agenda = Agenda.objects.create( - label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10 - ) - desk = Desk.objects.create(agenda=agenda, slug='desk') - meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar') - TimePeriod.objects.create( - weekday=tomorrow.date().weekday(), - start_time=datetime.time(9, 0), - end_time=datetime.time(17, 00), - desk=desk, - ) - agenda2 = agenda.duplicate() - virt_agenda = Agenda.objects.create( - label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=10 - ) - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda) - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2) - - event = Event.objects.create( - agenda=agenda, - meeting_type=meeting_type, - places=1, - start_datetime=localtime(tomorrow).replace(hour=9, minute=0), - desk=desk, - ) - Booking.objects.create(event=event, user_external_id='42') - event2 = Event.objects.create( - agenda=agenda, - meeting_type=meeting_type, - places=1, - start_datetime=localtime(tomorrow).replace(hour=10, minute=0), - desk=desk, - ) - cancelled = Booking.objects.create(event=event2, user_external_id='35') - cancelled.cancel() - - resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug)) - assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' - assert resp.json['data'][0]['disabled'] is False - assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' - assert resp.json['data'][2]['disabled'] is False - - resp = app.get( - '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug), - params={'exclude_user_external_id': '35'}, - ) - assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' - assert resp.json['data'][0]['disabled'] is False - assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' - assert resp.json['data'][2]['disabled'] is False - - with CaptureQueriesContext(connection) as ctx: - resp = app.get( - '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug), - params={'exclude_user_external_id': '42'}, - ) - assert len(ctx.captured_queries) == 11 - assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' - assert resp.json['data'][0]['disabled'] is True - assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' - assert resp.json['data'][2]['disabled'] is False - - virt_agenda.minimal_booking_delay = None - virt_agenda.maximal_booking_delay = None - virt_agenda.save() - resp = app.get( - '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug), - params={'exclude_user_external_id': '42'}, - ) - assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' - assert resp.json['data'][0]['disabled'] is True - assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' - assert resp.json['data'][2]['disabled'] is False - - @pytest.mark.freeze_time('2017-05-21') def test_unavailabilitycalendar_meetings_datetimes(app, user): meetings_agenda = Agenda.objects.create(label='Meeting', kind='meetings', maximal_booking_delay=7) @@ -1743,117 +1101,6 @@ def test_unavailabilitycalendar_meetings_datetimes(app, user): assert len(resp.json['data']) == len(resp3.json['data']) + 4 -def test_unavailabilitycalendar_on_virtual_datetimes(app, user, mock_now): - foo_agenda = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=7) - MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) - foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') - TimePeriod.objects.create( - weekday=0, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_1, - ) - TimePeriod.objects.create( - weekday=1, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=foo_desk_1, - ) - virt_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) - - api_url = '/api/agenda/%s/meetings/meeting-type/datetimes/' % (virt_agenda.slug) - resp = app.get(api_url) - # 8 slots - data = resp.json['data'] - assert len(data) == 8 - assert data[0]['datetime'] == '2017-05-22 10:00:00' - assert data[1]['datetime'] == '2017-05-22 10:30:00' - assert data[2]['datetime'] == '2017-05-22 11:00:00' - - # exclude one hour the first day through an unvalailability calendar on the foo agenda - unavailability_calendar = UnavailabilityCalendar.objects.create(label='foo holydays') - TimePeriodException.objects.create( - unavailability_calendar=unavailability_calendar, - start_datetime=make_aware(datetime.datetime(2017, 5, 22, 11, 0)), - end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), - ) - unavailability_calendar.desks.add(foo_desk_1) - - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 6 - assert data[0]['datetime'] == '2017-05-22 10:00:00' - assert data[1]['datetime'] == '2017-05-22 10:30:00' - # no more slots the 22 thanks to the unavailability calendar - assert data[2]['datetime'] == '2017-05-23 10:00:00' - - # exclude the second day - TimePeriodException.objects.create( - unavailability_calendar=unavailability_calendar, - start_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), - end_datetime=make_aware(datetime.datetime(2017, 5, 23, 18, 0)), - ) - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 2 - assert data[0]['datetime'] == '2017-05-22 10:00:00' - assert data[1]['datetime'] == '2017-05-22 10:30:00' - - # add a second real agenda - bar_agenda = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=7) - VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) - MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) - bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') - bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') - TimePeriod.objects.create( - weekday=0, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=bar_desk_1, - ) - TimePeriod.objects.create( - weekday=1, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=bar_desk_1, - ) - TimePeriod.objects.create( - weekday=0, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=bar_desk_2, - ) - TimePeriod.objects.create( - weekday=1, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=bar_desk_2, - ) - - # bar_agenda has the same time periods than foo_agenda, but no unavailability calendar - # so we are back at the start : 8 slots - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 8 - - # exclude one hour the second day through another unvalailability calendar on the bar agenda - unavailability_calendar = UnavailabilityCalendar.objects.create(label='bar holydays') - TimePeriodException.objects.create( - unavailability_calendar=unavailability_calendar, - start_datetime=make_aware(datetime.datetime(2017, 5, 23, 11, 0)), - end_datetime=make_aware(datetime.datetime(2017, 5, 23, 12, 0)), - ) - unavailability_calendar.desks.add(bar_desk_1, bar_desk_2) - - # 2 slots are gone - resp = app.get(api_url) - data = resp.json['data'] - assert len(data) == 6 - assert data[0]['datetime'] == '2017-05-22 10:00:00' - assert data[-1]['datetime'] == '2017-05-23 10:30:00' - - @pytest.mark.parametrize('test_datetime', (None, '2020-11-11 23:50', '2020-12-06 10:14')) def test_datetimes_maximal_booking_delay(app, user, freezer, test_datetime): if test_datetime: @@ -1886,159 +1133,6 @@ def test_datetimes_maximal_booking_delay(app, user, freezer, test_datetime): assert len(data) == 4 -def test_meetings_and_virtual_datetimes_date_filter(app): - agenda_foo = Agenda.objects.create( - label='Agenda Foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=7 - ) - meeting_type = MeetingType.objects.create(agenda=agenda_foo, label='Meeting Type', duration=30) - desk_foo = Desk.objects.create(agenda=agenda_foo, label='Desk 1') - weekday1 = ((localtime(now())).weekday() + 1) % 7 - weekday2 = ((localtime(now())).weekday() + 2) % 7 - weekday3 = ((localtime(now())).weekday() + 3) % 7 - weekday4 = ((localtime(now())).weekday() + 4) % 7 - weekday5 = ((localtime(now())).weekday() + 5) % 7 - weekday6 = ((localtime(now())).weekday() + 6) % 7 - for weekday in (weekday1, weekday2, weekday3, weekday4, weekday5, weekday6): - TimePeriod.objects.create( - weekday=weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk_foo, - ) - - virtual_agenda = Agenda.objects.create( - label='Agenda Virtual', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=7 - ) - VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_foo) - - # 4 slots each day * 6 days - foo_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda_foo.slug, meeting_type.slug) - resp = app.get(foo_api_url) - assert len(resp.json['data']) == 24 - virtual_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) - resp = app.get(virtual_api_url) - assert len(resp.json['data']) == 24 - - for value in ['foo', '2017-05-42']: - params = {'date_start': value} - resp = app.get(foo_api_url, params=params, status=400) - assert resp.json['err'] == 1 - assert resp.json['err_desc'] == 'invalid payload' - assert resp.json['errors']['date_start'] == [ - 'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].' - ] - - for value in ['foo', '2017-05-42']: - params = {'date_end': value} - resp = app.get(foo_api_url, params=params, status=400) - assert resp.json['err'] == 1 - assert resp.json['err_desc'] == 'invalid payload' - assert resp.json['errors']['date_end'] == [ - 'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].' - ] - - # exclude weekday1 through date_start, 4 slots each day * 5 days - params = {'date_start': localtime(now() + datetime.timedelta(days=2)).date().isoformat()} - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 20 - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 20 - - params = { - 'date_start': localtime(now() + datetime.timedelta(days=2)) - .replace(hour=12, minute=0, second=0, microsecond=0) - .isoformat() - } - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 16 - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 16 - - params = { - 'date_start': localtime(now() + datetime.timedelta(days=2)) - .replace(hour=11, minute=0, second=0, microsecond=0) - .isoformat() - } - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 18 - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 18 - - # minimal_booking_delay (which exclude weekday1 and wekkday2 ) takes precedence - # 4 slots each day * 4 days - agenda_foo.minimal_booking_delay = 3 - agenda_foo.save() - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 16 - # also on virtual agenda - virtual_agenda.minimal_booking_delay = 3 - virtual_agenda.save() - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 16 - - # reset - agenda_foo.minimal_booking_delay = 1 - virtual_agenda.minimal_booking_delay = 1 - agenda_foo.save() - virtual_agenda.save() - - # exclude weekday6 through date_end, 4 slots each day * 5 days - params = {'date_end': localtime(now() + datetime.timedelta(days=6)).date().isoformat()} - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 20 - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 20 - - params = { - 'date_end': localtime(now() + datetime.timedelta(days=6)) - .replace(hour=11, minute=0, second=0, microsecond=0) - .isoformat() - } - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 22 - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 22 - - # maximal_booking_delay (which exclude weekday5 and weekday6 ) takes precedence - # 4 slots each day * 4 days - agenda_foo.maximal_booking_delay = 5 - agenda_foo.save() - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 16 - # also on virtual agenda - virtual_agenda.maximal_booking_delay = 5 - virtual_agenda.save() - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 16 - - # now check with exceptions in DB - TimePeriodException.objects.create( - desk=desk_foo, - start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), - end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), - ) - - params = { - 'date_start': localtime(now() + datetime.timedelta(days=2)) - .replace(hour=12, minute=0, second=0, microsecond=0) - .isoformat() - } - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 8 - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 8 - - params = { - 'date_end': localtime(now() + datetime.timedelta(days=2)) - .replace(hour=11, minute=0, second=0, microsecond=0) - .isoformat() - } - resp = app.get(foo_api_url, params=params) - assert len(resp.json['data']) == 6 - resp = app.get(virtual_api_url, params=params) - assert len(resp.json['data']) == 6 - - def test_datetimes_api_meetings_agenda_meta(app, freezer): # 2017-05-20 -> saturday freezer.move_to(make_aware(datetime.datetime(year=2017, month=5, day=20, hour=1, minute=12))) @@ -2127,97 +1221,6 @@ def test_datetimes_api_meetings_agenda_meta(app, freezer): } -def test_datetimes_api_virtual_meetings_agenda_meta(app, freezer): - # 2017-05-20 -> saturday - freezer.move_to(make_aware(datetime.datetime(year=2017, month=5, day=20, hour=1, minute=12))) - meetings_agenda1 = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=3) - meetings_agenda2 = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=3) - virtual_agenda = Agenda.objects.create(label='Agenda Virtual', kind='virtual', maximal_booking_delay=3) - VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda1) - VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda2) - desk1 = Desk.objects.create(agenda=meetings_agenda1, label='Desk 1') - desk2 = Desk.objects.create(agenda=meetings_agenda2, label='Desk 2') - - test_1st_weekday = (localtime(now()).weekday() + 2) % 7 - for agenda, desk in zip((meetings_agenda1, meetings_agenda2), (desk1, desk2)): - meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30) - TimePeriod( - weekday=test_1st_weekday, - start_time=datetime.time(10, 0), - end_time=datetime.time(12, 0), - desk=desk, - ).save() - - virt_meeting_type = virtual_agenda.iter_meetingtypes()[0] - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, virt_meeting_type.slug) - resp = app.get(api_url) - assert len(resp.json['data']) == 4 - assert resp.json['data'][2]['disabled'] is False - assert resp.json['meta'] == { - 'no_bookable_datetimes': False, - 'bookable_datetimes_number_total': 4, - 'bookable_datetimes_number_available': 4, - 'first_bookable_slot': resp.json['data'][0], - } - - def simulate_booking(slot, agenda, desk): - dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M') - ev = Event( - agenda=agenda, - meeting_type=meeting_type, - places=1, - full=False, - start_datetime=make_aware(dt), - desk=desk, - ) - ev.save() - booking = Booking(event=ev, user_external_id='42') - booking.save() - - simulate_booking(resp.json['data'][0], meetings_agenda1, desk1) - resp = app.get(api_url) - assert len(resp.json['data']) == 4 - assert resp.json['data'][0]['disabled'] is False - assert resp.json['meta'] == { - 'no_bookable_datetimes': False, - 'bookable_datetimes_number_total': 4, - 'bookable_datetimes_number_available': 4, - 'first_bookable_slot': resp.json['data'][0], - } - resp = app.get(api_url, params={'exclude_user_external_id': '42'}) - assert len(resp.json['data']) == 4 - assert resp.json['data'][0]['disabled'] is True - assert resp.json['meta'] == { - 'no_bookable_datetimes': False, - 'bookable_datetimes_number_total': 4, - 'bookable_datetimes_number_available': 3, - 'first_bookable_slot': resp.json['data'][1], - } - - simulate_booking(resp.json['data'][0], meetings_agenda2, desk2) - resp = app.get(api_url) - assert len(resp.json['data']) == 4 - assert resp.json['data'][0]['disabled'] is True - assert resp.json['meta'] == { - 'no_bookable_datetimes': False, - 'bookable_datetimes_number_total': 4, - 'bookable_datetimes_number_available': 3, - 'first_bookable_slot': resp.json['data'][1], - } - - for idx in range(1, 4): - simulate_booking(resp.json['data'][idx], meetings_agenda1, desk1) - simulate_booking(resp.json['data'][idx], meetings_agenda2, desk2) - resp = app.get(api_url) - assert len(resp.json['data']) == 4 - assert resp.json['meta'] == { - 'no_bookable_datetimes': True, - 'bookable_datetimes_number_total': 4, - 'bookable_datetimes_number_available': 0, - 'first_bookable_slot': None, - } - - def test_date_filter_overlapping_events(app): # Create a meeting of 30 minutes from 09:45 to 10:15 and look for available # 5 minutes meeting between 10:00 and 10:30, there should be only 3 if the @@ -2259,55 +1262,6 @@ def test_date_filter_overlapping_events(app): assert len(resp.json['data']) == 3 -def test_virtual_agendas_time_change(app, freezer): - agenda = Agenda.objects.create( - label='Foo bar Meeting', kind='meetings', minimal_booking_delay=10, maximal_booking_delay=20 - ) - meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=15) - desk, _ = Desk.objects.get_or_create(agenda=agenda, label='Desk 1') - TimePeriodException.objects.create( - desk=desk, - start_datetime=make_aware(datetime.datetime(2022, 4, 4, 8, 0)), - end_datetime=make_aware(datetime.datetime(2022, 4, 4, 12, 0)), - ) - TimePeriod.objects.create( - weekday=0, - start_time=datetime.time(8, 30), - end_time=datetime.time(12, 0), - desk=desk, - ) - dt = datetime.datetime.strptime('2022-03-28-08:30', '%Y-%m-%d-%H:%M') - ev = Event.objects.create( - agenda=agenda, - meeting_type=meeting_type, - places=1, - start_datetime=make_aware(dt), - desk=desk, - ) - Booking.objects.create(event=ev) - - freezer.move_to(make_aware(datetime.datetime(2022, 3, 8, 7, 0))) - - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug) - resp = app.get(api_url) - for slot in resp.json['data']: - if slot['datetime'] == '2022-03-28 08:30:00': - assert False, 'slot should not appear due to maximal_booking_delay' - - # now got through virtual agenda - virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual') - virtual_agenda.real_agendas.add(agenda) - assert virtual_agenda.minimal_booking_delay is None - assert virtual_agenda.maximal_booking_delay is None - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) - resp = app.get(api_url) - for slot in resp.json['data']: - if slot['datetime'] == '2022-03-28 08:30:00': - assert ( - False - ), 'slot should not appear due to maximal_booking_delay of the real agenda (and no maximal_booking_delay) is defined on the real agenda' - - @pytest.mark.freeze_time('2022-01-20 14:00') # Thursday def test_datetimes_api_meetings_agenda_weekday_indexes(app): agenda = Agenda.objects.create( @@ -2376,84 +1330,6 @@ def test_datetimes_api_meetings_agenda_weekday_indexes(app): assert [x['datetime'] for x in resp.json['data']] == ['2022-01-31 11:00:00', '2022-01-31 11:30:00'] -@pytest.mark.freeze_time('2022-01-20 14:00') # Thursday -def test_datetimes_api_meetings_virtual_agenda_weekday_indexes(app): - agenda = Agenda.objects.create( - label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 - ) - desk = Desk.objects.create(agenda=agenda, label='desk') - meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) - virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual') - virtual_agenda.real_agendas.add(agenda) - - TimePeriod.objects.create( - weekday=0, - weekday_indexes=[1, 2], - start_time=datetime.time(11, 0), - end_time=datetime.time(12, 30), - desk=desk, - ) - - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) - resp = app.get(api_url) - assert len(resp.json['data']) == 12 - assert [x['datetime'] for x in resp.json['data']] == [ - '2022-02-07 11:00:00', - '2022-02-07 11:30:00', - '2022-02-07 12:00:00', - '2022-02-14 11:00:00', - '2022-02-14 11:30:00', - '2022-02-14 12:00:00', - '2022-03-07 11:00:00', - '2022-03-07 11:30:00', - '2022-03-07 12:00:00', - '2022-03-14 11:00:00', - '2022-03-14 11:30:00', - '2022-03-14 12:00:00', - ] - - # add exclusion period on virtual agenda - exclusion_period = TimePeriod.objects.create( - weekday=0, start_time=datetime.time(11, 30), end_time=datetime.time(12, 30), agenda=virtual_agenda - ) - resp = app.get(api_url) - assert len(resp.json['data']) == 4 - assert [x['datetime'] for x in resp.json['data']] == [ - '2022-02-07 11:00:00', - '2022-02-14 11:00:00', - '2022-03-07 11:00:00', - '2022-03-14 11:00:00', - ] - - exclusion_period.start_time = datetime.time(10, 30) - exclusion_period.end_time = datetime.time(11, 30) - exclusion_period.save() - resp = app.get(api_url) - assert len(resp.json['data']) == 8 - assert [x['datetime'] for x in resp.json['data']] == [ - '2022-02-07 11:30:00', - '2022-02-07 12:00:00', - '2022-02-14 11:30:00', - '2022-02-14 12:00:00', - '2022-03-07 11:30:00', - '2022-03-07 12:00:00', - '2022-03-14 11:30:00', - '2022-03-14 12:00:00', - ] - - # add second exclusion period on virtual agenda - TimePeriod.objects.create( - weekday=0, start_time=datetime.time(12, 00), end_time=datetime.time(12, 30), agenda=virtual_agenda - ) - resp = app.get(api_url) - assert [x['datetime'] for x in resp.json['data']] == [ - '2022-02-07 11:30:00', - '2022-02-14 11:30:00', - '2022-03-07 11:30:00', - '2022-03-14 11:30:00', - ] - - @pytest.mark.freeze_time('2022-10-24 10:00') def test_datetimes_api_meetings_agenda_date_time_period(app): agenda = Agenda.objects.create( @@ -2504,52 +1380,6 @@ def test_datetimes_api_meetings_agenda_date_time_period(app): ] -@pytest.mark.freeze_time('2022-10-24 10:00') -def test_datetimes_api_meetings_virtual_agenda_date_time_period(app): - agenda = Agenda.objects.create( - label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=30 - ) - desk = Desk.objects.create(agenda=agenda, label='desk') - meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) - virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual') - virtual_agenda.real_agendas.add(agenda) - - TimePeriod.objects.create( - date=datetime.date(2022, 10, 24), - start_time=datetime.time(12, 0), - end_time=datetime.time(14, 0), - desk=desk, - ) - - api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) - resp = app.get(api_url) - assert [x['datetime'] for x in resp.json['data']] == [ - '2022-10-24 12:00:00', - '2022-10-24 12:30:00', - '2022-10-24 13:00:00', - '2022-10-24 13:30:00', - ] - - # add exclusion period on virtual agenda - TimePeriod.objects.create( - weekday=0, start_time=datetime.time(12, 00), end_time=datetime.time(13, 00), agenda=virtual_agenda - ) - resp = app.get(api_url) - assert [x['datetime'] for x in resp.json['data']] == [ - '2022-10-24 13:00:00', - '2022-10-24 13:30:00', - ] - - # add second exclusion period on virtual agenda - TimePeriod.objects.create( - weekday=0, start_time=datetime.time(13, 30), end_time=datetime.time(14, 00), agenda=virtual_agenda - ) - resp = app.get(api_url) - assert [x['datetime'] for x in resp.json['data']] == [ - '2022-10-24 13:00:00', - ] - - @pytest.mark.freeze_time('2023-03-01 01:00') def test_datetimes_api_meetings_agenda_date_time_period_dst_change(app): agenda = Agenda.objects.create( diff --git a/tests/api/datetimes/test_virtual.py b/tests/api/datetimes/test_virtual.py new file mode 100644 index 00000000..ce8af801 --- /dev/null +++ b/tests/api/datetimes/test_virtual.py @@ -0,0 +1,1251 @@ +import datetime + +import pytest +from django.db import connection +from django.test.utils import CaptureQueriesContext + +from chrono.agendas.models import ( + Agenda, + Booking, + Desk, + Event, + MeetingType, + TimePeriod, + TimePeriodException, + UnavailabilityCalendar, + VirtualMember, +) +from chrono.utils.timezone import localtime, make_aware, now + +from ...utils import build_virtual_agenda + +pytestmark = pytest.mark.django_db + + +def test_virtual_agendas_meetings_datetimes_api(app, virtual_meetings_agenda): + real_agenda = virtual_meetings_agenda.real_agendas.first() + meeting_type = real_agenda.meetingtype_set.first() + default_desk = real_agenda.desk_set.first() + # Unkown meeting + app.get('/api/agenda/%s/meetings/xxx/datetimes/' % virtual_meetings_agenda.slug, status=404) + + virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_meetings_agenda.slug, virt_meeting_type.slug) + resp = app.get(api_url) + assert len(resp.json['data']) == 144 + + # cover completely to test limit condition in get_all_slots() + full_coverage = TimePeriodException.objects.create( + desk=default_desk, + start_datetime=make_aware(datetime.datetime(2017, 1, 1, 0, 0)), + end_datetime=make_aware(datetime.datetime(2018, 1, 1, 0, 0)), + ) + resp = app.get(api_url) + assert len(resp.json['data']) == 0 + full_coverage.delete() + + virtual_meetings_agenda.minimal_booking_delay = 7 + virtual_meetings_agenda.maximal_booking_delay = 28 + virtual_meetings_agenda.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 54 + + virtual_meetings_agenda.minimal_booking_delay = 1 + virtual_meetings_agenda.maximal_booking_delay = 56 + virtual_meetings_agenda.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 144 + + resp = app.get(api_url) + dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') + ev = Event( + agenda=real_agenda, + meeting_type=meeting_type, + places=1, + full=False, + start_datetime=make_aware(dt), + desk=default_desk, + ) + ev.save() + booking = Booking(event=ev) + booking.save() + resp2 = app.get(api_url) + assert len(resp2.json['data']) == 144 + assert resp.json['data'][0] == resp2.json['data'][0] + assert resp.json['data'][1] == resp2.json['data'][1] + assert resp.json['data'][2] != resp2.json['data'][2] + assert resp.json['data'][2]['disabled'] is False + assert resp2.json['data'][2]['disabled'] is True + assert resp.json['data'][3] == resp2.json['data'][3] + + # test with a timeperiod overlapping current moment, it should get one + # datetime for the current timeperiod + two from the next week. + if localtime(now()).time().hour == 23: + # skip this part of the test as it would require support for events + # crossing midnight + return + + TimePeriod.objects.filter(desk=default_desk).delete() + start_time = localtime(now()) - datetime.timedelta(minutes=10) + time_period = TimePeriod( + weekday=localtime(now()).weekday(), + start_time=start_time, + end_time=start_time + datetime.timedelta(hours=1), + desk=default_desk, + ) + time_period.save() + virtual_meetings_agenda.minimal_booking_delay = 0 + virtual_meetings_agenda.maximal_booking_delay = 10 + virtual_meetings_agenda.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 3 + + +def test_virtual_agendas_meetings_datetimes_api_with_similar_desk(app): + agenda_foo = Agenda.objects.create( + label='Agenda Foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=4 + ) + MeetingType.objects.create(agenda=agenda_foo, label='Meeting Type', duration=30) + test_1st_weekday = (localtime(now()).weekday() + 1) % 7 + test_2nd_weekday = (localtime(now()).weekday() + 2) % 7 + test_3rd_weekday = (localtime(now()).weekday() + 3) % 7 + + desk_foo = Desk.objects.create(agenda=agenda_foo, label='Desk 1') + TimePeriod.objects.create( + weekday=test_1st_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk_foo, + ) + TimePeriod.objects.create( + weekday=test_2nd_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk_foo, + ) + TimePeriod.objects.create( + weekday=test_3rd_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk_foo, + ) + + agenda_bar = Agenda.objects.create( + label='Agenda Bar', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=4 + ) + meeting_type_bar = MeetingType.objects.create(agenda=agenda_bar, label='Meeting Type', duration=30) + desk_bar = Desk.objects.create(agenda=agenda_bar, label='Desk 1') + TimePeriod.objects.create( + weekday=test_1st_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk_bar, + ) + TimePeriod.objects.create( + weekday=test_2nd_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk_bar, + ) + TimePeriod.objects.create( + weekday=test_3rd_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk_bar, + ) + + virtual_agenda = Agenda.objects.create( + label='Agenda Virtual', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=4 + ) + VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_foo) + VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_bar) + + # 4 slots each day * 3 days + foo_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda_foo.slug, meeting_type_bar.slug) + resp = app.get(foo_api_url) + assert len(resp.json['data']) == 12 + # same thing bar agenda + bar_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda_foo.slug, meeting_type_bar.slug) + resp = app.get(bar_api_url) + assert len(resp.json['data']) == 12 + # same thing on the virtual agenda + virtual_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type_bar.slug) + resp = app.get(virtual_api_url) + assert len(resp.json['data']) == 12 + + # exclude first day + start = (localtime(now()) + datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + end = (localtime(now()) + datetime.timedelta(days=1)).replace( + hour=23, minute=59, second=59, microsecond=0 + ) + TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_foo) + TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_bar) + # exclude second day + start = (localtime(now()) + datetime.timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) + end = (localtime(now()) + datetime.timedelta(days=2)).replace( + hour=23, minute=59, second=59, microsecond=0 + ) + TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_foo) + TimePeriodException.objects.create(start_datetime=start, end_datetime=end, desk=desk_bar) + + # 4 slots each day * 1 day + resp = app.get(foo_api_url) + assert len(resp.json['data']) == 4 + # same thing bar agenda + resp = app.get(bar_api_url) + assert len(resp.json['data']) == 4 + # same thing on the virtual agenda + resp = app.get(virtual_api_url) + assert len(resp.json['data']) == 4 + + +def test_virtual_agendas_meetings_datetimes_delays_api(app, mock_now): + foo_agenda = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=7) + MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) + foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') + TimePeriod.objects.create( + weekday=0, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_1, + ) + TimePeriod.objects.create( + weekday=1, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_1, + ) + + bar_agenda = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=7) + MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) + bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') + TimePeriod.objects.create( + weekday=2, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=bar_desk_1, + ) + TimePeriod.objects.create( + weekday=3, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=bar_desk_1, + ) + + virt_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') + + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) + virt_meeting_type = virt_agenda.iter_meetingtypes()[0] + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) + resp = app.get(api_url) + # 8 slots for m each agenda + assert len(resp.json['data']) == 16 + + # restrict foo's minimal_booking_delay : only bar's slots are left + foo_agenda.minimal_booking_delay = 6 + foo_agenda.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 8 + + # restrict bar's maximal_booking_delay : only half of bar's slots are left + bar_agenda.maximal_booking_delay = 5 + bar_agenda.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 4 + + # put back very slots from foo + foo_agenda.minimal_booking_delay = 1 + foo_agenda.maximal_booking_delay = 7 + foo_agenda.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 12 + + +def test_virtual_agendas_meetings_datetimes_exluded_periods(app, mock_now): + foo_agenda = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=7) + MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) + foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') + TimePeriod.objects.create( + weekday=0, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_1, + ) + TimePeriod.objects.create( + weekday=1, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_1, + ) + virt_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) + + api_url = '/api/agenda/%s/meetings/meeting-type/datetimes/' % (virt_agenda.slug) + resp = app.get(api_url) + # 8 slots + data = resp.json['data'] + assert len(data) == 8 + assert data[0]['datetime'] == '2017-05-22 10:00:00' + assert data[1]['datetime'] == '2017-05-22 10:30:00' + assert data[2]['datetime'] == '2017-05-22 11:00:00' + + # exclude one hour the first day + tp1 = TimePeriod.objects.create( + weekday=0, start_time=datetime.time(11, 0), end_time=datetime.time(12, 0), agenda=virt_agenda + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 6 + assert data[0]['datetime'] == '2017-05-22 10:00:00' + assert data[1]['datetime'] == '2017-05-22 10:30:00' + # no more slots the 22 thanks to the exclusion period + assert data[2]['datetime'] == '2017-05-23 10:00:00' + + # exclude the second day + tp2 = TimePeriod.objects.create( + weekday=1, start_time=datetime.time(9, 0), end_time=datetime.time(18, 0), agenda=virt_agenda + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 2 + assert data[0]['datetime'] == '2017-05-22 10:00:00' + assert data[1]['datetime'] == '2017-05-22 10:30:00' + + # go back to no restriction + tp1.delete() + tp2.delete() + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 8 + + # excluded period applies to every desk + foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2') + TimePeriod.objects.create( + weekday=3, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_2, + ) + TimePeriod.objects.create( + weekday=4, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_2, + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 16 + + # exclude one hour the first day + tp1 = TimePeriod.objects.create( + weekday=0, start_time=datetime.time(11, 0), end_time=datetime.time(12, 0), agenda=virt_agenda + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 14 + + # exclude one hour the last day + tp2 = TimePeriod.objects.create( + weekday=4, start_time=datetime.time(11, 0), end_time=datetime.time(12, 0), agenda=virt_agenda + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 12 + + # go back to no restriction + tp1.delete() + tp2.delete() + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 16 + + # add a second real agenda + bar_agenda = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=7) + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) + MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) + bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') + bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') + TimePeriod.objects.create( + weekday=0, + start_time=datetime.time(14, 0), + end_time=datetime.time(16, 0), + desk=bar_desk_1, + ) + TimePeriod.objects.create( + weekday=1, + start_time=datetime.time(14, 0), + end_time=datetime.time(16, 0), + desk=bar_desk_1, + ) + TimePeriod.objects.create( + weekday=2, + start_time=datetime.time(14, 0), + end_time=datetime.time(16, 0), + desk=bar_desk_2, + ) + TimePeriod.objects.create( + weekday=3, + start_time=datetime.time(14, 0), + end_time=datetime.time(16, 0), + desk=bar_desk_2, + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 32 + + # exclude the first day, 11 to 15 : 4 slots + tp1 = TimePeriod.objects.create( + weekday=0, start_time=datetime.time(11, 0), end_time=datetime.time(15, 0), agenda=virt_agenda + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 28 + + +def test_virtual_agendas_meetings_exception(app, user, virtual_meetings_agenda): + app.authorization = ('Basic', ('john.doe', 'password')) + real_agenda = virtual_meetings_agenda.real_agendas.first() + desk = real_agenda.desk_set.first() + virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] + datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % ( + virtual_meetings_agenda.slug, + virt_meeting_type.slug, + ) + resp = app.get(datetimes_url) + + # test exception at the lowest limit + excp1 = TimePeriodException.objects.create( + desk=desk, + start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), + end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), + ) + resp2 = app.get(datetimes_url) + assert len(resp.json['data']) == len(resp2.json['data']) + 4 + + # test exception at the highest limit + excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 22, 11, 0)) + excp1.save() + resp2 = app.get(datetimes_url) + assert len(resp.json['data']) == len(resp2.json['data']) + 2 + + # add an exception with an end datetime less than excp1 end datetime + # and make sure that excp1 end datetime preveil + excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 23, 11, 0)) + excp1.save() + + TimePeriodException.objects.create( + desk=excp1.desk, + start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)), + end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), + ) + + resp2 = app.get(datetimes_url) + assert len(resp.json['data']) == len(resp2.json['data']) + 6 + + # with a second desk + desk2 = Desk.objects.create(label='Desk 2', agenda=real_agenda) + time_period = desk.timeperiod_set.first() + TimePeriod.objects.create( + desk=desk2, + start_time=time_period.start_time, + end_time=time_period.end_time, + weekday=time_period.weekday, + ) + resp3 = app.get(datetimes_url) + assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed + + +def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, mock_now): + foo_agenda = Agenda.objects.create( + label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 + ) + foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) + foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') + + test_1st_weekday = (localtime(now()).weekday() + 2) % 7 + test_2nd_weekday = (localtime(now()).weekday() + 3) % 7 + test_3rd_weekday = (localtime(now()).weekday() + 4) % 7 + test_4th_weekday = (localtime(now()).weekday() + 5) % 7 + + def create_time_perdiods(desk, end=12): + TimePeriod.objects.create( + weekday=test_1st_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(end, 0), + desk=desk, + ) + TimePeriod.objects.create( + weekday=test_2nd_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(end, 0), + desk=desk, + ) + + create_time_perdiods(foo_desk_1) + virt_agenda = Agenda.objects.create( + label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=6 + ) + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) + virt_meeting_type = virt_agenda.iter_meetingtypes()[0] + + # We are saturday and we can book for next monday and tuesday, 4 slots available each day + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) + resp = app.get(api_url) + assert len(resp.json['data']) == 8 + assert resp.json['data'][0]['id'] == 'meeting-type:2017-05-22-1000' + + virt_agenda.maximal_booking_delay = 10 # another monday comes in + virt_agenda.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 12 + + # Back to next monday and tuesday restriction + virt_agenda.maximal_booking_delay = 6 + virt_agenda.save() + + # Add another agenda + bar_agenda = Agenda.objects.create( + label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 + ) + bar_meeting_type = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) + bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') + create_time_perdiods(bar_desk_1, end=13) # bar_agenda has two more slots each day + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) + with CaptureQueriesContext(connection) as ctx: + resp = app.get(api_url) + assert len(resp.json['data']) == 12 + assert len(ctx.captured_queries) == 12 + + # simulate booking + dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') + ev = Event.objects.create( + agenda=foo_agenda, + meeting_type=foo_meeting_type, + places=1, + full=False, + start_datetime=make_aware(dt), + desk=foo_desk_1, + ) + booking1 = Booking.objects.create(event=ev) + + resp = app.get(api_url) + assert len(resp.json['data']) == 12 + # No disabled slot, because the booked slot is still available in second agenda + for slot in resp.json['data']: + assert slot['disabled'] is False + + ev = Event.objects.create( + agenda=bar_agenda, + meeting_type=bar_meeting_type, + places=1, + full=False, + start_datetime=make_aware(dt), + desk=bar_desk_1, + ) + booking2 = Booking.objects.create(event=ev) + + resp = app.get(api_url) + assert len(resp.json['data']) == 12 + # now one slot is disabled + for i, slot in enumerate(resp.json['data']): + if i == 2: + assert slot['disabled'] + else: + assert slot['disabled'] is False + + # Cancel booking, every slot available + booking1.cancel() + booking2.cancel() + resp = app.get(api_url) + assert len(resp.json['data']) == 12 + for slot in resp.json['data']: + assert slot['disabled'] is False + + # Add new desk on foo_agenda, open on wednesday + foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2') + TimePeriod.objects.create( + weekday=test_3rd_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_2, + ) + resp = app.get(api_url) + assert len(resp.json['data']) == 16 + + # Add new desk on bar_agenda, open on thursday + bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') + TimePeriod.objects.create( + weekday=test_4th_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=bar_desk_2, + ) + resp = app.get(api_url) + assert len(resp.json['data']) == 20 + + +@pytest.mark.freeze_time('2021-02-25') +def test_virtual_agendas_meetings_datetimes_exclude_slots(app): + tomorrow = now() + datetime.timedelta(days=1) + agenda = Agenda.objects.create( + label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10 + ) + desk = Desk.objects.create(agenda=agenda, slug='desk') + meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar') + TimePeriod.objects.create( + weekday=tomorrow.date().weekday(), + start_time=datetime.time(9, 0), + end_time=datetime.time(17, 00), + desk=desk, + ) + agenda2 = agenda.duplicate() + virt_agenda = Agenda.objects.create( + label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=10 + ) + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda) + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2) + + event = Event.objects.create( + agenda=agenda, + meeting_type=meeting_type, + places=1, + start_datetime=localtime(tomorrow).replace(hour=9, minute=0), + desk=desk, + ) + Booking.objects.create(event=event, user_external_id='42') + event2 = Event.objects.create( + agenda=agenda, + meeting_type=meeting_type, + places=1, + start_datetime=localtime(tomorrow).replace(hour=10, minute=0), + desk=desk, + ) + cancelled = Booking.objects.create(event=event2, user_external_id='35') + cancelled.cancel() + + resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug)) + assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' + assert resp.json['data'][0]['disabled'] is False + assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' + assert resp.json['data'][2]['disabled'] is False + + resp = app.get( + '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug), + params={'exclude_user_external_id': '35'}, + ) + assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' + assert resp.json['data'][0]['disabled'] is False + assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' + assert resp.json['data'][2]['disabled'] is False + + with CaptureQueriesContext(connection) as ctx: + resp = app.get( + '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug), + params={'exclude_user_external_id': '42'}, + ) + assert len(ctx.captured_queries) == 13 + assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' + assert resp.json['data'][0]['disabled'] is True + assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' + assert resp.json['data'][2]['disabled'] is False + + virt_agenda.minimal_booking_delay = None + virt_agenda.maximal_booking_delay = None + virt_agenda.save() + resp = app.get( + '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug), + params={'exclude_user_external_id': '42'}, + ) + assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900' + assert resp.json['data'][0]['disabled'] is True + assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000' + assert resp.json['data'][2]['disabled'] is False + + +def test_unavailabilitycalendar_on_virtual_datetimes(app, user, mock_now): + foo_agenda = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=7) + MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) + foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') + TimePeriod.objects.create( + weekday=0, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_1, + ) + TimePeriod.objects.create( + weekday=1, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=foo_desk_1, + ) + virt_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) + + api_url = '/api/agenda/%s/meetings/meeting-type/datetimes/' % (virt_agenda.slug) + resp = app.get(api_url) + # 8 slots + data = resp.json['data'] + assert len(data) == 8 + assert data[0]['datetime'] == '2017-05-22 10:00:00' + assert data[1]['datetime'] == '2017-05-22 10:30:00' + assert data[2]['datetime'] == '2017-05-22 11:00:00' + + # exclude one hour the first day through an unvalailability calendar on the foo agenda + unavailability_calendar = UnavailabilityCalendar.objects.create(label='foo holydays') + TimePeriodException.objects.create( + unavailability_calendar=unavailability_calendar, + start_datetime=make_aware(datetime.datetime(2017, 5, 22, 11, 0)), + end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), + ) + unavailability_calendar.desks.add(foo_desk_1) + + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 6 + assert data[0]['datetime'] == '2017-05-22 10:00:00' + assert data[1]['datetime'] == '2017-05-22 10:30:00' + # no more slots the 22 thanks to the unavailability calendar + assert data[2]['datetime'] == '2017-05-23 10:00:00' + + # exclude the second day + TimePeriodException.objects.create( + unavailability_calendar=unavailability_calendar, + start_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), + end_datetime=make_aware(datetime.datetime(2017, 5, 23, 18, 0)), + ) + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 2 + assert data[0]['datetime'] == '2017-05-22 10:00:00' + assert data[1]['datetime'] == '2017-05-22 10:30:00' + + # add a second real agenda + bar_agenda = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=7) + VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) + MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) + bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') + bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') + TimePeriod.objects.create( + weekday=0, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=bar_desk_1, + ) + TimePeriod.objects.create( + weekday=1, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=bar_desk_1, + ) + TimePeriod.objects.create( + weekday=0, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=bar_desk_2, + ) + TimePeriod.objects.create( + weekday=1, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=bar_desk_2, + ) + + # bar_agenda has the same time periods than foo_agenda, but no unavailability calendar + # so we are back at the start : 8 slots + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 8 + + # exclude one hour the second day through another unvalailability calendar on the bar agenda + unavailability_calendar = UnavailabilityCalendar.objects.create(label='bar holydays') + TimePeriodException.objects.create( + unavailability_calendar=unavailability_calendar, + start_datetime=make_aware(datetime.datetime(2017, 5, 23, 11, 0)), + end_datetime=make_aware(datetime.datetime(2017, 5, 23, 12, 0)), + ) + unavailability_calendar.desks.add(bar_desk_1, bar_desk_2) + + # 2 slots are gone + resp = app.get(api_url) + data = resp.json['data'] + assert len(data) == 6 + assert data[0]['datetime'] == '2017-05-22 10:00:00' + assert data[-1]['datetime'] == '2017-05-23 10:30:00' + + +def test_meetings_and_virtual_datetimes_date_filter(app): + agenda_foo = Agenda.objects.create( + label='Agenda Foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=7 + ) + meeting_type = MeetingType.objects.create(agenda=agenda_foo, label='Meeting Type', duration=30) + desk_foo = Desk.objects.create(agenda=agenda_foo, label='Desk 1') + weekday1 = ((localtime(now())).weekday() + 1) % 7 + weekday2 = ((localtime(now())).weekday() + 2) % 7 + weekday3 = ((localtime(now())).weekday() + 3) % 7 + weekday4 = ((localtime(now())).weekday() + 4) % 7 + weekday5 = ((localtime(now())).weekday() + 5) % 7 + weekday6 = ((localtime(now())).weekday() + 6) % 7 + for weekday in (weekday1, weekday2, weekday3, weekday4, weekday5, weekday6): + TimePeriod.objects.create( + weekday=weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk_foo, + ) + + virtual_agenda = Agenda.objects.create( + label='Agenda Virtual', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=7 + ) + VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_foo) + + # 4 slots each day * 6 days + foo_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda_foo.slug, meeting_type.slug) + resp = app.get(foo_api_url) + assert len(resp.json['data']) == 24 + virtual_api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) + resp = app.get(virtual_api_url) + assert len(resp.json['data']) == 24 + + for value in ['foo', '2017-05-42']: + params = {'date_start': value} + resp = app.get(foo_api_url, params=params, status=400) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == 'invalid payload' + assert resp.json['errors']['date_start'] == [ + 'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].' + ] + + for value in ['foo', '2017-05-42']: + params = {'date_end': value} + resp = app.get(foo_api_url, params=params, status=400) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == 'invalid payload' + assert resp.json['errors']['date_end'] == [ + 'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].' + ] + + # exclude weekday1 through date_start, 4 slots each day * 5 days + params = {'date_start': localtime(now() + datetime.timedelta(days=2)).date().isoformat()} + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 20 + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 20 + + params = { + 'date_start': localtime(now() + datetime.timedelta(days=2)) + .replace(hour=12, minute=0, second=0, microsecond=0) + .isoformat() + } + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 16 + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 16 + + params = { + 'date_start': localtime(now() + datetime.timedelta(days=2)) + .replace(hour=11, minute=0, second=0, microsecond=0) + .isoformat() + } + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 18 + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 18 + + # minimal_booking_delay (which exclude weekday1 and wekkday2 ) takes precedence + # 4 slots each day * 4 days + agenda_foo.minimal_booking_delay = 3 + agenda_foo.save() + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 16 + # also on virtual agenda + virtual_agenda.minimal_booking_delay = 3 + virtual_agenda.save() + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 16 + + # reset + agenda_foo.minimal_booking_delay = 1 + virtual_agenda.minimal_booking_delay = 1 + agenda_foo.save() + virtual_agenda.save() + + # exclude weekday6 through date_end, 4 slots each day * 5 days + params = {'date_end': localtime(now() + datetime.timedelta(days=6)).date().isoformat()} + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 20 + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 20 + + params = { + 'date_end': localtime(now() + datetime.timedelta(days=6)) + .replace(hour=11, minute=0, second=0, microsecond=0) + .isoformat() + } + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 22 + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 22 + + # maximal_booking_delay (which exclude weekday5 and weekday6 ) takes precedence + # 4 slots each day * 4 days + agenda_foo.maximal_booking_delay = 5 + agenda_foo.save() + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 16 + # also on virtual agenda + virtual_agenda.maximal_booking_delay = 5 + virtual_agenda.save() + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 16 + + # now check with exceptions in DB + TimePeriodException.objects.create( + desk=desk_foo, + start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), + end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), + ) + + params = { + 'date_start': localtime(now() + datetime.timedelta(days=2)) + .replace(hour=12, minute=0, second=0, microsecond=0) + .isoformat() + } + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 8 + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 8 + + params = { + 'date_end': localtime(now() + datetime.timedelta(days=2)) + .replace(hour=11, minute=0, second=0, microsecond=0) + .isoformat() + } + resp = app.get(foo_api_url, params=params) + assert len(resp.json['data']) == 6 + resp = app.get(virtual_api_url, params=params) + assert len(resp.json['data']) == 6 + + +def test_datetimes_api_virtual_meetings_agenda_meta(app, freezer): + # 2017-05-20 -> saturday + freezer.move_to(make_aware(datetime.datetime(year=2017, month=5, day=20, hour=1, minute=12))) + meetings_agenda1 = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=3) + meetings_agenda2 = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=3) + virtual_agenda = Agenda.objects.create(label='Agenda Virtual', kind='virtual', maximal_booking_delay=3) + VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda1) + VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=meetings_agenda2) + desk1 = Desk.objects.create(agenda=meetings_agenda1, label='Desk 1') + desk2 = Desk.objects.create(agenda=meetings_agenda2, label='Desk 2') + + test_1st_weekday = (localtime(now()).weekday() + 2) % 7 + for agenda, desk in zip((meetings_agenda1, meetings_agenda2), (desk1, desk2)): + meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30) + TimePeriod( + weekday=test_1st_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(12, 0), + desk=desk, + ).save() + + virt_meeting_type = virtual_agenda.iter_meetingtypes()[0] + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, virt_meeting_type.slug) + resp = app.get(api_url) + assert len(resp.json['data']) == 4 + assert resp.json['data'][2]['disabled'] is False + assert resp.json['meta'] == { + 'no_bookable_datetimes': False, + 'bookable_datetimes_number_total': 4, + 'bookable_datetimes_number_available': 4, + 'first_bookable_slot': resp.json['data'][0], + } + + def simulate_booking(slot, agenda, desk): + dt = datetime.datetime.strptime(slot['id'].split(':')[1], '%Y-%m-%d-%H%M') + ev = Event( + agenda=agenda, + meeting_type=meeting_type, + places=1, + full=False, + start_datetime=make_aware(dt), + desk=desk, + ) + ev.save() + booking = Booking(event=ev, user_external_id='42') + booking.save() + + simulate_booking(resp.json['data'][0], meetings_agenda1, desk1) + resp = app.get(api_url) + assert len(resp.json['data']) == 4 + assert resp.json['data'][0]['disabled'] is False + assert resp.json['meta'] == { + 'no_bookable_datetimes': False, + 'bookable_datetimes_number_total': 4, + 'bookable_datetimes_number_available': 4, + 'first_bookable_slot': resp.json['data'][0], + } + resp = app.get(api_url, params={'exclude_user_external_id': '42'}) + assert len(resp.json['data']) == 4 + assert resp.json['data'][0]['disabled'] is True + assert resp.json['meta'] == { + 'no_bookable_datetimes': False, + 'bookable_datetimes_number_total': 4, + 'bookable_datetimes_number_available': 3, + 'first_bookable_slot': resp.json['data'][1], + } + + simulate_booking(resp.json['data'][0], meetings_agenda2, desk2) + resp = app.get(api_url) + assert len(resp.json['data']) == 4 + assert resp.json['data'][0]['disabled'] is True + assert resp.json['meta'] == { + 'no_bookable_datetimes': False, + 'bookable_datetimes_number_total': 4, + 'bookable_datetimes_number_available': 3, + 'first_bookable_slot': resp.json['data'][1], + } + + for idx in range(1, 4): + simulate_booking(resp.json['data'][idx], meetings_agenda1, desk1) + simulate_booking(resp.json['data'][idx], meetings_agenda2, desk2) + resp = app.get(api_url) + assert len(resp.json['data']) == 4 + assert resp.json['meta'] == { + 'no_bookable_datetimes': True, + 'bookable_datetimes_number_total': 4, + 'bookable_datetimes_number_available': 0, + 'first_bookable_slot': None, + } + + +def test_virtual_agendas_time_change(app, freezer): + agenda = Agenda.objects.create( + label='Foo bar Meeting', kind='meetings', minimal_booking_delay=10, maximal_booking_delay=20 + ) + meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=15) + desk, _ = Desk.objects.get_or_create(agenda=agenda, label='Desk 1') + TimePeriodException.objects.create( + desk=desk, + start_datetime=make_aware(datetime.datetime(2022, 4, 4, 8, 0)), + end_datetime=make_aware(datetime.datetime(2022, 4, 4, 12, 0)), + ) + TimePeriod.objects.create( + weekday=0, + start_time=datetime.time(8, 30), + end_time=datetime.time(12, 0), + desk=desk, + ) + dt = datetime.datetime.strptime('2022-03-28-08:30', '%Y-%m-%d-%H:%M') + ev = Event.objects.create( + agenda=agenda, + meeting_type=meeting_type, + places=1, + start_datetime=make_aware(dt), + desk=desk, + ) + Booking.objects.create(event=ev) + + freezer.move_to(make_aware(datetime.datetime(2022, 3, 8, 7, 0))) + + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug) + resp = app.get(api_url) + for slot in resp.json['data']: + if slot['datetime'] == '2022-03-28 08:30:00': + assert False, 'slot should not appear due to maximal_booking_delay' + + # now got through virtual agenda + virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual') + virtual_agenda.real_agendas.add(agenda) + assert virtual_agenda.minimal_booking_delay is None + assert virtual_agenda.maximal_booking_delay is None + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) + resp = app.get(api_url) + for slot in resp.json['data']: + if slot['datetime'] == '2022-03-28 08:30:00': + assert ( + False + ), 'slot should not appear due to maximal_booking_delay of the real agenda (and no maximal_booking_delay) is defined on the real agenda' + + +@pytest.mark.freeze_time('2022-01-20 14:00') # Thursday +def test_datetimes_api_meetings_virtual_agenda_weekday_indexes(app): + agenda = Agenda.objects.create( + label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 + ) + desk = Desk.objects.create(agenda=agenda, label='desk') + meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) + virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual') + virtual_agenda.real_agendas.add(agenda) + + TimePeriod.objects.create( + weekday=0, + weekday_indexes=[1, 2], + start_time=datetime.time(11, 0), + end_time=datetime.time(12, 30), + desk=desk, + ) + + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) + resp = app.get(api_url) + assert len(resp.json['data']) == 12 + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-02-07 11:00:00', + '2022-02-07 11:30:00', + '2022-02-07 12:00:00', + '2022-02-14 11:00:00', + '2022-02-14 11:30:00', + '2022-02-14 12:00:00', + '2022-03-07 11:00:00', + '2022-03-07 11:30:00', + '2022-03-07 12:00:00', + '2022-03-14 11:00:00', + '2022-03-14 11:30:00', + '2022-03-14 12:00:00', + ] + + # add exclusion period on virtual agenda + exclusion_period = TimePeriod.objects.create( + weekday=0, start_time=datetime.time(11, 30), end_time=datetime.time(12, 30), agenda=virtual_agenda + ) + resp = app.get(api_url) + assert len(resp.json['data']) == 4 + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-02-07 11:00:00', + '2022-02-14 11:00:00', + '2022-03-07 11:00:00', + '2022-03-14 11:00:00', + ] + + exclusion_period.start_time = datetime.time(10, 30) + exclusion_period.end_time = datetime.time(11, 30) + exclusion_period.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 8 + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-02-07 11:30:00', + '2022-02-07 12:00:00', + '2022-02-14 11:30:00', + '2022-02-14 12:00:00', + '2022-03-07 11:30:00', + '2022-03-07 12:00:00', + '2022-03-14 11:30:00', + '2022-03-14 12:00:00', + ] + + # add second exclusion period on virtual agenda + TimePeriod.objects.create( + weekday=0, start_time=datetime.time(12, 00), end_time=datetime.time(12, 30), agenda=virtual_agenda + ) + resp = app.get(api_url) + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-02-07 11:30:00', + '2022-02-14 11:30:00', + '2022-03-07 11:30:00', + '2022-03-14 11:30:00', + ] + + +@pytest.mark.freeze_time('2022-10-24 10:00') +def test_datetimes_api_meetings_virtual_agenda_date_time_period(app): + agenda = Agenda.objects.create( + label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=30 + ) + desk = Desk.objects.create(agenda=agenda, label='desk') + meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) + virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual') + virtual_agenda.real_agendas.add(agenda) + + TimePeriod.objects.create( + date=datetime.date(2022, 10, 24), + start_time=datetime.time(12, 0), + end_time=datetime.time(14, 0), + desk=desk, + ) + + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug) + resp = app.get(api_url) + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-10-24 12:00:00', + '2022-10-24 12:30:00', + '2022-10-24 13:00:00', + '2022-10-24 13:30:00', + ] + + # add exclusion period on virtual agenda + TimePeriod.objects.create( + weekday=0, start_time=datetime.time(12, 00), end_time=datetime.time(13, 00), agenda=virtual_agenda + ) + resp = app.get(api_url) + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-10-24 13:00:00', + '2022-10-24 13:30:00', + ] + + # add second exclusion period on virtual agenda + TimePeriod.objects.create( + weekday=0, start_time=datetime.time(13, 30), end_time=datetime.time(14, 00), agenda=virtual_agenda + ) + resp = app.get(api_url) + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-10-24 13:00:00', + ] + + +def test_show_available_agendas(app, user): + agenda = build_virtual_agenda( + agendas={ + 'Agenda 1': { + 'desks': { + 'Desk 1': 'monday-sunday 08:00-12:00 14:00-17:00', + 'Desk 2': 'monday-sunday 08:00-12:00 14:00-17:00', + }, + }, + 'Agenda 2': { + 'desks': { + 'Desk 1': 'monday-sunday 08:00-12:00 14:00-17:00', + 'Desk 2': 'monday-sunday 08:00-12:00 14:00-17:00', + }, + }, + }, + meeting_types=[30], + ) + api_url = '/api/agenda/%s/meetings/mt-30/datetimes/' % agenda.slug + resp = app.get(api_url) + first_slot = resp.json['data'][0] + assert first_slot['disabled'] is False + assert len(first_slot['agendas']) == 2 + assert first_slot['agendas'][0]['text'] == 'Agenda 1' + assert first_slot['agendas'][1]['text'] == 'Agenda 2' + + ## Make a booking directly on agenda 1, and check that nothing changed + app.authorization = ('Basic', ('john.doe', 'password')) + app.post(first_slot['agendas'][0]['api']['fillslot_url']) + resp = app.get(api_url) + first_slot = resp.json['data'][0] + assert first_slot['disabled'] is False + assert len(first_slot['agendas']) == 2 + assert first_slot['agendas'][0]['text'] == 'Agenda 1' + assert first_slot['agendas'][1]['text'] == 'Agenda 2' + + ## Make a second booking directly on agenda 1, agenda 2 should be the only one left available + app.post(first_slot['agendas'][0]['api']['fillslot_url']) + resp = app.get(api_url) + first_slot = resp.json['data'][0] + assert first_slot['disabled'] is False + assert len(first_slot['agendas']) == 1 + assert first_slot['agendas'][0]['text'] == 'Agenda 2' + + ## Make a booking directly on agenda 2, and check that nothing changed + app.post(first_slot['agendas'][0]['api']['fillslot_url']) + resp = app.get(api_url) + first_slot = resp.json['data'][0] + assert first_slot['disabled'] is False + assert len(first_slot['agendas']) == 1 + assert first_slot['agendas'][0]['text'] == 'Agenda 2' + + ## Make a booking directly on agenda 2, the slots is full and no agenda available + app.post(first_slot['agendas'][0]['api']['fillslot_url']) + resp = app.get(api_url) + first_slot = resp.json['data'][0] + assert first_slot['disabled'] is True + assert len(first_slot['agendas']) == 0