From ec474b46d25d9a5331752d79e66670e625dba7f8 Mon Sep 17 00:00:00 2001 From: Emmanuel Cazenave Date: Wed, 26 Feb 2020 18:18:15 +0100 Subject: [PATCH] api: restrict slots with exluded timeperiods (#40058) --- chrono/agendas/models.py | 41 ++++++++++++++ chrono/api/views.py | 26 +++++---- tests/test_agendas.py | 90 ++++++++++++++++++++++++++++++ tests/test_api.py | 117 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+), 11 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 5f5a089d..4a5f35f4 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -361,6 +361,47 @@ class TimePeriod(models.Model): 'end_time': self.end_time.strftime('%H:%M'), } + def get_effective_timeperiods(self, excluded_timeperiods): + effective_timeperiods = [self] + for excluded_timeperiod in excluded_timeperiods: + res = [] + for effective_timeperiod in effective_timeperiods: + if ( + excluded_timeperiod.weekday != effective_timeperiod.weekday + or excluded_timeperiod.start_time >= effective_timeperiod.end_time + or excluded_timeperiod.end_time <= effective_timeperiod.start_time + ): + res.append(effective_timeperiod) + continue + if ( + excluded_timeperiod.start_time <= effective_timeperiod.start_time + and excluded_timeperiod.end_time >= effective_timeperiod.end_time + ): + # completely exclude + continue + if excluded_timeperiod.start_time > effective_timeperiod.start_time: + res.append( + TimePeriod( + weekday=effective_timeperiod.weekday, + start_time=effective_timeperiod.start_time, + end_time=excluded_timeperiod.start_time, + desk=effective_timeperiod.desk, + ) + ) + if excluded_timeperiod.end_time < effective_timeperiod.end_time: + res.append( + TimePeriod( + weekday=effective_timeperiod.weekday, + start_time=excluded_timeperiod.end_time, + end_time=effective_timeperiod.end_time, + desk=effective_timeperiod.desk, + ) + ) + + effective_timeperiods = res + + return effective_timeperiods + def get_time_slots(self, min_datetime, max_datetime, meeting_type): meeting_duration = datetime.timedelta(minutes=meeting_type.duration) duration = datetime.timedelta(minutes=self.desk.agenda.get_base_meeting_duration()) diff --git a/chrono/api/views.py b/chrono/api/views.py index 3c43df38..dd35b961 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -76,6 +76,7 @@ def get_all_slots(agenda, meeting_type): base_date = now().date() agendas = agenda.get_real_agendas() + base_agenda = agenda open_slots = {} for agenda in agendas: @@ -88,17 +89,20 @@ def get_all_slots(agenda, meeting_type): if used_time_period_filters['max_datetime'] is None: used_time_period_filters['max_datetime'] = get_max_datetime(agenda) - for time_period in TimePeriod.objects.filter(desk__agenda=agenda): - duration = ( - datetime.datetime.combine(base_date, time_period.end_time) - - datetime.datetime.combine(base_date, time_period.start_time) - ).seconds / 60 - if duration < meeting_type.duration: - # skip time period that can't even hold a single meeting - continue - for slot in time_period.get_time_slots(**used_time_period_filters): - slot.full = False - open_slots[agenda][time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot) + for raw_time_period in TimePeriod.objects.filter(desk__agenda=agenda): + for time_period in raw_time_period.get_effective_timeperiods( + base_agenda.excluded_timeperiods.all() + ): + duration = ( + datetime.datetime.combine(base_date, time_period.end_time) + - datetime.datetime.combine(base_date, time_period.start_time) + ).seconds / 60 + if duration < meeting_type.duration: + # skip time period that can't even hold a single meeting + continue + for slot in time_period.get_time_slots(**used_time_period_filters): + slot.full = False + open_slots[agenda][time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot) # remove excluded slot for agenda in agendas: diff --git a/tests/test_agendas.py b/tests/test_agendas.py index 4df582db..03d28b54 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -16,6 +16,7 @@ from chrono.agendas.models import ( Event, ICSError, MeetingType, + TimePeriod, TimePeriodException, TimePeriodExceptionSource, VirtualMember, @@ -594,3 +595,92 @@ def test_virtual_agenda_base_meeting_duration(): meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60) meeting_type.save() assert virt_agenda.get_base_meeting_duration() == 60 + + +def test_get_effective_timeperiods(): + time_period = TimePeriod(weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)) + # empty exclusion set + effective_timeperiods = time_period.get_effective_timeperiods(TimePeriod.objects.none()) + assert len(effective_timeperiods) == 1 + effective_timeperiod = effective_timeperiods[0] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == time_period.start_time + assert effective_timeperiod.end_time == time_period.end_time + + # exclusions are on a different day + excluded_timeperiods = [ + TimePeriod(weekday=1, start_time=datetime.time(17, 0), end_time=datetime.time(18, 0)), + TimePeriod(weekday=2, start_time=datetime.time(17, 0), end_time=datetime.time(18, 0)), + ] + effective_timeperiods = time_period.get_effective_timeperiods(excluded_timeperiods) + assert len(effective_timeperiods) == 1 + effective_timeperiod = effective_timeperiods[0] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == time_period.start_time + assert effective_timeperiod.end_time == time_period.end_time + + # one exclusion, end_time should be earlier + excluded_timeperiods = [ + TimePeriod(weekday=0, start_time=datetime.time(17, 0), end_time=datetime.time(18, 0)) + ] + effective_timeperiods = time_period.get_effective_timeperiods(excluded_timeperiods) + assert len(effective_timeperiods) == 1 + effective_timeperiod = effective_timeperiods[0] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(10, 0) + assert effective_timeperiod.end_time == datetime.time(17, 0) + + # one exclusion, start_time should be later + excluded_timeperiods = [ + TimePeriod(weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(16, 0)) + ] + effective_timeperiods = time_period.get_effective_timeperiods(excluded_timeperiods) + assert len(effective_timeperiods) == 1 + effective_timeperiod = effective_timeperiods[0] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(16, 0) + assert effective_timeperiod.end_time == datetime.time(18, 0) + + # one exclusion, splits effective timeperiod in two + excluded_timeperiods = [ + TimePeriod(weekday=0, start_time=datetime.time(12, 0), end_time=datetime.time(16, 0)) + ] + effective_timeperiods = time_period.get_effective_timeperiods(excluded_timeperiods) + assert len(effective_timeperiods) == 2 + effective_timeperiod = effective_timeperiods[0] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(10, 0) + assert effective_timeperiod.end_time == datetime.time(12, 0) + effective_timeperiod = effective_timeperiods[1] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(16, 0) + assert effective_timeperiod.end_time == datetime.time(18, 0) + + # several exclusion, splits effective timeperiod into pieces + excluded_timeperiods = [ + TimePeriod(weekday=0, start_time=datetime.time(12, 0), end_time=datetime.time(13, 0)), + TimePeriod(weekday=0, start_time=datetime.time(10, 30), end_time=datetime.time(11, 30)), + TimePeriod(weekday=0, start_time=datetime.time(16, 30), end_time=datetime.time(17, 00)), + ] + effective_timeperiods = time_period.get_effective_timeperiods(excluded_timeperiods) + assert len(effective_timeperiods) == 4 + + effective_timeperiod = effective_timeperiods[0] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(10, 0) + assert effective_timeperiod.end_time == datetime.time(10, 30) + + effective_timeperiod = effective_timeperiods[1] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(11, 30) + assert effective_timeperiod.end_time == datetime.time(12, 0) + + effective_timeperiod = effective_timeperiods[2] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(13, 0) + assert effective_timeperiod.end_time == datetime.time(16, 30) + + effective_timeperiod = effective_timeperiods[3] + assert effective_timeperiod.weekday == time_period.weekday + assert effective_timeperiod.start_time == datetime.time(17, 0) + assert effective_timeperiod.end_time == datetime.time(18, 0) diff --git a/tests/test_api.py b/tests/test_api.py index b81cc073..913ddaca 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2676,6 +2676,123 @@ def test_virtual_agendas_meetings_datetimes_delays_api(app, mock_now): 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()