From 4961fa19a27b3bf0334ed02f1c18a743a26a045c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Mon, 20 Nov 2017 01:10:44 +0400 Subject: [PATCH] general: exhaustively list available meeting datetimes (#19150) --- chrono/agendas/models.py | 20 ++++++++++++---- tests/test_agendas.py | 21 ++++++++++++++++- tests/test_api.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_time_periods.py | 16 ++++++++----- 4 files changed, 103 insertions(+), 11 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index b648117..047ab33 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import datetime +import fractions import requests import vobject @@ -95,6 +96,15 @@ class Agenda(models.Model): group_ids = [x.id for x in user.groups.all()] return bool(self.view_role_id in group_ids) + def get_base_meeting_duration(self): + durations = [x.duration for x in MeetingType.objects.filter(agenda=self)] + if not durations: + raise ValueError() + gcd = durations[0] + for duration in durations[1:]: + gcd = fractions.gcd(duration, gcd) + return gcd + def export_json(self): agenda = { 'label': self.label, @@ -187,7 +197,8 @@ class TimePeriod(models.Model): } def get_time_slots(self, min_datetime, max_datetime, meeting_type): - duration = datetime.timedelta(minutes=meeting_type.duration) + meeting_duration = datetime.timedelta(minutes=meeting_type.duration) + duration = datetime.timedelta(minutes=self.desk.agenda.get_base_meeting_duration()) min_datetime = make_naive(min_datetime) max_datetime = make_naive(max_datetime) @@ -199,19 +210,20 @@ class TimePeriod(models.Model): event_datetime = real_min_datetime.replace(hour=self.start_time.hour, minute=self.start_time.minute, second=0, microsecond=0) while event_datetime < max_datetime: - end_time = event_datetime + duration + end_time = event_datetime + meeting_duration + next_time = event_datetime + duration if end_time.time() > self.end_time: # back to morning event_datetime = event_datetime.replace(hour=self.start_time.hour, minute=self.start_time.minute) # but next week event_datetime += datetime.timedelta(days=7) - end_time = event_datetime + duration + next_time = event_datetime + duration if event_datetime > max_datetime: break yield TimeSlot(start_datetime=make_aware(event_datetime), meeting_type=meeting_type, desk=self.desk) - event_datetime = end_time + event_datetime = next_time class MeetingType(models.Model): diff --git a/tests/test_agendas.py b/tests/test_agendas.py index a99a9e3..6ff8820 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -10,7 +10,7 @@ from django.core.management import call_command from django.core.management.base import CommandError from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, - Desk, TimePeriodException, ICSError) + Desk, TimePeriod, TimePeriodException, ICSError) pytestmark = pytest.mark.django_db @@ -330,3 +330,22 @@ END:VCALENDAR""" mocked_get.return_value = mocked_response call_command('sync_desks_timeperiod_exceptions') assert not TimePeriodException.objects.filter(desk=desk).exists() + +def test_base_meeting_duration(): + agenda = Agenda(label='Meeting', kind='meetings') + agenda.save() + + with pytest.raises(ValueError): + agenda.get_base_meeting_duration() + + meeting_type = MeetingType(agenda=agenda, label='Foo', duration=30) + meeting_type.save() + assert agenda.get_base_meeting_duration() == 30 + + meeting_type = MeetingType(agenda=agenda, label='Bar', duration=60) + meeting_type.save() + assert agenda.get_base_meeting_duration() == 30 + + meeting_type = MeetingType(agenda=agenda, label='Bar', duration=45) + meeting_type.save() + assert agenda.get_base_meeting_duration() == 15 diff --git a/tests/test_api.py b/tests/test_api.py index fb2f546..199c6d9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1023,3 +1023,60 @@ def test_agenda_meeting_api_desk_info(app, meetings_agenda, user): resp = app.post(booking_url2) assert resp.json['desk']['label'] == desk.label assert resp.json['desk']['slug'] == desk.slug + + +def test_agenda_meeting_gcd_durations(app, meetings_agenda, user): + meetings_agenda.maximal_booking_delay = 8 + meetings_agenda.save() + + time_period = TimePeriod.objects.get(end_time=datetime.time(12, 0)) + time_period.end_time = datetime.time(13, 0) + time_period.save() + + meeting_type_30 = MeetingType.objects.get(duration=30) + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) + assert len(resp.json['data']) == 20 + + meeting_type_20 = MeetingType(agenda=meetings_agenda, label='Lorem', duration=20) + meeting_type_20.save() + + assert meetings_agenda.get_base_meeting_duration() == 10 + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) + assert len(resp.json['data']) == 56 + # 16:30 is time period end time (17:00) minus meeting type duration + assert resp.json['data'][-1]['datetime'] == '2017-05-23 16:30:00' + + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) + assert len(resp.json['data']) == 58 + assert resp.json['data'][-1]['datetime'] == '2017-05-23 16:40:00' + + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) + event_id = resp.json['data'][0]['id'] + app.authorization = ('Basic', ('john.doe', 'password')) + app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) + assert Booking.objects.count() == 1 + + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) + assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 55 + event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id'] + resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) + assert resp.json['datetime'].startswith('2017-05-22T10:30:00') + assert Booking.objects.count() == 2 + + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) + event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id'] + resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) + assert resp.json['datetime'].startswith('2017-05-22T10:50:00') + assert Booking.objects.count() == 3 + + # create a gap + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) + event_id = [x for x in resp.json['data'] if not x.get('disabled')][1]['id'] + resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) + assert resp.json['datetime'].startswith('2017-05-22T11:30:00') + assert Booking.objects.count() == 4 + + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) + assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith('2017-05-22 12:00:00') + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) + assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith('2017-05-22 12:00:00') diff --git a/tests/test_time_periods.py b/tests/test_time_periods.py index bfcb02d..98ef49e 100644 --- a/tests/test_time_periods.py +++ b/tests/test_time_periods.py @@ -12,13 +12,15 @@ def test_timeperiod_time_slots(): agenda = Agenda(label=u'Foo bar', slug='bar') agenda.save() desk = Desk.objects.create(label='Desk 1', agenda=agenda) + meeting_type = MeetingType(duration=60, agenda=agenda) + meeting_type.save() timeperiod = TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots( min_datetime=make_aware(datetime.datetime(2016, 9, 1)), max_datetime=make_aware(datetime.datetime(2016, 10, 1)), - meeting_type=MeetingType(duration=60)) + meeting_type=meeting_type) events = list(sorted(events, key=lambda x: x.start_datetime)) assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 5, 9, 0) assert events[1].start_datetime.timetuple()[:5] == (2016, 9, 5, 10, 0) @@ -35,7 +37,7 @@ def test_timeperiod_time_slots(): events = timeperiod.get_time_slots( min_datetime=make_aware(datetime.datetime(2016, 9, 1)), max_datetime=make_aware(datetime.datetime(2016, 10, 1)), - meeting_type=MeetingType(duration=60)) + meeting_type=meeting_type) events = list(sorted(events, key=lambda x: x.start_datetime)) assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 6, 9, 0) assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 27, 11, 0) @@ -48,7 +50,7 @@ def test_timeperiod_time_slots(): events = timeperiod.get_time_slots( min_datetime=make_aware(datetime.datetime(2016, 9, 1)), max_datetime=make_aware(datetime.datetime(2016, 10, 1)), - meeting_type=MeetingType(duration=60)) + meeting_type=meeting_type) events = list(sorted(events, key=lambda x: x.start_datetime)) assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 1, 9, 0) assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 29, 11, 0) @@ -61,7 +63,7 @@ def test_timeperiod_time_slots(): events = timeperiod.get_time_slots( min_datetime=make_aware(datetime.datetime(2016, 9, 1)), max_datetime=make_aware(datetime.datetime(2016, 10, 1)), - meeting_type=MeetingType(duration=60)) + meeting_type=meeting_type) events = list(sorted(events, key=lambda x: x.start_datetime)) assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 2, 9, 0) assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 30, 11, 0) @@ -74,20 +76,22 @@ def test_timeperiod_time_slots(): events = timeperiod.get_time_slots( min_datetime=make_aware(datetime.datetime(2016, 9, 1)), max_datetime=make_aware(datetime.datetime(2016, 10, 1)), - meeting_type=MeetingType(duration=60)) + meeting_type=meeting_type) events = list(sorted(events, key=lambda x: x.start_datetime)) assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0) assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 0) assert len(events) == 12 # shorter duration -> double the events + meeting_type.duration = 30 + meeting_type.save() timeperiod = TimePeriod(desk=desk, weekday=5, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots( min_datetime=make_aware(datetime.datetime(2016, 9, 1)), max_datetime=make_aware(datetime.datetime(2016, 10, 1)), - meeting_type=MeetingType(duration=30)) + meeting_type=meeting_type) events = list(sorted(events, key=lambda x: x.start_datetime)) assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0) assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 30) -- 2.15.0