chrono/tests/test_time_periods.py

519 lines
19 KiB
Python

import datetime
import pytest
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.test import override_settings
from django.utils.encoding import force_str
from chrono.agendas.models import Agenda, Desk, MeetingType, TimePeriod, TimePeriodException
from chrono.utils.timezone import localtime, make_aware
pytestmark = pytest.mark.django_db
def test_timeperiod_time_slots():
agenda = Agenda(label='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.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
events = sorted(events)
assert events[0].timetuple()[:5] == (2016, 9, 5, 9, 0)
assert events[1].timetuple()[:5] == (2016, 9, 5, 10, 0)
assert events[2].timetuple()[:5] == (2016, 9, 5, 11, 0)
assert events[3].timetuple()[:5] == (2016, 9, 12, 9, 0)
assert events[4].timetuple()[:5] == (2016, 9, 12, 10, 0)
assert events[-1].timetuple()[:5] == (2016, 9, 26, 11, 0)
assert len(events) == 12
# another start before the timeperiod
timeperiod = TimePeriod(
desk=desk, weekday=1, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
events = timeperiod.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
events = sorted(events)
assert events[0].timetuple()[:5] == (2016, 9, 6, 9, 0)
assert events[-1].timetuple()[:5] == (2016, 9, 27, 11, 0)
assert len(events) == 12
# a start on the day of the timeperiod
timeperiod = TimePeriod(
desk=desk, weekday=3, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
events = timeperiod.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
events = sorted(events)
assert events[0].timetuple()[:5] == (2016, 9, 1, 9, 0)
assert events[-1].timetuple()[:5] == (2016, 9, 29, 11, 0)
assert len(events) == 15
# a start after the day of the timeperiod
timeperiod = TimePeriod(
desk=desk, weekday=4, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
events = timeperiod.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
events = sorted(events)
assert events[0].timetuple()[:5] == (2016, 9, 2, 9, 0)
assert events[-1].timetuple()[:5] == (2016, 9, 30, 11, 0)
assert len(events) == 15
# another start after the day of the timeperiod
timeperiod = TimePeriod(
desk=desk, weekday=5, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
events = timeperiod.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
events = sorted(events)
assert events[0].timetuple()[:5] == (2016, 9, 3, 9, 0)
assert events[-1].timetuple()[:5] == (2016, 9, 24, 11, 0)
assert len(events) == 12
# shorter duration -> double the events
meeting_type.duration = 30
meeting_type.save()
del agenda.__dict__['cached_meetingtypes']
timeperiod = TimePeriod(
desk=desk, weekday=5, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
events = timeperiod.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
events = sorted(events)
assert events[0].timetuple()[:5] == (2016, 9, 3, 9, 0)
assert events[-1].timetuple()[:5] == (2016, 9, 24, 11, 30)
assert len(events) == 24
@override_settings(LANGUAGE_CODE='fr-fr')
def test_time_period_exception_as_string():
# single day
assert (
force_str(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18)),
end_datetime=make_aware(datetime.datetime(2018, 1, 19)),
)
)
== '18 jan. 2018'
)
# multiple full days
assert (
force_str(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18)),
end_datetime=make_aware(datetime.datetime(2018, 1, 20)),
)
)
== '18 jan. 2018 → 20 jan. 2018'
)
# a few hours in a day
assert (
force_str(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)),
end_datetime=make_aware(datetime.datetime(2018, 1, 18, 12, 0)),
)
)
== '18 jan. 2018 10:00 → 12:00'
)
# multiple days and different times
assert (
force_str(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)),
end_datetime=make_aware(datetime.datetime(2018, 1, 20, 12, 0)),
)
)
== '18 jan. 2018 10:00 → 20 jan. 2018 12:00'
)
def test_desk_opening_hours():
def set_prefetched_exceptions(desk):
desk.prefetched_exceptions = TimePeriodException.objects.filter(
Q(desk=desk) | Q(unavailability_calendar__desks=desk)
)
agenda = Agenda(label='Foo bar', slug='bar')
agenda.save()
desk = Desk.objects.create(label='Desk 1', agenda=agenda)
# nothing yet
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 0
# morning
TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)).save()
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 1
assert hours[0].begin.time() == datetime.time(9, 0)
assert hours[0].end.time() == datetime.time(12, 0)
# and afternoon
TimePeriod(desk=desk, weekday=0, start_time=datetime.time(14, 0), end_time=datetime.time(17, 0)).save()
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 2
assert hours[0].begin.time() == datetime.time(9, 0)
assert hours[0].end.time() == datetime.time(12, 0)
assert hours[1].begin.time() == datetime.time(14, 0)
assert hours[1].end.time() == datetime.time(17, 0)
# full day exception
exception = TimePeriodException(
desk=desk,
start_datetime=make_aware(datetime.datetime(2018, 1, 22)),
end_datetime=make_aware(datetime.datetime(2018, 1, 23)),
)
exception.save()
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 0
# closed from 11am
exception.start_datetime = make_aware(datetime.datetime(2018, 1, 22, 11, 0))
exception.save()
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 1
assert localtime(hours[0].begin).time() == datetime.time(9, 0)
assert localtime(hours[0].end).time() == datetime.time(11, 0)
# closed in the middle
exception.start_datetime = make_aware(datetime.datetime(2018, 1, 22, 10, 0))
exception.end_datetime = make_aware(datetime.datetime(2018, 1, 22, 11, 0))
exception.save()
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 3
assert localtime(hours[0].begin).time() == datetime.time(9, 0)
assert localtime(hours[0].end).time() == datetime.time(10, 0)
assert localtime(hours[1].begin).time() == datetime.time(11, 0)
assert localtime(hours[1].end).time() == datetime.time(12, 0)
assert localtime(hours[2].begin).time() == datetime.time(14, 0)
assert localtime(hours[2].end).time() == datetime.time(17, 0)
def test_desk_opening_hours_weekday_indexes():
def set_prefetched_exceptions(desk):
desk.prefetched_exceptions = TimePeriodException.objects.filter(
Q(desk=desk) | Q(unavailability_calendar__desks=desk)
)
agenda = Agenda.objects.create(label='Foo bar', slug='bar')
desk = Desk.objects.create(label='Desk 1', agenda=agenda)
# morning
timeperiod = TimePeriod.objects.create(
desk=desk,
weekday=0,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0),
weekday_indexes=[4],
)
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 1
assert hours[0].begin.time() == datetime.time(9, 0)
assert hours[0].end.time() == datetime.time(12, 0)
# and afternoon
TimePeriod.objects.create(
desk=desk,
weekday=0,
start_time=datetime.time(14, 0),
end_time=datetime.time(17, 0),
weekday_indexes=[4],
)
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 2
assert hours[0].begin.time() == datetime.time(9, 0)
assert hours[0].end.time() == datetime.time(12, 0)
assert hours[1].begin.time() == datetime.time(14, 0)
assert hours[1].end.time() == datetime.time(17, 0)
timeperiod.weekday_indexes = [5]
timeperiod.save()
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 1
assert hours[0].begin.time() == datetime.time(14, 0)
assert hours[0].end.time() == datetime.time(17, 0)
hours = desk.get_opening_hours(datetime.date(2018, 1, 29))
assert len(hours) == 1
assert hours[0].begin.time() == datetime.time(9, 0)
assert hours[0].end.time() == datetime.time(12, 0)
# full day exception
exception = TimePeriodException(
desk=desk,
start_datetime=make_aware(datetime.datetime(2018, 1, 22)),
end_datetime=make_aware(datetime.datetime(2018, 1, 23)),
)
exception.save()
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2018, 1, 22))
assert len(hours) == 0
def test_desk_opening_hours_date_time_period():
def set_prefetched_exceptions(desk):
desk.prefetched_exceptions = TimePeriodException.objects.filter(
Q(desk=desk) | Q(unavailability_calendar__desks=desk)
)
agenda = Agenda.objects.create(label='Foo bar', slug='bar')
desk = Desk.objects.create(label='Desk 1', agenda=agenda)
# morning
TimePeriod.objects.create(
desk=desk,
date=datetime.date(2022, 10, 24),
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0),
)
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2022, 10, 24))
assert len(hours) == 1
assert hours[0].begin.time() == datetime.time(9, 0)
assert hours[0].end.time() == datetime.time(12, 0)
# and afternoon
TimePeriod.objects.create(
desk=desk,
date=datetime.date(2022, 10, 24),
start_time=datetime.time(14, 0),
end_time=datetime.time(17, 0),
)
set_prefetched_exceptions(desk)
previous_hours = hours
hours = desk.get_opening_hours(datetime.date(2022, 10, 24))
assert len(hours) == 2
assert hours[0] == previous_hours[0]
assert hours[1].begin.time() == datetime.time(14, 0)
assert hours[1].end.time() == datetime.time(17, 0)
# mix with repeating period
TimePeriod.objects.create(
desk=desk,
weekday=0,
start_time=datetime.time(19, 0),
end_time=datetime.time(20, 0),
)
previous_hours = hours
hours = desk.get_opening_hours(datetime.date(2022, 10, 24))
assert len(hours) == 3
assert hours[:2] == previous_hours[:2]
assert hours[2].begin.time() == datetime.time(19, 0)
assert hours[2].end.time() == datetime.time(20, 0)
# full day exception
TimePeriodException.objects.create(
desk=desk,
start_datetime=make_aware(datetime.datetime(2022, 10, 24)),
end_datetime=make_aware(datetime.datetime(2022, 10, 25)),
)
set_prefetched_exceptions(desk)
hours = desk.get_opening_hours(datetime.date(2022, 10, 24))
assert len(hours) == 0
# next week
hours = desk.get_opening_hours(datetime.date(2022, 10, 31))
assert len(hours) == 1
assert hours[0].begin.time() == datetime.time(19, 0)
assert hours[0].end.time() == datetime.time(20, 0)
def test_timeperiod_midnight_overlap_time_slots():
# https://dev.entrouvert.org/issues/29142
agenda = Agenda(label='Foo bar', slug='bar')
agenda.save()
desk = Desk.objects.create(label='Desk 1', agenda=agenda)
meeting_type = MeetingType(duration=120, agenda=agenda)
meeting_type.save()
timeperiod = TimePeriod(
desk=desk, weekday=0, start_time=datetime.time(21, 0), end_time=datetime.time(23, 0)
)
events = timeperiod.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
events = sorted(events)
assert events[0].timetuple()[:5] == (2016, 9, 5, 21, 0)
assert events[1].timetuple()[:5] == (2016, 9, 12, 21, 0)
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
def test_time_period_check_constraint():
TimePeriod.objects.create(
weekday=0, start_time=datetime.time(hour=1, minute=0), end_time=datetime.time(hour=2, minute=0)
)
TimePeriod.objects.create(
date=datetime.date.today(),
start_time=datetime.time(hour=1, minute=0),
end_time=datetime.time(hour=2, minute=0),
)
# missing weekday or date
with pytest.raises(IntegrityError):
with transaction.atomic():
TimePeriod.objects.create(
start_time=datetime.time(hour=1, minute=0), end_time=datetime.time(hour=2, minute=0)
)
# both weekday and date
with pytest.raises(IntegrityError):
with transaction.atomic():
TimePeriod.objects.create(
weekday=0,
date=datetime.date.today(),
start_time=datetime.time(hour=1, minute=0),
end_time=datetime.time(hour=2, minute=0),
)
def test_timeperiod_date_time_slots():
agenda = Agenda(label='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,
date=datetime.date(2022, 10, 24),
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0),
)
events = timeperiod.as_shared_timeperiods().get_time_slots(
min_datetime=make_aware(datetime.datetime(2022, 10, 1)),
max_datetime=make_aware(datetime.datetime(2022, 11, 1)),
meeting_duration=meeting_type.duration,
base_duration=agenda.get_base_meeting_duration(),
)
assert [x.timetuple()[:5] for x in sorted(events)] == [
(2022, 10, 24, 9, 0),
(2022, 10, 24, 10, 0),
(2022, 10, 24, 11, 0),
]