From 47844d35e42150657c37f6ee006bdc9838f68490 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 20 Jan 2022 16:34:43 +0100 Subject: [PATCH 4/5] api: account for time period weekday indexes (#45159) --- chrono/agendas/models.py | 25 ++++++---- tests/api/test_fillslot.py | 44 +++++++++++++++++ tests/api/test_meetings_datetimes.py | 68 ++++++++++++++++++++++++++ tests/test_time_periods.py | 71 ++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 8 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 9a353ea6..fc37f2b2 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -53,6 +53,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext from chrono.interval import Interval, IntervalSet +from chrono.utils.date import get_weekday_index from chrono.utils.db import SumCardinality from chrono.utils.publik_urls import translate_from_publik_url from chrono.utils.requests_wrapper import requests as requests_wrapper @@ -1181,7 +1182,7 @@ class VirtualMember(models.Model): WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0]) -class WeekTime(collections.namedtuple('WeekTime', ['weekday', 'time'])): +class WeekTime(collections.namedtuple('WeekTime', ['weekday', 'weekday_indexes', 'time'])): """Representation of a time point in a weekday, ex.: Monday at 5 o'clock.""" def __repr__(self): @@ -1269,13 +1270,14 @@ class TimePeriod(models.Model): def as_weektime_interval(self): return Interval( - WeekTime(self.weekday, self.start_time), - WeekTime(self.weekday, self.end_time), + WeekTime(self.weekday, self.weekday_indexes, self.start_time), + WeekTime(self.weekday, self.weekday_indexes, self.end_time), ) def as_shared_timeperiods(self): return SharedTimePeriod( weekday=self.weekday, + weekday_indexes=self.weekday_indexes, start_time=self.start_time, end_time=self.end_time, desks=[self.desk], @@ -1303,10 +1305,11 @@ class SharedTimePeriod: of get_all_slots() for details). """ - __slots__ = ['weekday', 'start_time', 'end_time', 'desks'] + __slots__ = ['weekday', 'weekday_indexes', 'start_time', 'end_time', 'desks'] - def __init__(self, weekday, start_time, end_time, desks): + def __init__(self, weekday, weekday_indexes, start_time, end_time, desks): self.weekday = weekday + self.weekday_indexes = weekday_indexes self.start_time = start_time self.end_time = end_time self.desks = set(desks) @@ -1376,7 +1379,11 @@ class SharedTimePeriod: while event_datetime < max_datetime: end_time = event_datetime + meeting_duration next_time = event_datetime + duration - if end_time.time() > self.end_time or event_datetime.date() != next_time.date(): + if ( + end_time.time() > self.end_time + or event_datetime.date() != next_time.date() + or (self.weekday_indexes and get_weekday_index(event_datetime) not in self.weekday_indexes) + ): # switch to naive time for day/week changes event_datetime = make_naive(event_datetime) # back to morning @@ -1385,9 +1392,10 @@ class SharedTimePeriod: ) # but next week event_datetime += datetime.timedelta(days=7) + # and re-align to timezone afterwards event_datetime = make_aware(event_datetime) - next_time = event_datetime + duration + continue # don't end after max_datetime if event_datetime > max_datetime: @@ -1399,9 +1407,10 @@ class SharedTimePeriod: @classmethod def from_weektime_interval(cls, weektime_interval, desks=()): begin, end = weektime_interval - assert begin.weekday == end.weekday + assert begin.weekday == end.weekday and begin.weekday_indexes == end.weekday_indexes return cls( weekday=begin.weekday, + weekday_indexes=begin.weekday_indexes, start_time=begin.time, end_time=end.time, desks=desks, diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py index 88f981f9..af29d75a 100644 --- a/tests/api/test_fillslot.py +++ b/tests/api/test_fillslot.py @@ -889,6 +889,50 @@ def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, u assert resp.json['data'][event_index]['disabled'] +@pytest.mark.freeze_time('2022-01-20 14:00') # Thursday +def test_booking_api_meeting_weekday_indexes(app, user): + agenda = Agenda.objects.create( + label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 + ) + meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) + desk = Desk.objects.create(agenda=agenda, label='desk') + + time_period = TimePeriod.objects.create( + weekday=3, # Thursday + weekday_indexes=[1, 3], + start_time=datetime.time(11, 0), + end_time=datetime.time(12, 0), + desk=desk, + ) + datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug)) + slot = datetimes_resp.json['data'][0]['id'] + assert slot == 'plop:2022-02-03-1100' + + app.authorization = ('Basic', ('john.doe', 'password')) + + # single booking + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot)) + assert Booking.objects.count() == 1 + assert resp.json['duration'] == 30 + + # multiple slots + slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']] + assert slots == ['plop:2022-02-03-1130', 'plop:2022-02-17-1100'] + resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots}) + assert Booking.objects.count() == 3 + + # try to book slot on a skipped week + slot = datetimes_resp.json['data'][3]['id'] + time_period.weekday_indexes = [1] + time_period.save() + resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug)) + assert slot not in {slot['id'] for slot in resp.json['data']} + + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot)) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == 'no more desk available' + + def test_booking_api_with_data(app, user): agenda = Agenda.objects.create(label='Foo bar', kind='events') event = Event.objects.create( diff --git a/tests/api/test_meetings_datetimes.py b/tests/api/test_meetings_datetimes.py index 90624528..98cdfa82 100644 --- a/tests/api/test_meetings_datetimes.py +++ b/tests/api/test_meetings_datetimes.py @@ -2300,3 +2300,71 @@ def test_virtual_agendas_time_change(app, freezer): 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( + label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 + ) + meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) + desk = Desk.objects.create(agenda=agenda, label='desk') + + time_period = TimePeriod.objects.create( + weekday=3, # Thursday + start_time=datetime.time(11, 0), + end_time=datetime.time(12, 0), + desk=desk, + ) + api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug) + + resp = app.get(api_url) + assert len(resp.json['data']) == 16 + assert [x['datetime'] for x in resp.json['data']][:6] == [ + '2022-01-27 11:00:00', + '2022-01-27 11:30:00', + '2022-02-03 11:00:00', + '2022-02-03 11:30:00', + '2022-02-10 11:00:00', + '2022-02-10 11:30:00', + ] + every_weeks_resp = resp + + time_period.weekday_indexes = [1] + time_period.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 4 + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-02-03 11:00:00', + '2022-02-03 11:30:00', + '2022-03-03 11:00:00', + '2022-03-03 11:30:00', + ] + + time_period.weekday_indexes = [1, 3] + time_period.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 8 + assert [x['datetime'] for x in resp.json['data']] == [ + '2022-02-03 11:00:00', + '2022-02-03 11:30:00', + '2022-02-17 11:00:00', + '2022-02-17 11:30:00', + '2022-03-03 11:00:00', + '2022-03-03 11:30:00', + '2022-03-17 11:00:00', + '2022-03-17 11:30:00', + ] + + time_period.weekday_indexes = [1, 2, 3, 4, 5] + time_period.save() + resp = app.get(api_url) + assert resp.json == every_weeks_resp.json + + # there are five Mondays this month + time_period.weekday = 0 + time_period.weekday_indexes = [5] + time_period.save() + resp = app.get(api_url) + assert len(resp.json['data']) == 2 + assert [x['datetime'] for x in resp.json['data']] == ['2022-01-31 11:00:00', '2022-01-31 11:30:00'] diff --git a/tests/test_time_periods.py b/tests/test_time_periods.py index 93137eeb..f3eabed7 100644 --- a/tests/test_time_periods.py +++ b/tests/test_time_periods.py @@ -255,3 +255,74 @@ def test_timeperiod_midnight_overlap_time_slots(): assert events[2].timetuple()[:5] == (2016, 9, 19, 21, 0) assert events[3].timetuple()[:5] == (2016, 9, 26, 21, 0) assert len(events) == 4 + + +def test_timeperiod_weekday_indexes(): + agenda = Agenda.objects.create( + label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 + ) + meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=60) + desk = Desk.objects.create(agenda=agenda, label='desk') + + timeperiod = TimePeriod.objects.create( + weekday=0, # Monday + start_time=datetime.time(11, 0), + end_time=datetime.time(12, 0), + desk=desk, + ) + + def get_events(min_datetime, max_datetime): + return sorted( + timeperiod.as_shared_timeperiods().get_time_slots( + min_datetime=make_aware(min_datetime), + max_datetime=make_aware(max_datetime), + meeting_duration=meeting_type.duration, + base_duration=agenda.get_base_meeting_duration(), + ) + ) + + events = get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) + assert events[0].timetuple()[:5] == (2022, 3, 7, 11, 0) + assert events[1].timetuple()[:5] == (2022, 3, 14, 11, 0) + assert events[2].timetuple()[:5] == (2022, 3, 21, 11, 0) + assert events[3].timetuple()[:5] == (2022, 3, 28, 11, 0) + assert len(events) == 4 + + timeperiod.weekday_indexes = [1] + timeperiod.save() + events = get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) + assert events[0].timetuple()[:5] == (2022, 3, 7, 11, 0) + assert len(events) == 1 + + timeperiod.weekday_indexes = [3, 4] + timeperiod.save() + events = get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) + assert events[0].timetuple()[:5] == (2022, 3, 21, 11, 0) + assert events[1].timetuple()[:5] == (2022, 3, 28, 11, 0) + assert len(events) == 2 + + timeperiod.weekday_indexes = [5] + timeperiod.save() + assert get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) == [] + + # month with five Mondays + events = get_events(datetime.datetime(2022, 5, 1), datetime.datetime(2022, 6, 1)) + assert events[0].timetuple()[:5] == (2022, 5, 30, 11, 0) + assert len(events) == 1 + + # reduce ranges + events = get_events(datetime.datetime(2022, 5, 30), datetime.datetime(2022, 5, 31)) + assert events[0].timetuple()[:5] == (2022, 5, 30, 11, 0) + assert len(events) == 1 + + assert get_events(datetime.datetime(2022, 5, 29), datetime.datetime(2022, 5, 30)) == [] + assert get_events(datetime.datetime(2022, 5, 1), datetime.datetime(2022, 5, 20)) == [] + + # midnight overlap + timeperiod.start_time = datetime.time(22, 0) + timeperiod.end_time = datetime.time(23, 0) + timeperiod.save() + + events = get_events(datetime.datetime(2022, 5, 1), datetime.datetime(2022, 6, 1)) + assert events[0].timetuple()[:5] == (2022, 5, 30, 22, 0) + assert len(events) == 1 -- 2.30.2