3638 lines
147 KiB
Python
3638 lines
147 KiB
Python
import datetime
|
||
import json
|
||
import smtplib
|
||
from unittest import mock
|
||
|
||
import pytest
|
||
import requests
|
||
from django.contrib.auth.models import Group, User
|
||
from django.core.files.base import ContentFile
|
||
from django.core.management import call_command
|
||
from django.db import IntegrityError, connection, transaction
|
||
from django.db.models import Q
|
||
from django.test import override_settings
|
||
|
||
from chrono.agendas.models import (
|
||
Agenda,
|
||
AgendaNotificationsSettings,
|
||
AgendaReminderSettings,
|
||
Booking,
|
||
Category,
|
||
Desk,
|
||
Event,
|
||
EventCancellationReport,
|
||
ICSError,
|
||
MeetingType,
|
||
Person,
|
||
Resource,
|
||
SharedCustodyAgenda,
|
||
SharedCustodyHolidayRule,
|
||
SharedCustodyPeriod,
|
||
SharedCustodyRule,
|
||
TimePeriod,
|
||
TimePeriodException,
|
||
TimePeriodExceptionGroup,
|
||
TimePeriodExceptionSource,
|
||
UnavailabilityCalendar,
|
||
VirtualMember,
|
||
)
|
||
from chrono.utils.timezone import localtime, make_aware, make_naive, now
|
||
|
||
pytestmark = pytest.mark.django_db
|
||
|
||
ICS_SAMPLE = """BEGIN:VCALENDAR
|
||
VERSION:2.0
|
||
PRODID:-//foo.bar//EN
|
||
BEGIN:VEVENT
|
||
DTSTAMP:20170824T082855Z
|
||
DTSTART:20170831T170800Z
|
||
DTEND:20170831T203400Z
|
||
SEQUENCE:1
|
||
SUMMARY:Événement 1
|
||
END:VEVENT
|
||
BEGIN:VEVENT
|
||
DTSTAMP:20170824T092855Z
|
||
DTSTART:20170830T180800Z
|
||
DTEND:20170831T223400Z
|
||
SEQUENCE:2
|
||
END:VEVENT
|
||
END:VCALENDAR"""
|
||
|
||
ICS_SAMPLE_WITH_DURATION = """BEGIN:VCALENDAR
|
||
VERSION:2.0
|
||
PRODID:-//foo.bar//EN
|
||
BEGIN:VEVENT
|
||
DTSTAMP:20170824T082855Z
|
||
DTSTART:20170831T170800Z
|
||
DURATION:PT3H26M
|
||
SEQUENCE:1
|
||
SUMMARY:Event 1
|
||
END:VEVENT
|
||
BEGIN:VEVENT
|
||
DTSTAMP:20170824T092855Z
|
||
DTSTART:20170830T180800Z
|
||
DURATION:P1D4H26M
|
||
SEQUENCE:2
|
||
SUMMARY:Event 2
|
||
END:VEVENT
|
||
END:VCALENDAR"""
|
||
|
||
ICS_SAMPLE_WITH_RECURRENT_EVENT = """BEGIN:VCALENDAR
|
||
VERSION:2.0
|
||
PRODID:-//foo.bar//EN
|
||
BEGIN:VEVENT
|
||
DTSTAMP:20170720T145803Z
|
||
DESCRIPTION:Vacances d'ete
|
||
DTSTART;VALUE=DATE:20180101
|
||
DTEND;VALUE=DATE:20180101
|
||
SUMMARY:reccurent event
|
||
END:VEVENT
|
||
BEGIN:VEVENT
|
||
DTSTAMP:20180824T082855Z
|
||
DTSTART:20180101
|
||
DTEND:20180101
|
||
SUMMARY:New Year's Eve
|
||
RRULE:FREQ=YEARLY
|
||
END:VEVENT
|
||
END:VCALENDAR"""
|
||
|
||
ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST = """BEGIN:VCALENDAR
|
||
VERSION:2.0
|
||
PRODID:-//foo.bar//EN
|
||
BEGIN:VEVENT
|
||
DTSTAMP:20180824T082855Z
|
||
DTSTART:20120101
|
||
DURATION:PT24H
|
||
SUMMARY:New Year's Eve
|
||
RRULE:FREQ=YEARLY
|
||
END:VEVENT
|
||
END:VCALENDAR"""
|
||
|
||
ICS_SAMPLE_WITH_NO_EVENTS = """BEGIN:VCALENDAR
|
||
VERSION:2.0
|
||
PRODID:-//foo.bar//EN
|
||
END:VCALENDAR"""
|
||
|
||
INVALID_ICS_SAMPLE = """content
|
||
"""
|
||
|
||
|
||
with open('tests/data/atreal.ics') as f:
|
||
ICS_ATREAL = f.read()
|
||
|
||
|
||
with open('tests/data/holidays.ics') as f:
|
||
ICS_HOLIDAYS = f.read()
|
||
|
||
|
||
def test_slug():
|
||
agenda = Agenda(label='Foo bar')
|
||
agenda.save()
|
||
assert agenda.slug == 'foo-bar'
|
||
|
||
|
||
def test_existing_slug():
|
||
agenda = Agenda(label='Foo bar', slug='bar')
|
||
agenda.save()
|
||
assert agenda.slug == 'bar'
|
||
|
||
|
||
def test_duplicate_slugs():
|
||
agenda = Agenda(label='Foo baz')
|
||
agenda.save()
|
||
assert agenda.slug == 'foo-baz'
|
||
agenda = Agenda(label='Foo baz')
|
||
agenda.save()
|
||
assert agenda.slug == 'foo-baz-1'
|
||
agenda = Agenda(label='Foo baz')
|
||
agenda.save()
|
||
assert agenda.slug == 'foo-baz-2'
|
||
|
||
|
||
def test_resource_slug():
|
||
resource = Resource.objects.create(label='Foo bar')
|
||
assert resource.slug == 'foo-bar'
|
||
|
||
|
||
def test_resource_existing_slug():
|
||
resource = Resource.objects.create(label='Foo bar', slug='bar')
|
||
assert resource.slug == 'bar'
|
||
|
||
|
||
def test_resource_duplicate_slugs():
|
||
resource = Resource.objects.create(label='Foo baz')
|
||
assert resource.slug == 'foo-baz'
|
||
resource = Resource.objects.create(label='Foo baz')
|
||
assert resource.slug == 'foo-baz-1'
|
||
resource = Resource.objects.create(label='Foo baz')
|
||
assert resource.slug == 'foo-baz-2'
|
||
|
||
|
||
def test_category_slug():
|
||
category = Category.objects.create(label='Foo bar')
|
||
assert category.slug == 'foo-bar'
|
||
|
||
|
||
def test_category_existing_slug():
|
||
category = Category.objects.create(label='Foo bar', slug='bar')
|
||
assert category.slug == 'bar'
|
||
|
||
|
||
def test_category_duplicate_slugs():
|
||
category = Category.objects.create(label='Foo baz')
|
||
assert category.slug == 'foo-baz'
|
||
category = Category.objects.create(label='Foo baz')
|
||
assert category.slug == 'foo-baz-1'
|
||
category = Category.objects.create(label='Foo baz')
|
||
assert category.slug == 'foo-baz-2'
|
||
|
||
|
||
def test_agenda_minimal_booking_delay(freezer):
|
||
freezer.move_to('2021-07-09')
|
||
agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=4)
|
||
assert agenda.min_booking_datetime == make_aware(datetime.datetime(2021, 7, 13, 0, 0, 0))
|
||
|
||
freezer.move_to('2022-03-18')
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 22, 0, 0, 0))
|
||
|
||
# DST change on sunday
|
||
freezer.move_to('2022-03-25')
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 29, 0, 0, 0))
|
||
|
||
# DST change on sunday
|
||
freezer.move_to('2021-10-29')
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime == make_aware(datetime.datetime(2021, 11, 2, 0, 0, 0))
|
||
|
||
|
||
def test_agenda_minimal_booking_delay_in_working_days(settings, freezer):
|
||
freezer.move_to('2021-07-09')
|
||
agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=1)
|
||
|
||
settings.WORKING_DAY_CALENDAR = None
|
||
agenda.minimal_booking_delay_in_working_days = True
|
||
agenda.save()
|
||
assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
|
||
agenda.minimal_booking_delay_in_working_days = False
|
||
agenda.save()
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
|
||
|
||
settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France'
|
||
agenda.minimal_booking_delay_in_working_days = True
|
||
agenda.save()
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 12)
|
||
agenda.minimal_booking_delay_in_working_days = False
|
||
agenda.save()
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
|
||
|
||
agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=2)
|
||
agenda.minimal_booking_delay_in_working_days = True
|
||
agenda.save()
|
||
|
||
freezer.move_to('2022-03-18')
|
||
assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 22, 0, 0, 0))
|
||
|
||
# DST change on sunday
|
||
freezer.move_to('2022-03-25')
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 29, 0, 0, 0))
|
||
|
||
# DST change on sunday, and first of november is off
|
||
freezer.move_to('2021-10-29')
|
||
del agenda.min_booking_datetime
|
||
assert agenda.min_booking_datetime == make_aware(datetime.datetime(2021, 11, 3, 0, 0, 0))
|
||
|
||
|
||
def test_agenda_maximal_booking_delay(freezer):
|
||
freezer.move_to('2021-07-09')
|
||
agenda = Agenda.objects.create(label='Agenda', maximal_booking_delay=4)
|
||
assert agenda.max_booking_datetime == make_aware(datetime.datetime(2021, 7, 13, 0, 0, 0))
|
||
|
||
freezer.move_to('2022-03-18')
|
||
del agenda.max_booking_datetime
|
||
assert agenda.max_booking_datetime == make_aware(datetime.datetime(2022, 3, 22, 0, 0, 0))
|
||
|
||
# DST change on sunday
|
||
freezer.move_to('2022-03-25')
|
||
del agenda.max_booking_datetime
|
||
assert agenda.max_booking_datetime == make_aware(datetime.datetime(2022, 3, 29, 0, 0, 0))
|
||
|
||
# DST change on sunday
|
||
freezer.move_to('2021-10-29')
|
||
del agenda.max_booking_datetime
|
||
assert agenda.max_booking_datetime == make_aware(datetime.datetime(2021, 11, 2, 0, 0, 0))
|
||
|
||
|
||
def delay_parameter_to_label(argvalue):
|
||
if isinstance(argvalue, str):
|
||
return argvalue
|
||
return repr(argvalue)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'current_time,min_booking_datetime',
|
||
[
|
||
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
|
||
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 7)),
|
||
# summer DST change on sunday 28th
|
||
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 29, 1, 30)),
|
||
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 2, 30)),
|
||
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 3, 30)),
|
||
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 1, 30)),
|
||
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 3, 30)),
|
||
# winter DST change on sunday 31th
|
||
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 1, 30)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
|
||
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 1, 30)),
|
||
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 2, 30)),
|
||
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 2, 30)),
|
||
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 3, 30)),
|
||
],
|
||
ids=delay_parameter_to_label,
|
||
)
|
||
def test_agenda_minimal_booking_delay_no_minimal_booking_time(freezer, current_time, min_booking_datetime):
|
||
freezer.move_to(current_time)
|
||
agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=4, minimal_booking_time=None)
|
||
assert make_naive(agenda.min_booking_datetime) == min_booking_datetime
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'current_time,min_booking_datetime',
|
||
[
|
||
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 16, 8)),
|
||
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 24, 7)),
|
||
# summer DST change on sunday 28th
|
||
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 31, 1, 30)),
|
||
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 31, 2, 30)),
|
||
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 31, 3, 30)),
|
||
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 1, 30)),
|
||
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 3, 30)),
|
||
# winter DST change on sunday 31th
|
||
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 5, 1, 30)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 5, 2, 30)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 5, 2, 30)),
|
||
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 5, 1, 30)),
|
||
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 5, 2, 30)),
|
||
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 5, 2, 30)),
|
||
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 5, 3, 30)),
|
||
],
|
||
ids=delay_parameter_to_label,
|
||
)
|
||
def test_agenda_minimal_booking_delay_in_working_days_no_minimal_booking_time(
|
||
settings, freezer, current_time, min_booking_datetime
|
||
):
|
||
settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France'
|
||
freezer.move_to(current_time)
|
||
agenda = Agenda.objects.create(
|
||
label='Agenda',
|
||
minimal_booking_delay=4,
|
||
minimal_booking_time=None,
|
||
minimal_booking_delay_in_working_days=True,
|
||
)
|
||
assert make_naive(agenda.min_booking_datetime) == min_booking_datetime
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'current_time,min_booking_datetime',
|
||
[
|
||
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
|
||
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 8)),
|
||
# summer DST change on sunday
|
||
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
|
||
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
|
||
# winter DST change on sunday
|
||
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
|
||
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
|
||
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
|
||
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
|
||
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
|
||
],
|
||
ids=delay_parameter_to_label,
|
||
)
|
||
def test_agenda_minimal_booking_delay_minimal_booking_time_at_8(freezer, current_time, min_booking_datetime):
|
||
freezer.move_to(current_time)
|
||
agenda = Agenda.objects.create(
|
||
label='Agenda', minimal_booking_delay=4, minimal_booking_time=datetime.time(8)
|
||
)
|
||
assert make_naive(agenda.min_booking_datetime) == min_booking_datetime
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'current_time,max_booking_datetime',
|
||
[
|
||
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
|
||
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 7)),
|
||
# summer DST change on sunday 28th
|
||
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 29, 1, 30)),
|
||
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 2, 30)),
|
||
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 3, 30)),
|
||
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 1, 30)),
|
||
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 3, 30)),
|
||
# winter DST change on sunday 31th
|
||
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 1, 30)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
|
||
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 1, 30)),
|
||
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 2, 30)),
|
||
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 2, 30)),
|
||
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 3, 30)),
|
||
],
|
||
ids=delay_parameter_to_label,
|
||
)
|
||
def test_agenda_maximal_booking_delay_no_minimal_booking_time(freezer, current_time, max_booking_datetime):
|
||
freezer.move_to(current_time)
|
||
agenda = Agenda.objects.create(label='Agenda', maximal_booking_delay=4, minimal_booking_time=None)
|
||
assert make_naive(agenda.max_booking_datetime) == max_booking_datetime
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'current_time,max_booking_datetime',
|
||
[
|
||
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
|
||
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 8)),
|
||
# summer DST change on sunday
|
||
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
|
||
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
|
||
# winter DST change on sunday
|
||
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
|
||
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
|
||
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
|
||
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
|
||
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
|
||
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
|
||
],
|
||
ids=delay_parameter_to_label,
|
||
)
|
||
def test_agenda_maximal_booking_delay_minimal_booking_time_at_8(freezer, current_time, max_booking_datetime):
|
||
freezer.move_to(current_time)
|
||
agenda = Agenda.objects.create(
|
||
label='Agenda', maximal_booking_delay=4, minimal_booking_time=datetime.time(8)
|
||
)
|
||
assert make_naive(agenda.max_booking_datetime) == max_booking_datetime
|
||
|
||
|
||
@pytest.mark.parametrize('with_prefetch', [True, False])
|
||
def test_agenda_is_available_for_simple_management(settings, with_prefetch):
|
||
settings.EXCEPTIONS_SOURCES = {
|
||
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
|
||
}
|
||
|
||
def check_is_available(result, use_prefetch=True):
|
||
agenda = Agenda.objects.get()
|
||
if with_prefetch and use_prefetch:
|
||
agenda.prefetch_desks_and_exceptions(with_sources=True, min_date=now())
|
||
assert agenda.is_available_for_simple_management() == result
|
||
|
||
agenda = Agenda.objects.create(label='Agenda', kind='meetings')
|
||
# no desks
|
||
check_is_available(True)
|
||
|
||
# check kind
|
||
agenda.kind = 'events'
|
||
agenda.save()
|
||
check_is_available(False, use_prefetch=False)
|
||
agenda.kind = 'virtual'
|
||
agenda.save()
|
||
check_is_available(False, use_prefetch=False)
|
||
|
||
# only one desk
|
||
agenda.kind = 'meetings'
|
||
agenda.save()
|
||
desk = Desk.objects.create(label='Desk', agenda=agenda)
|
||
check_is_available(True)
|
||
|
||
# create some related data for this desk
|
||
time_period = TimePeriod.objects.create(
|
||
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
|
||
)
|
||
desk.import_timeperiod_exceptions_from_settings(enable=True)
|
||
source1 = desk.timeperiodexceptionsource_set.get(settings_slug='holidays')
|
||
source2 = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
|
||
source3 = TimePeriodExceptionSource.objects.create(
|
||
desk=desk,
|
||
ics_filename='sample.ics',
|
||
ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'),
|
||
)
|
||
date_now = now()
|
||
exception = TimePeriodException.objects.create(
|
||
label='Exception',
|
||
desk=desk,
|
||
start_datetime=date_now + datetime.timedelta(days=1),
|
||
end_datetime=date_now + datetime.timedelta(days=2),
|
||
)
|
||
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
unavailability_calendar.desks.add(desk)
|
||
unavailability_calendar2 = UnavailabilityCalendar.objects.create(label='Calendar 2')
|
||
|
||
# still ok
|
||
check_is_available(True)
|
||
|
||
# duplicate the desk twice
|
||
desk2 = desk.duplicate()
|
||
desk.duplicate()
|
||
|
||
# still ok
|
||
check_is_available(True)
|
||
|
||
# changes on time periods
|
||
for _desk in [desk, desk2]:
|
||
time_period2 = TimePeriod.objects.create(
|
||
weekday=2, desk=_desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
|
||
)
|
||
check_is_available(False)
|
||
time_period2.delete()
|
||
check_is_available(True)
|
||
time_period.weekday = 2
|
||
time_period.save()
|
||
check_is_available(False)
|
||
time_period.weekday = 1
|
||
time_period.start_time = datetime.time(10, 1)
|
||
time_period.save()
|
||
check_is_available(False)
|
||
time_period.start_time = datetime.time(10, 0)
|
||
time_period.end_time = datetime.time(12, 1)
|
||
time_period.save()
|
||
check_is_available(False)
|
||
time_period.end_time = datetime.time(12, 0)
|
||
time_period.save()
|
||
check_is_available(True)
|
||
|
||
# changes on exceptions
|
||
for _desk in [desk, desk2]:
|
||
exception2 = TimePeriodException.objects.create(
|
||
label='Exception',
|
||
desk=_desk,
|
||
start_datetime=date_now + datetime.timedelta(days=3),
|
||
end_datetime=date_now + datetime.timedelta(days=4),
|
||
)
|
||
check_is_available(False)
|
||
exception2.delete()
|
||
check_is_available(True)
|
||
exception.label = 'Exception blah'
|
||
exception.save()
|
||
check_is_available(False)
|
||
exception.label = 'Exception'
|
||
exception.start_datetime = date_now + datetime.timedelta(days=3)
|
||
exception.save()
|
||
check_is_available(False)
|
||
exception.start_datetime = date_now + datetime.timedelta(days=1)
|
||
exception.end_datetime = date_now + datetime.timedelta(days=1)
|
||
exception.save()
|
||
check_is_available(False)
|
||
exception.end_datetime = date_now + datetime.timedelta(days=2)
|
||
exception.save()
|
||
check_is_available(True)
|
||
# exceptions from source are not checked
|
||
exception3 = TimePeriodException.objects.create(
|
||
label='Exception',
|
||
desk=desk,
|
||
start_datetime=date_now + datetime.timedelta(days=3),
|
||
end_datetime=date_now + datetime.timedelta(days=4),
|
||
source=source2,
|
||
)
|
||
check_is_available(True)
|
||
exception3.delete()
|
||
|
||
# changes on sources - from settings
|
||
for _desk in [desk, desk2]:
|
||
source = TimePeriodExceptionSource.objects.create(
|
||
desk=_desk, settings_slug='holidays-bis', enabled=True
|
||
)
|
||
check_is_available(False)
|
||
source.delete()
|
||
check_is_available(True)
|
||
source1.enabled = False
|
||
source1.save()
|
||
check_is_available(False)
|
||
source1.enabled = True
|
||
source1.settings_slug = 'holidays-bis'
|
||
source1.save()
|
||
check_is_available(False)
|
||
source1.settings_slug = 'holidays'
|
||
source1.save()
|
||
check_is_available(True)
|
||
|
||
# changes on sources - from url
|
||
for _desk in [desk, desk2]:
|
||
source = TimePeriodExceptionSource.objects.create(
|
||
desk=_desk, ics_url='http://example.com/sample-bis.ics'
|
||
)
|
||
check_is_available(False)
|
||
source.delete()
|
||
check_is_available(True)
|
||
source2.ics_url = 'http://example.com/sample-bis.ics'
|
||
source2.save()
|
||
check_is_available(False)
|
||
source2.ics_url = 'http://example.com/sample.ics'
|
||
source2.save()
|
||
check_is_available(True)
|
||
|
||
# changes on sources - from file
|
||
for _desk in [desk, desk2]:
|
||
source = TimePeriodExceptionSource.objects.create(
|
||
desk=_desk,
|
||
ics_filename='sample-bis.ics',
|
||
ics_file=ContentFile(ICS_SAMPLE, name='sample-bis.ics'),
|
||
)
|
||
check_is_available(False)
|
||
source.delete()
|
||
check_is_available(True)
|
||
source3.ics_filename = 'sample-bis.ics'
|
||
source3.save()
|
||
check_is_available(False)
|
||
source3.ics_filename = 'sample.ics'
|
||
source3.save()
|
||
check_is_available(True)
|
||
# ics_file content is not checked
|
||
|
||
# changes on unavailability calendars
|
||
for _desk in [desk, desk2]:
|
||
unavailability_calendar2.desks.add(_desk)
|
||
check_is_available(False)
|
||
unavailability_calendar2.desks.remove(_desk)
|
||
check_is_available(True)
|
||
|
||
|
||
def test_event_slug():
|
||
other_agenda = Agenda.objects.create(label='Foo bar')
|
||
Event.objects.create(agenda=other_agenda, places=42, start_datetime=now(), slug='foo-bar')
|
||
|
||
agenda = Agenda.objects.create(label='Foo baz')
|
||
event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar')
|
||
assert event.slug == 'foo-bar'
|
||
event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar')
|
||
assert event.slug == 'foo-bar-1'
|
||
event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar')
|
||
assert event.slug == 'foo-bar-2'
|
||
|
||
event = Event.objects.create(agenda=agenda, places=42, start_datetime=now())
|
||
assert event.slug == 'foo-baz-event'
|
||
event = Event.objects.create(agenda=agenda, places=42, start_datetime=now())
|
||
assert event.slug == 'foo-baz-event-1'
|
||
event = Event.objects.create(agenda=agenda, places=42, start_datetime=now())
|
||
assert event.slug == 'foo-baz-event-2'
|
||
|
||
|
||
def test_event_existing_slug():
|
||
other_agenda = Agenda.objects.create(label='Foo bar')
|
||
Event.objects.create(agenda=other_agenda, places=42, start_datetime=now(), slug='bar')
|
||
|
||
agenda = Agenda.objects.create(label='Foo baz')
|
||
event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar', slug='bar')
|
||
assert event.slug == 'bar'
|
||
|
||
|
||
def test_event_manager():
|
||
agenda = Agenda(label='Foo baz')
|
||
agenda.save()
|
||
event = Event(start_datetime=now(), places=10, agenda=agenda)
|
||
event.save()
|
||
booking = Booking(event=event)
|
||
booking.save()
|
||
assert Event.objects.all()[0].booked_places == 1
|
||
booking.cancellation_datetime = now()
|
||
booking.save()
|
||
assert Event.objects.all()[0].booked_places == 0
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'start_days, start_minutes, min_delay, max_delay, pub_days, expected',
|
||
[
|
||
# no delay
|
||
(10, 0, 0, 0, None, True),
|
||
# test publication_datetime
|
||
(10, 0, 0, 0, 1, False),
|
||
(10, 0, 0, 0, 0, True),
|
||
# test min and max delays
|
||
(10, 0, 20, 0, None, False),
|
||
(10, 0, 1, 5, None, False),
|
||
(10, 0, 1, 20, None, True),
|
||
# special case for events that happens today
|
||
(0, 10, 0, 20, None, True),
|
||
(0, -10, 0, 20, None, False),
|
||
],
|
||
)
|
||
def test_event_bookable_period(start_days, start_minutes, min_delay, max_delay, pub_days, expected):
|
||
agenda = Agenda.objects.create(
|
||
label='Foo bar', minimal_booking_delay=min_delay, maximal_booking_delay=max_delay
|
||
)
|
||
event = Event.objects.create(
|
||
start_datetime=localtime() + datetime.timedelta(days=start_days, minutes=start_minutes),
|
||
publication_datetime=(localtime() + datetime.timedelta(days=pub_days)) if pub_days else None,
|
||
places=10,
|
||
agenda=agenda,
|
||
)
|
||
assert event.in_bookable_period() == expected
|
||
|
||
|
||
def test_meeting_type_slugs():
|
||
agenda1 = Agenda(label='Foo bar')
|
||
agenda1.save()
|
||
agenda2 = Agenda(label='Foo bar second')
|
||
agenda2.save()
|
||
|
||
meeting_type1 = MeetingType(agenda=agenda1, label='Baz')
|
||
meeting_type1.save()
|
||
assert meeting_type1.slug == 'baz'
|
||
|
||
meeting_type2 = MeetingType(agenda=agenda1, label='Baz')
|
||
meeting_type2.save()
|
||
assert meeting_type2.slug == 'baz-1'
|
||
|
||
meeting_type3 = MeetingType(agenda=agenda2, label='Baz')
|
||
meeting_type3.save()
|
||
assert meeting_type3.slug == 'baz'
|
||
|
||
|
||
def test_timeperiodexception_creation_from_ics():
|
||
agenda = Agenda.objects.create(label='Test 1 agenda')
|
||
desk = Desk.objects.create(label='Test 1 desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE, name='sample.ics')
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||
|
||
|
||
def test_timeperiodexception_creation_from_ics_without_startdt():
|
||
agenda = Agenda.objects.create(label='Test 2 agenda')
|
||
desk = Desk.objects.create(label='Test 2 desk', agenda=agenda)
|
||
lines = []
|
||
# remove start datetimes from ics
|
||
for line in ICS_SAMPLE.splitlines():
|
||
if line.startswith('DTSTART:'):
|
||
continue
|
||
lines.append(line)
|
||
ics_sample = ContentFile("\n".join(lines), name='sample.ics')
|
||
source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample)
|
||
with pytest.raises(ICSError) as e:
|
||
source._check_ics_content()
|
||
assert str(e.value) == 'Event "Événement 1" has no start date.'
|
||
|
||
|
||
def test_timeperiodexception_creation_from_ics_without_enddt():
|
||
agenda = Agenda.objects.create(label='Test 3 agenda')
|
||
desk = Desk.objects.create(label='Test 3 desk', agenda=agenda)
|
||
lines = []
|
||
# remove end datetimes from ics
|
||
for line in ICS_SAMPLE.splitlines():
|
||
if line.startswith('DTEND:'):
|
||
continue
|
||
lines.append(line)
|
||
ics_sample = ContentFile("\n".join(lines), name='sample.ics')
|
||
source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
for exception in TimePeriodException.objects.filter(desk=desk):
|
||
end_time = localtime(exception.end_datetime).time()
|
||
assert end_time == datetime.time(23, 59, 59, 999999)
|
||
|
||
|
||
@pytest.mark.freeze_time('2017-12-01')
|
||
def test_timeperiodexception_creation_from_ics_with_recurrences():
|
||
agenda = Agenda.objects.create(label='Test 4 agenda')
|
||
desk = Desk.objects.create(label='Test 4 desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_RECURRENT_EVENT, name='sample.ics')
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 3
|
||
|
||
|
||
def test_timeexception_creation_from_ics_with_dates():
|
||
agenda = Agenda.objects.create(label='Test 5 agenda')
|
||
desk = Desk.objects.create(label='Test 5 desk', agenda=agenda)
|
||
lines = []
|
||
# remove end datetimes from ics
|
||
for line in ICS_SAMPLE_WITH_RECURRENT_EVENT.splitlines():
|
||
if line.startswith('RRULE:'):
|
||
continue
|
||
lines.append(line)
|
||
ics_sample = ContentFile("\n".join(lines), name='sample.ics')
|
||
source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||
for exception in TimePeriodException.objects.filter(desk=desk):
|
||
assert localtime(exception.start_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0))
|
||
assert localtime(exception.end_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0))
|
||
|
||
|
||
def test_timeexception_create_from_invalid_ics():
|
||
agenda = Agenda.objects.create(label='Test 6 agenda')
|
||
desk = Desk.objects.create(label='Test 6 desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics', ics_file=ContentFile(INVALID_ICS_SAMPLE, name='sample.ics')
|
||
)
|
||
with pytest.raises(ICSError) as e:
|
||
source._check_ics_content()
|
||
assert str(e.value) == 'File format is invalid.'
|
||
|
||
|
||
def test_timeexception_create_from_ics_with_no_events():
|
||
agenda = Agenda.objects.create(label='Test 7 agenda')
|
||
desk = Desk.objects.create(label='Test 7 desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_NO_EVENTS, name='sample.ics')
|
||
)
|
||
with pytest.raises(ICSError) as e:
|
||
source._check_ics_content()
|
||
assert str(e.value) == "The file doesn't contain any events."
|
||
|
||
|
||
@mock.patch('chrono.agendas.models.requests.get')
|
||
def test_timeperiodexception_creation_from_remote_ics(mocked_get):
|
||
agenda = Agenda.objects.create(label='Test 8 agenda')
|
||
desk = Desk.objects.create(label='Test 8 desk', agenda=agenda)
|
||
mocked_response = mock.Mock()
|
||
mocked_response.text = ICS_SAMPLE
|
||
mocked_get.return_value = mocked_response
|
||
source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics')
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||
assert 'Événement 1' in [x.label for x in desk.timeperiodexception_set.all()]
|
||
|
||
mocked_response.text = ICS_SAMPLE_WITH_NO_EVENTS
|
||
mocked_get.return_value = mocked_response
|
||
with pytest.raises(ICSError) as e:
|
||
source._check_ics_content()
|
||
assert str(e.value) == "The file doesn't contain any events."
|
||
|
||
|
||
@mock.patch('chrono.agendas.models.requests.get')
|
||
def test_timeperiodexception_remote_ics_encoding(mocked_get):
|
||
agenda = Agenda.objects.create(label='Test 8 agenda')
|
||
desk = Desk.objects.create(label='Test 8 desk', agenda=agenda)
|
||
mocked_response = mock.Mock()
|
||
mocked_response.content = ICS_SAMPLE.encode('iso-8859-15')
|
||
mocked_response.text = ICS_SAMPLE
|
||
mocked_get.return_value = mocked_response
|
||
source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics')
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||
assert 'Événement 1' in [x.label for x in desk.timeperiodexception_set.all()]
|
||
|
||
|
||
@mock.patch('chrono.agendas.models.requests.get')
|
||
def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get):
|
||
agenda = Agenda.objects.create(label='Test 9 agenda')
|
||
desk = Desk.objects.create(label='Test 9 desk', agenda=agenda)
|
||
mocked_response = mock.Mock()
|
||
mocked_response.text = ICS_SAMPLE
|
||
mocked_get.return_value = mocked_response
|
||
|
||
def mocked_requests_connection_error(*args, **kwargs):
|
||
raise requests.ConnectionError('unreachable')
|
||
|
||
source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics')
|
||
mocked_get.side_effect = mocked_requests_connection_error
|
||
with pytest.raises(ICSError) as e:
|
||
source._check_ics_content()
|
||
assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, unreachable)."
|
||
|
||
|
||
@mock.patch('chrono.agendas.models.requests.get')
|
||
def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get):
|
||
agenda = Agenda.objects.create(label='Test 10 agenda')
|
||
desk = Desk.objects.create(label='Test 10 desk', agenda=agenda)
|
||
mocked_response = mock.Mock()
|
||
mocked_response.status_code = 403
|
||
mocked_get.return_value = mocked_response
|
||
|
||
def mocked_requests_http_forbidden_error(*args, **kwargs):
|
||
raise requests.HTTPError(response=mocked_response)
|
||
|
||
source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics')
|
||
mocked_get.side_effect = mocked_requests_http_forbidden_error
|
||
with pytest.raises(ICSError) as e:
|
||
source._check_ics_content()
|
||
assert (
|
||
str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403)."
|
||
)
|
||
|
||
|
||
@mock.patch('chrono.agendas.models.requests.get')
|
||
def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys):
|
||
agenda = Agenda.objects.create(label='Test 11 agenda')
|
||
desk = Desk.objects.create(label='Test 11 desk', agenda=agenda)
|
||
source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
|
||
mocked_response = mock.Mock()
|
||
mocked_response.status_code = 403
|
||
mocked_get.return_value = mocked_response
|
||
|
||
def mocked_requests_http_forbidden_error(*args, **kwargs):
|
||
raise requests.HTTPError(response=mocked_response)
|
||
|
||
mocked_get.side_effect = mocked_requests_http_forbidden_error
|
||
call_command('sync_desks_timeperiod_exceptions')
|
||
err = capsys.readouterr()[1]
|
||
assert (
|
||
err
|
||
== 'unable to create timeperiod exceptions for "Test 11 desk": Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403).\n'
|
||
)
|
||
|
||
assert source.ics_url is not None
|
||
assert source.ics_filename is None
|
||
assert source.ics_file.name is None
|
||
with mock.patch(
|
||
'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics'
|
||
) as refresh:
|
||
call_command('sync_desks_timeperiod_exceptions')
|
||
assert refresh.call_args_list == [mock.call()]
|
||
|
||
source.ics_url = None
|
||
source.ics_filename = 'sample.ics'
|
||
source.ics_file = ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics')
|
||
source.save()
|
||
with mock.patch(
|
||
'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics'
|
||
) as refresh:
|
||
call_command('sync_desks_timeperiod_exceptions')
|
||
assert refresh.call_args_list == [mock.call()]
|
||
|
||
TimePeriodExceptionSource.objects.update(ics_file='')
|
||
with mock.patch(
|
||
'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics'
|
||
) as refresh:
|
||
call_command('sync_desks_timeperiod_exceptions')
|
||
assert refresh.call_args_list == []
|
||
|
||
TimePeriodExceptionSource.objects.update(ics_file=None)
|
||
with mock.patch(
|
||
'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics'
|
||
) as refresh:
|
||
call_command('sync_desks_timeperiod_exceptions')
|
||
assert refresh.call_args_list == []
|
||
|
||
|
||
@override_settings(
|
||
EXCEPTIONS_SOURCES={
|
||
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
|
||
}
|
||
)
|
||
def test_timeperiodexception_from_settings():
|
||
agenda = Agenda(label='Test 1 agenda')
|
||
agenda.save()
|
||
desk = Desk(label='Test 1 desk', agenda=agenda)
|
||
desk.save()
|
||
desk.import_timeperiod_exceptions_from_settings(enable=True)
|
||
|
||
source = TimePeriodExceptionSource.objects.get(desk=desk)
|
||
assert source.settings_slug == 'holidays'
|
||
assert source.enabled
|
||
assert TimePeriodException.objects.filter(desk=desk, source=source).exists()
|
||
|
||
exception = TimePeriodException.objects.first()
|
||
from workalendar.europe import France
|
||
|
||
date, label = France().holidays()[0]
|
||
exception = TimePeriodException.objects.filter(label=label).first()
|
||
assert exception.end_datetime - exception.start_datetime == datetime.timedelta(days=1)
|
||
assert localtime(exception.start_datetime).date() == date
|
||
|
||
source.disable()
|
||
assert not source.enabled
|
||
assert not TimePeriodException.objects.filter(desk=desk, source=source).exists()
|
||
|
||
source.enable()
|
||
assert source.enabled
|
||
assert TimePeriodException.objects.filter(desk=desk, source=source).exists()
|
||
|
||
|
||
def test_timeperiodexception_from_settings_command():
|
||
setting = {
|
||
'EXCEPTIONS_SOURCES': {
|
||
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
|
||
}
|
||
}
|
||
agenda = Agenda(label='Test 1 agenda')
|
||
agenda.save()
|
||
|
||
desk1 = Desk(label='Test 1 desk', agenda=agenda)
|
||
desk1.save()
|
||
assert not TimePeriodExceptionSource.objects.filter(desk=desk1).exists()
|
||
|
||
with override_settings(**setting):
|
||
desk2 = Desk(label='Test 2 desk', agenda=agenda)
|
||
desk2.save()
|
||
desk2.import_timeperiod_exceptions_from_settings(enable=True)
|
||
desk3 = Desk(label='Test 3 desk', agenda=agenda)
|
||
desk3.save()
|
||
desk3.import_timeperiod_exceptions_from_settings(enable=False)
|
||
call_command('sync_desks_timeperiod_exceptions_from_settings')
|
||
assert not TimePeriodExceptionSource.objects.get(desk=desk1).enabled
|
||
source2 = TimePeriodExceptionSource.objects.get(desk=desk2)
|
||
assert source2.enabled
|
||
source3 = TimePeriodExceptionSource.objects.get(desk=desk3)
|
||
assert not source3.enabled
|
||
|
||
exceptions_count = source2.timeperiodexception_set.count()
|
||
# Alsace Moselle has more holidays
|
||
setting['EXCEPTIONS_SOURCES']['holidays']['class'] = 'workalendar.europe.FranceAlsaceMoselle'
|
||
with override_settings(**setting):
|
||
call_command('sync_desks_timeperiod_exceptions_from_settings')
|
||
source2.refresh_from_db()
|
||
assert exceptions_count < source2.timeperiodexception_set.count()
|
||
|
||
setting['LANGUAGE_CODE'] = 'fr-fr'
|
||
with override_settings(**setting):
|
||
call_command('sync_desks_timeperiod_exceptions_from_settings')
|
||
assert not TimePeriodException.objects.filter(label='All Saints Day').exists()
|
||
assert TimePeriodException.objects.filter(label='Toussaint').exists()
|
||
|
||
setting['EXCEPTIONS_SOURCES'] = {}
|
||
with override_settings(**setting):
|
||
call_command('sync_desks_timeperiod_exceptions_from_settings')
|
||
assert not TimePeriodExceptionSource.objects.exists()
|
||
|
||
|
||
def test_timeperiodexception_groups():
|
||
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
source = unavailability_calendar.timeperiodexceptionsource_set.create(
|
||
ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics')
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.count() == 11
|
||
group1, group2 = TimePeriodExceptionGroup.objects.all()
|
||
assert group1.label == 'Vacances de Noël'
|
||
assert group1.slug == 'christmas_holidays'
|
||
assert group2.label == 'Vacances d’Été'
|
||
assert group2.slug == 'summer_holidays'
|
||
|
||
assert group1.exceptions.count() == 6
|
||
assert group2.exceptions.count() == 5
|
||
|
||
unavailability_calendar.delete()
|
||
assert not TimePeriodException.objects.exists()
|
||
assert not TimePeriodExceptionGroup.objects.exists()
|
||
|
||
# check no groups are created for desks
|
||
agenda = Agenda.objects.create(label='Test 1 agenda')
|
||
desk = Desk.objects.create(label='Test 1 desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics')
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.count() == 11
|
||
assert not TimePeriodExceptionGroup.objects.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=0)
|
||
meeting_type.save()
|
||
|
||
with pytest.raises(ValueError):
|
||
agenda.get_base_meeting_duration()
|
||
|
||
meeting_type = MeetingType(agenda=agenda, label='Foo', duration=30)
|
||
meeting_type.save()
|
||
del agenda.__dict__['cached_meetingtypes']
|
||
assert agenda.get_base_meeting_duration() == 30
|
||
|
||
meeting_type = MeetingType(agenda=agenda, label='Bar', duration=60)
|
||
meeting_type.save()
|
||
del agenda.__dict__['cached_meetingtypes']
|
||
assert agenda.get_base_meeting_duration() == 30
|
||
|
||
meeting_type = MeetingType(agenda=agenda, label='Bar', duration=45)
|
||
meeting_type.save()
|
||
del agenda.__dict__['cached_meetingtypes']
|
||
assert agenda.get_base_meeting_duration() == 15
|
||
|
||
|
||
def test_timeperiodexception_creation_from_ics_with_duration():
|
||
# test that event defined using duration works and give the same start and
|
||
# end dates
|
||
agenda = Agenda.objects.create(label='Test 1 agenda')
|
||
desk = Desk.objects.create(label='Test 1 desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics')
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == {
|
||
make_aware(datetime.datetime(2017, 8, 31, 19, 8, 0)),
|
||
make_aware(datetime.datetime(2017, 8, 30, 20, 8, 0)),
|
||
}
|
||
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == {
|
||
make_aware(datetime.datetime(2017, 8, 31, 22, 34, 0)),
|
||
make_aware(datetime.datetime(2017, 9, 1, 0, 34, 0)),
|
||
}
|
||
|
||
|
||
@pytest.mark.freeze_time('2017-12-01')
|
||
def test_timeperiodexception_creation_from_ics_with_recurrences_in_the_past():
|
||
# test that recurrent events before today are not created
|
||
# also test that duration + recurrent events works
|
||
agenda = Agenda.objects.create(label='Test 4 agenda')
|
||
desk = Desk.objects.create(label='Test 4 desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics',
|
||
ics_file=ContentFile(ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST, name='sample.ics'),
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == {
|
||
make_aware(datetime.datetime(2018, 1, 1)),
|
||
make_aware(datetime.datetime(2019, 1, 1)),
|
||
}
|
||
|
||
|
||
def test_timeperiodexception_creation_from_ics_with_recurrences_atreal():
|
||
agenda = Agenda.objects.create(label='Test atreal agenda')
|
||
desk = Desk.objects.create(label='Test atreal desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics', ics_file=ContentFile(ICS_ATREAL, name='sample.ics')
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).exists()
|
||
|
||
|
||
def test_management_role_deletion():
|
||
group = Group(name='Group')
|
||
group.save()
|
||
agenda = Agenda(label='Test agenda', edit_role=group, view_role=group)
|
||
agenda.save()
|
||
|
||
Group.objects.all().delete()
|
||
|
||
assert Agenda.objects.get(id=agenda.id).view_role is None
|
||
assert Agenda.objects.get(id=agenda.id).edit_role is None
|
||
|
||
|
||
def test_virtual_agenda_init():
|
||
agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
|
||
agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
|
||
virt_agenda = Agenda.objects.create(label='Virtual agenda', kind='virtual')
|
||
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
|
||
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
|
||
virt_agenda.save()
|
||
|
||
assert virt_agenda.real_agendas.count() == 2
|
||
assert virt_agenda.real_agendas.get(pk=agenda1.pk)
|
||
assert virt_agenda.real_agendas.get(pk=agenda2.pk)
|
||
|
||
for agenda in (agenda1, agenda2):
|
||
assert agenda.virtual_agendas.count() == 1
|
||
assert agenda.virtual_agendas.get() == virt_agenda
|
||
|
||
|
||
def test_virtual_agenda_base_meeting_duration():
|
||
virt_agenda = Agenda.objects.create(label='Virtual agenda', kind='virtual')
|
||
|
||
with pytest.raises(ValueError):
|
||
virt_agenda.get_base_meeting_duration()
|
||
|
||
agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
|
||
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
|
||
|
||
with pytest.raises(ValueError):
|
||
virt_agenda.get_base_meeting_duration()
|
||
|
||
meeting_type = MeetingType(agenda=agenda1, label='Foo', duration=30)
|
||
meeting_type.save()
|
||
del virt_agenda.__dict__['cached_meetingtypes']
|
||
assert virt_agenda.get_base_meeting_duration() == 30
|
||
|
||
meeting_type = MeetingType(agenda=agenda1, label='Bar', duration=60)
|
||
meeting_type.save()
|
||
del virt_agenda.__dict__['cached_meetingtypes']
|
||
assert virt_agenda.get_base_meeting_duration() == 30
|
||
|
||
agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
|
||
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
|
||
virt_agenda.save()
|
||
|
||
meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60)
|
||
meeting_type.save()
|
||
del virt_agenda.__dict__['cached_meetingtypes']
|
||
assert virt_agenda.get_base_meeting_duration() == 60
|
||
|
||
|
||
def test_agenda_get_effective_time_periods(db):
|
||
real_agenda = Agenda.objects.create(label='Real Agenda', kind='meetings')
|
||
MeetingType.objects.create(agenda=real_agenda, label='MT1')
|
||
desk = Desk.objects.create(label='Real Agenda Desk1', agenda=real_agenda)
|
||
time_period = TimePeriod.objects.create(
|
||
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0), desk=desk
|
||
)
|
||
virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
|
||
VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=real_agenda)
|
||
|
||
# empty exclusion set
|
||
common_timeperiods = list(virtual_agenda.get_effective_time_periods())
|
||
assert len(common_timeperiods) == 1
|
||
common_timeperiod = common_timeperiods[0]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == time_period.start_time
|
||
assert common_timeperiod.end_time == time_period.end_time
|
||
|
||
# exclusions are on a different day
|
||
def exclude_time_periods(time_periods):
|
||
virtual_agenda.excluded_timeperiods.clear()
|
||
for time_period in time_periods:
|
||
time_period.agenda = virtual_agenda
|
||
time_period.save()
|
||
|
||
exclude_time_periods(
|
||
[
|
||
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)),
|
||
]
|
||
)
|
||
common_timeperiods = list(virtual_agenda.get_effective_time_periods())
|
||
assert len(common_timeperiods) == 1
|
||
common_timeperiod = common_timeperiods[0]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == time_period.start_time
|
||
assert common_timeperiod.end_time == time_period.end_time
|
||
|
||
# one exclusion, end_time should be earlier
|
||
exclude_time_periods(
|
||
[TimePeriod(weekday=0, start_time=datetime.time(17, 0), end_time=datetime.time(18, 0))]
|
||
)
|
||
common_timeperiods = list(virtual_agenda.get_effective_time_periods())
|
||
assert len(common_timeperiods) == 1
|
||
common_timeperiod = common_timeperiods[0]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(10, 0)
|
||
assert common_timeperiod.end_time == datetime.time(17, 0)
|
||
|
||
# one exclusion, start_time should be later
|
||
exclude_time_periods(
|
||
[TimePeriod(weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(16, 0))]
|
||
)
|
||
common_timeperiods = list(virtual_agenda.get_effective_time_periods())
|
||
assert len(common_timeperiods) == 1
|
||
common_timeperiod = common_timeperiods[0]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(16, 0)
|
||
assert common_timeperiod.end_time == datetime.time(18, 0)
|
||
|
||
# one exclusion, splits effective timeperiod in two
|
||
exclude_time_periods(
|
||
[TimePeriod(weekday=0, start_time=datetime.time(12, 0), end_time=datetime.time(16, 0))]
|
||
)
|
||
common_timeperiods = list(virtual_agenda.get_effective_time_periods())
|
||
assert len(common_timeperiods) == 2
|
||
common_timeperiod = common_timeperiods[0]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(10, 0)
|
||
assert common_timeperiod.end_time == datetime.time(12, 0)
|
||
common_timeperiod = common_timeperiods[1]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(16, 0)
|
||
assert common_timeperiod.end_time == datetime.time(18, 0)
|
||
|
||
# several exclusion, splits effective timeperiod into pieces
|
||
exclude_time_periods(
|
||
[
|
||
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)),
|
||
]
|
||
)
|
||
common_timeperiods = list(virtual_agenda.get_effective_time_periods())
|
||
assert len(common_timeperiods) == 4
|
||
|
||
common_timeperiod = common_timeperiods[0]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(10, 0)
|
||
assert common_timeperiod.end_time == datetime.time(10, 30)
|
||
|
||
common_timeperiod = common_timeperiods[1]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(11, 30)
|
||
assert common_timeperiod.end_time == datetime.time(12, 0)
|
||
|
||
common_timeperiod = common_timeperiods[2]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(13, 0)
|
||
assert common_timeperiod.end_time == datetime.time(16, 30)
|
||
|
||
common_timeperiod = common_timeperiods[3]
|
||
assert common_timeperiod.weekday == time_period.weekday
|
||
assert common_timeperiod.start_time == datetime.time(17, 0)
|
||
assert common_timeperiod.end_time == datetime.time(18, 0)
|
||
|
||
|
||
def test_agenda_get_effective_time_periods_with_date(db):
|
||
agenda = Agenda.objects.create(label='Test Agenda', kind='meetings')
|
||
MeetingType.objects.create(agenda=agenda, label='MT1')
|
||
desk = Desk.objects.create(label='Test Agenda Desk1', agenda=agenda)
|
||
TimePeriod.objects.create(
|
||
date=datetime.date(2022, 1, 1),
|
||
start_time=datetime.time(10, 0),
|
||
end_time=datetime.time(18, 0),
|
||
desk=desk,
|
||
)
|
||
TimePeriod.objects.create(
|
||
date=datetime.date(2022, 1, 8),
|
||
start_time=datetime.time(10, 0),
|
||
end_time=datetime.time(18, 0),
|
||
desk=desk,
|
||
)
|
||
|
||
assert len(list(agenda.get_effective_time_periods())) == 2
|
||
|
||
|
||
def test_agenda_get_effective_time_periods_with_weekday_indexes(db):
|
||
agenda = Agenda.objects.create(label='Test Agenda', kind='meetings')
|
||
MeetingType.objects.create(agenda=agenda, label='MT1')
|
||
desk = Desk.objects.create(label='Test Agenda Desk1', agenda=agenda)
|
||
TimePeriod.objects.create(
|
||
weekday=0,
|
||
weekday_indexes=[1],
|
||
start_time=datetime.time(10, 0),
|
||
end_time=datetime.time(18, 0),
|
||
desk=desk,
|
||
)
|
||
TimePeriod.objects.create(
|
||
weekday=0,
|
||
weekday_indexes=[2],
|
||
start_time=datetime.time(10, 0),
|
||
end_time=datetime.time(18, 0),
|
||
desk=desk,
|
||
)
|
||
|
||
assert len(list(agenda.get_effective_time_periods())) == 2
|
||
|
||
|
||
def test_exception_read_only():
|
||
agenda = Agenda.objects.create(label='Agenda', kind='meetings')
|
||
desk = Desk.objects.create(agenda=agenda, label='Desk')
|
||
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
source1 = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
|
||
source2 = TimePeriodExceptionSource.objects.create(
|
||
desk=desk,
|
||
ics_filename='sample.ics',
|
||
ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'),
|
||
)
|
||
source3 = TimePeriodExceptionSource.objects.create(
|
||
desk=desk, settings_slug='slug', settings_label='Foo Bar', enabled=True
|
||
)
|
||
|
||
exception = TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=now() - datetime.timedelta(days=2),
|
||
end_datetime=now() - datetime.timedelta(days=1),
|
||
)
|
||
assert exception.read_only is False
|
||
|
||
for source in [source1, source2, source3]:
|
||
exception.source = source
|
||
exception.save()
|
||
assert exception.read_only is True
|
||
|
||
exception.source = None
|
||
exception.desk = None
|
||
exception.unavailability_calendar = unavailability_calendar
|
||
exception.save()
|
||
assert exception.read_only is True
|
||
|
||
|
||
def test_desk_exceptions_within_two_weeks():
|
||
def set_prefetched_exceptions(desk):
|
||
desk.prefetched_exceptions = TimePeriodException.objects.filter(
|
||
Q(desk=desk) | Q(unavailability_calendar__desks=desk)
|
||
)
|
||
|
||
agenda = Agenda.objects.create(label='Agenda', kind='meetings')
|
||
desk = Desk.objects.create(agenda=agenda, label='Desk')
|
||
|
||
# no exception
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == []
|
||
|
||
# exception ends in the past
|
||
exception = TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=now() - datetime.timedelta(days=2),
|
||
end_datetime=now() - datetime.timedelta(days=1),
|
||
)
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == []
|
||
|
||
# exception ends in the future - 14 days
|
||
exception.end_datetime = now() + datetime.timedelta(days=10)
|
||
# but starts in the past
|
||
exception.start_datetime = now() - datetime.timedelta(days=2)
|
||
exception.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception]
|
||
|
||
# exception ends in the future - 14 days
|
||
exception.end_datetime = now() + datetime.timedelta(days=10)
|
||
# but starts in the future
|
||
exception.start_datetime = now() + datetime.timedelta(days=2)
|
||
exception.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception]
|
||
|
||
# exception ends in the future + 14 days
|
||
exception.end_datetime = now() + datetime.timedelta(days=20)
|
||
# but starts in the past
|
||
exception.start_datetime = now() - datetime.timedelta(days=2)
|
||
exception.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception]
|
||
|
||
# create another one, very far way from now
|
||
exception2 = TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=now() + datetime.timedelta(days=200),
|
||
end_datetime=now() + datetime.timedelta(days=201),
|
||
)
|
||
|
||
# exception ends in the future + 14 days
|
||
exception.end_datetime = now() + datetime.timedelta(days=20)
|
||
# but starts in the future - 14 days
|
||
exception.start_datetime = now() + datetime.timedelta(days=2)
|
||
exception.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception]
|
||
|
||
# exception ends in the future + 14 days
|
||
exception.end_datetime = now() + datetime.timedelta(days=20)
|
||
# but starts in the future + 14 days
|
||
exception.start_datetime = now() + datetime.timedelta(days=21)
|
||
exception.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception]
|
||
|
||
# exception in the past
|
||
exception.end_datetime = now() - datetime.timedelta(days=20)
|
||
exception.start_datetime = now() - datetime.timedelta(days=21)
|
||
exception.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception2]
|
||
|
||
# check ordering of the queryset: exception is after exception2
|
||
exception.start_datetime = now() + datetime.timedelta(days=10)
|
||
exception.end_datetime = now() + datetime.timedelta(days=11)
|
||
exception.save()
|
||
exception2.start_datetime = now() + datetime.timedelta(days=5)
|
||
exception2.end_datetime = now() + datetime.timedelta(days=6)
|
||
exception2.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception2, exception]
|
||
|
||
# add an exception from unavailability calendar
|
||
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
exception3 = TimePeriodException.objects.create(
|
||
unavailability_calendar=unavailability_calendar,
|
||
start_datetime=now() + datetime.timedelta(days=2),
|
||
end_datetime=now() + datetime.timedelta(days=3),
|
||
)
|
||
unavailability_calendar.desks.add(desk)
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception3, exception2, exception]
|
||
|
||
# change it
|
||
exception3.start_datetime = now() + datetime.timedelta(days=7)
|
||
exception3.end_datetime = now() + datetime.timedelta(days=8)
|
||
exception3.save()
|
||
set_prefetched_exceptions(desk)
|
||
assert list(desk.get_exceptions_within_two_weeks()) == [exception2, exception3, exception]
|
||
|
||
|
||
def test_desk_duplicate():
|
||
agenda = Agenda.objects.create(label='Agenda')
|
||
desk = Desk.objects.create(label='Desk', agenda=agenda)
|
||
time_period = TimePeriod.objects.create(
|
||
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
|
||
)
|
||
TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
|
||
source2 = TimePeriodExceptionSource.objects.create(
|
||
desk=desk,
|
||
ics_filename='sample.ics',
|
||
ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'),
|
||
)
|
||
time_period_exception = TimePeriodException.objects.create(
|
||
label='Exception',
|
||
desk=desk,
|
||
start_datetime=now() + datetime.timedelta(days=1),
|
||
end_datetime=now() + datetime.timedelta(days=2),
|
||
)
|
||
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
unavailability_calendar.desks.add(desk)
|
||
|
||
new_desk = desk.duplicate(label="New Desk")
|
||
assert new_desk.pk != desk.pk
|
||
assert new_desk.label == 'New Desk'
|
||
assert new_desk.slug == 'new-desk'
|
||
assert new_desk.timeperiod_set.count() == 1
|
||
new_time_period = TimePeriod.objects.get(desk=new_desk)
|
||
assert new_time_period.weekday == time_period.weekday
|
||
assert new_time_period.start_time == time_period.start_time
|
||
assert new_time_period.end_time == time_period.end_time
|
||
assert new_desk.timeperiodexception_set.count() == 1
|
||
new_time_period_exception = TimePeriodException.objects.get(desk=new_desk)
|
||
assert new_time_period_exception.label == time_period_exception.label
|
||
assert new_time_period_exception.start_datetime == time_period_exception.start_datetime
|
||
assert new_time_period_exception.end_datetime == time_period_exception.end_datetime
|
||
assert new_desk.timeperiodexceptionsource_set.count() == 2
|
||
assert TimePeriodExceptionSource.objects.filter(
|
||
desk=new_desk, ics_url='http://example.com/sample.ics'
|
||
).exists()
|
||
new_source2 = TimePeriodExceptionSource.objects.get(desk=new_desk, ics_filename='sample.ics')
|
||
assert new_source2.ics_file.path != source2.ics_file.path
|
||
assert new_desk.unavailability_calendars.count() == 1
|
||
assert new_desk.unavailability_calendars.get() == unavailability_calendar
|
||
|
||
# duplicate again !
|
||
new_desk = desk.duplicate(label="New Desk")
|
||
assert new_desk.slug == 'new-desk-1'
|
||
|
||
|
||
def test_desk_duplicate_exception_sources():
|
||
agenda = Agenda.objects.create(label='Agenda')
|
||
desk = Desk.objects.create(label='Desk', agenda=agenda)
|
||
source = desk.timeperiodexceptionsource_set.create(
|
||
ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE, name='sample.ics')
|
||
)
|
||
source.refresh_timeperiod_exceptions_from_ics()
|
||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||
|
||
new_desk = desk.duplicate(label="New Desk")
|
||
new_source = new_desk.timeperiodexceptionsource_set.get(ics_filename='sample.ics')
|
||
assert new_desk.timeperiodexception_set.count() == 2
|
||
|
||
source.delete()
|
||
assert new_desk.timeperiodexception_set.count() == 2
|
||
|
||
new_source.delete()
|
||
assert not new_desk.timeperiodexception_set.exists()
|
||
|
||
|
||
@override_settings(
|
||
EXCEPTIONS_SOURCES={
|
||
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
|
||
}
|
||
)
|
||
def test_desk_duplicate_exception_source_from_settings():
|
||
agenda = Agenda.objects.create(label='Agenda')
|
||
desk = Desk.objects.create(label='Desk', agenda=agenda)
|
||
desk.import_timeperiod_exceptions_from_settings(enable=True)
|
||
|
||
source = desk.timeperiodexceptionsource_set.get(settings_slug='holidays')
|
||
assert source.enabled
|
||
exceptions_count = desk.timeperiodexception_set.count()
|
||
|
||
new_desk = desk.duplicate(label="New Desk")
|
||
assert new_desk.timeperiodexceptionsource_set.filter(settings_slug='holidays').count() == 1
|
||
assert new_desk.timeperiodexceptionsource_set.get(settings_slug='holidays').enabled
|
||
assert new_desk.timeperiodexception_set.count() == exceptions_count
|
||
|
||
source.disable()
|
||
|
||
new_desk = desk.duplicate(label="New Desk")
|
||
assert not new_desk.timeperiodexceptionsource_set.get(settings_slug='holidays').enabled
|
||
assert not new_desk.timeperiodexception_set.exists()
|
||
|
||
|
||
def test_agenda_meetings_duplicate():
|
||
group = Group(name='Group')
|
||
group.save()
|
||
agenda = Agenda.objects.create(label='Agenda', kind='meetings', view_role=group)
|
||
desk = Desk.objects.create(label='Desk', agenda=agenda)
|
||
meeting_type = MeetingType.objects.create(agenda=agenda, label='meeting', duration=30)
|
||
time_period = TimePeriod.objects.create(
|
||
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
|
||
)
|
||
TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
|
||
source2 = TimePeriodExceptionSource.objects.create(
|
||
desk=desk,
|
||
ics_filename='sample.ics',
|
||
ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'),
|
||
)
|
||
time_period_exception = TimePeriodException.objects.create(
|
||
label='Exception',
|
||
desk=desk,
|
||
start_datetime=now() + datetime.timedelta(days=1),
|
||
end_datetime=now() + datetime.timedelta(days=2),
|
||
)
|
||
resource = Resource.objects.create(label='Foo bar')
|
||
agenda.resources.add(resource)
|
||
|
||
new_agenda = agenda.duplicate()
|
||
assert new_agenda.pk != agenda.pk
|
||
assert new_agenda.label == 'Copy of Agenda'
|
||
assert new_agenda.slug == 'copy-of-agenda'
|
||
assert new_agenda.kind == 'meetings'
|
||
assert new_agenda.view_role == group
|
||
assert new_agenda.resources.first() == resource
|
||
|
||
new_meeting_type = new_agenda.meetingtype_set.first()
|
||
assert new_meeting_type.pk != meeting_type.pk
|
||
assert new_meeting_type.label == meeting_type.label
|
||
assert new_meeting_type.duration == meeting_type.duration
|
||
assert new_meeting_type.slug == meeting_type.slug
|
||
|
||
new_desk = new_agenda.desk_set.first()
|
||
assert new_desk.pk != desk.pk
|
||
assert new_desk.label == desk.label
|
||
assert new_desk.slug == desk.slug
|
||
assert new_desk.timeperiod_set.count() == 1
|
||
new_time_period = TimePeriod.objects.get(desk=new_desk)
|
||
assert new_time_period.weekday == time_period.weekday
|
||
assert new_time_period.start_time == time_period.start_time
|
||
assert new_time_period.end_time == time_period.end_time
|
||
assert new_desk.timeperiodexception_set.count() == 1
|
||
new_time_period_exception = TimePeriodException.objects.get(desk=new_desk)
|
||
assert new_time_period_exception.label == time_period_exception.label
|
||
assert new_time_period_exception.start_datetime == time_period_exception.start_datetime
|
||
assert new_time_period_exception.end_datetime == time_period_exception.end_datetime
|
||
assert new_desk.timeperiodexceptionsource_set.count() == 2
|
||
assert TimePeriodExceptionSource.objects.filter(
|
||
desk=new_desk, ics_url='http://example.com/sample.ics'
|
||
).exists()
|
||
new_source2 = TimePeriodExceptionSource.objects.get(desk=new_desk, ics_filename='sample.ics')
|
||
assert new_source2.ics_file.path != source2.ics_file.path
|
||
|
||
# duplicate again !
|
||
new_agenda = agenda.duplicate()
|
||
assert new_agenda.slug == 'copy-of-agenda-1'
|
||
|
||
|
||
@override_settings(
|
||
EXCEPTIONS_SOURCES={
|
||
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
|
||
}
|
||
)
|
||
def test_agenda_events_duplicate():
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now() + datetime.timedelta(days=1),
|
||
duration=10,
|
||
places=10,
|
||
label='event',
|
||
slug='event',
|
||
)
|
||
desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||
desk.import_timeperiod_exceptions_from_settings(enable=True)
|
||
TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=now(),
|
||
end_datetime=now() + datetime.timedelta(minutes=30),
|
||
)
|
||
assert desk.timeperiodexception_set.count() == 34
|
||
AgendaNotificationsSettings.objects.create(
|
||
agenda=agenda,
|
||
full_event=AgendaNotificationsSettings.EMAIL_FIELD,
|
||
full_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
|
||
)
|
||
AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1, email_extra_info='top')
|
||
|
||
new_agenda = agenda.duplicate()
|
||
assert new_agenda.pk != agenda.pk
|
||
assert new_agenda.kind == 'events'
|
||
assert new_agenda.notifications_settings.full_event == AgendaNotificationsSettings.EMAIL_FIELD
|
||
assert new_agenda.notifications_settings.full_event_emails == ['hop@entrouvert.com', 'top@entrouvert.com']
|
||
assert new_agenda.reminder_settings.days_before_email == 1
|
||
assert new_agenda.reminder_settings.email_extra_info == 'top'
|
||
|
||
new_desk = new_agenda.desk_set.get()
|
||
assert new_desk.slug == '_exceptions_holder'
|
||
assert new_desk.timeperiodexception_set.count() == 34
|
||
assert new_desk.timeperiodexception_set.filter(source__isnull=True).count() == 1
|
||
source = new_desk.timeperiodexceptionsource_set.get()
|
||
assert source.enabled is True
|
||
assert source.settings_slug == 'holidays'
|
||
|
||
new_event = new_agenda.event_set.first()
|
||
assert new_event.pk != event.pk
|
||
assert new_event.label == event.label
|
||
assert new_event.duration == event.duration
|
||
assert new_event.duration == event.duration
|
||
assert new_event.places == event.places
|
||
assert new_event.start_datetime == event.start_datetime
|
||
|
||
|
||
def test_agenda_events_recurrence_duplicate(freezer):
|
||
freezer.move_to('2021-01-06 12:00') # Wednesday
|
||
now_ = now()
|
||
end = now_ + datetime.timedelta(days=15)
|
||
orig_agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
Desk.objects.create(agenda=orig_agenda, slug='_exceptions_holder')
|
||
event = Event.objects.create(
|
||
agenda=orig_agenda,
|
||
start_datetime=now_,
|
||
recurrence_days=[now_.weekday()],
|
||
label='Event',
|
||
places=10,
|
||
recurrence_end_date=end,
|
||
)
|
||
event.create_all_recurrences()
|
||
dup_agenda = orig_agenda.duplicate()
|
||
|
||
for agenda in (orig_agenda, dup_agenda):
|
||
assert agenda.event_set.count() == 4 # one recurring event, 3 real events
|
||
assert agenda.event_set.filter(recurrence_days__isnull=False).count() == 1
|
||
rec_event = agenda.event_set.filter(recurrence_days__isnull=False).first()
|
||
for event in agenda.event_set.filter(recurrence_days__isnull=True):
|
||
assert event.primary_event == rec_event
|
||
|
||
|
||
def test_agenda_virtual_duplicate():
|
||
agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
|
||
agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
|
||
virt_agenda = Agenda.objects.create(label='Virtual agenda', kind='virtual')
|
||
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
|
||
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
|
||
virt_agenda.save()
|
||
|
||
excluded_timeperiod = TimePeriod.objects.create(
|
||
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), agenda=virt_agenda
|
||
)
|
||
|
||
new_agenda = virt_agenda.duplicate()
|
||
assert new_agenda.pk != virt_agenda.pk
|
||
assert new_agenda.kind == 'virtual'
|
||
|
||
new_time_period = new_agenda.excluded_timeperiods.first()
|
||
assert new_time_period.pk != excluded_timeperiod.pk
|
||
assert new_time_period.agenda == new_agenda
|
||
assert new_time_period.weekday == excluded_timeperiod.weekday
|
||
assert new_time_period.start_time == excluded_timeperiod.start_time
|
||
assert new_time_period.end_time == excluded_timeperiod.end_time
|
||
|
||
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists()
|
||
assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists()
|
||
|
||
|
||
def test_agendas_cancel_events_command():
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event')
|
||
|
||
for _ in range(5):
|
||
Booking.objects.create(event=event, cancel_callback_url='http://example.org/jump/trigger/')
|
||
|
||
event.cancellation_scheduled = True
|
||
event.save()
|
||
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
call_command('cancel_events')
|
||
assert mock_send.call_count == 5
|
||
|
||
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 5
|
||
event.refresh_from_db()
|
||
assert not event.cancellation_scheduled
|
||
assert event.cancelled
|
||
|
||
|
||
def test_agendas_cancel_events_command_network_error(freezer):
|
||
freezer.move_to('2020-01-01')
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event')
|
||
|
||
def mocked_requests_connection_error(request, **kwargs):
|
||
if 'good' in request.url:
|
||
return mock.Mock(status_code=200)
|
||
raise requests.exceptions.ConnectionError('unreachable')
|
||
|
||
booking_good_url = Booking.objects.create(event=event, cancel_callback_url='http://good.org/')
|
||
for _ in range(5):
|
||
Booking.objects.create(event=event, cancel_callback_url='http://example.org/jump/trigger/')
|
||
|
||
event.cancellation_scheduled = True
|
||
event.save()
|
||
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
mock_send.side_effect = mocked_requests_connection_error
|
||
call_command('cancel_events')
|
||
assert mock_send.call_count == 6
|
||
|
||
booking_good_url.refresh_from_db()
|
||
assert booking_good_url.cancellation_datetime
|
||
assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 5
|
||
event.refresh_from_db()
|
||
assert not event.cancellation_scheduled
|
||
assert not event.cancelled
|
||
|
||
report = EventCancellationReport.objects.get(event=event)
|
||
assert report.bookings.count() == 5
|
||
assert len(report.booking_errors) == 5
|
||
|
||
for booking in report.bookings.all():
|
||
assert report.booking_errors[str(booking.pk)] == 'unreachable'
|
||
|
||
# old reports are automatically removed
|
||
freezer.move_to('2020-03-01')
|
||
call_command('cancel_events')
|
||
assert not EventCancellationReport.objects.exists()
|
||
|
||
|
||
@mock.patch('django.contrib.auth.models.Group.role', create=True)
|
||
@pytest.mark.parametrize(
|
||
'emails_to_members,emails',
|
||
[
|
||
(False, []),
|
||
(False, ['test@entrouvert.com']),
|
||
(True, []),
|
||
(True, ['test@entrouvert.com']),
|
||
],
|
||
)
|
||
def test_agenda_notifications_role_email(mocked_role, emails_to_members, emails, mailoutbox):
|
||
group = Group.objects.create(name='group')
|
||
user = User.objects.create(username='user', email='user@entrouvert.com')
|
||
user.groups.add(group)
|
||
mocked_role.emails_to_members = emails_to_members
|
||
mocked_role.emails = emails
|
||
expected_recipients = emails
|
||
if emails_to_members:
|
||
expected_recipients.append(user.email)
|
||
expected_email_count = 1 if emails else 0
|
||
|
||
agenda = Agenda.objects.create(label='Foo bar', kind='event', edit_role=group)
|
||
|
||
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
|
||
settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
|
||
settings.almost_full_event = AgendaNotificationsSettings.EDIT_ROLE
|
||
settings.save()
|
||
|
||
# book 9/10 places to reach almost full state
|
||
for _ in range(9):
|
||
Booking.objects.create(event=event)
|
||
event.refresh_from_db()
|
||
assert event.almost_full
|
||
|
||
call_command('send_email_notifications')
|
||
assert len(mailoutbox) == expected_email_count
|
||
if mailoutbox:
|
||
assert mailoutbox[0].recipients() == expected_recipients
|
||
assert mailoutbox[0].subject == 'Alert: event "Hop" is almost full (90%)'
|
||
assert 'manage/agendas/%s/events/%s/' % (agenda.id, event.id) in mailoutbox[0].body
|
||
assert 'manage/agendas/%s/events/%s/' % (agenda.id, event.id) in mailoutbox[0].alternatives[0][0]
|
||
|
||
# no new email on subsequent run
|
||
call_command('send_email_notifications')
|
||
assert len(mailoutbox) == expected_email_count
|
||
|
||
|
||
def test_agenda_notifications_email_list(mailoutbox):
|
||
agenda = Agenda.objects.create(label='Foo bar', kind='event')
|
||
|
||
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
|
||
settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
|
||
settings.full_event = AgendaNotificationsSettings.EMAIL_FIELD
|
||
settings.full_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com']
|
||
settings.save()
|
||
|
||
for _ in range(10):
|
||
Booking.objects.create(event=event)
|
||
event.refresh_from_db()
|
||
assert event.full
|
||
|
||
call_command('send_email_notifications')
|
||
assert len(mailoutbox) == 1
|
||
assert mailoutbox[0].recipients() == recipients
|
||
assert mailoutbox[0].subject == 'Alert: event "Hop" is full'
|
||
assert (
|
||
'view it here: https://example.com/manage/agendas/%s/events/%s/'
|
||
% (
|
||
agenda.pk,
|
||
event.pk,
|
||
)
|
||
in mailoutbox[0].body
|
||
)
|
||
|
||
# no new email on subsequent run
|
||
call_command('send_email_notifications')
|
||
assert len(mailoutbox) == 1
|
||
|
||
|
||
def test_agenda_notifications_cancelled(mailoutbox):
|
||
agenda = Agenda.objects.create(label='Foo bar', kind='event')
|
||
|
||
event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop')
|
||
settings = AgendaNotificationsSettings.objects.create(agenda=agenda)
|
||
settings.cancelled_event = AgendaNotificationsSettings.EMAIL_FIELD
|
||
settings.cancelled_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com']
|
||
settings.save()
|
||
|
||
event.cancelled = True
|
||
event.save()
|
||
|
||
call_command('send_email_notifications')
|
||
assert len(mailoutbox) == 1
|
||
assert mailoutbox[0].recipients() == recipients
|
||
assert mailoutbox[0].subject == 'Alert: event "Hop" is cancelled'
|
||
|
||
# no new email on subsequent run
|
||
call_command('send_email_notifications')
|
||
assert len(mailoutbox) == 1
|
||
|
||
|
||
def test_agenda_reminders(mailoutbox, freezer):
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
|
||
# add some old event with booking
|
||
freezer.move_to('2019-01-01')
|
||
old_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event')
|
||
Booking.objects.create(event=old_event, user_email='old@test.org')
|
||
|
||
# no reminder configured
|
||
call_command('send_booking_reminders')
|
||
assert len(mailoutbox) == 0
|
||
|
||
# move to present day
|
||
freezer.move_to('2020-01-01 14:00')
|
||
# configure reminder the day before
|
||
AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1)
|
||
# event starts in 2 days
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
|
||
|
||
for _ in range(5):
|
||
Booking.objects.create(event=event, user_email='t@test.org')
|
||
# extra booking with no email, should be ignored
|
||
Booking.objects.create(event=event)
|
||
|
||
freezer.move_to('2020-01-02 10:00')
|
||
# not time to send reminders yet
|
||
call_command('send_booking_reminders')
|
||
assert len(mailoutbox) == 0
|
||
|
||
# one of the booking is cancelled
|
||
Booking.objects.filter(user_email='t@test.org').first().cancel()
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
call_command('send_booking_reminders')
|
||
assert len(mailoutbox) == 4
|
||
mailoutbox.clear()
|
||
|
||
call_command('send_booking_reminders')
|
||
assert len(mailoutbox) == 0
|
||
|
||
# booking is placed the day of the event, notfication should no be sent
|
||
freezer.move_to('2020-01-03 08:00')
|
||
Booking.objects.create(event=event, user_email='t@test.org')
|
||
call_command('send_booking_reminders')
|
||
assert len(mailoutbox) == 0
|
||
|
||
|
||
@pytest.mark.freeze_time('2020-01-01 14:00')
|
||
def test_agenda_reminders_extra_emails(mailoutbox, freezer):
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1)
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
|
||
|
||
Booking.objects.create(
|
||
event=event,
|
||
user_email='t@test.org',
|
||
extra_emails=['t@test.org', 'u@test.org', 'v@test.org'],
|
||
)
|
||
Booking.objects.create(event=event, extra_emails=['w@test.org'])
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
call_command('send_booking_reminders')
|
||
|
||
assert len(mailoutbox) == 4
|
||
assert all(len(x.to) == 1 for x in mailoutbox)
|
||
assert {x.to[0] for x in mailoutbox} == {'t@test.org', 'u@test.org', 'v@test.org', 'w@test.org'}
|
||
|
||
|
||
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO')
|
||
def test_agenda_reminders_sms(freezer):
|
||
freezer.move_to('2020-01-01 14:00')
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
AgendaReminderSettings.objects.create(agenda=agenda, days_before_sms=1)
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
|
||
|
||
for _ in range(5):
|
||
Booking.objects.create(event=event, user_phone_number='+336123456789')
|
||
Booking.objects.create(event=event)
|
||
Booking.objects.create(
|
||
event=event,
|
||
user_phone_number='+33111111111',
|
||
extra_phone_numbers=['+33111111111', '+33222222222', '+33333333333'],
|
||
)
|
||
Booking.objects.create(event=event, extra_phone_numbers=['+336123456789'])
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
call_command('send_booking_reminders')
|
||
|
||
assert mock_send.call_count == 7
|
||
body = json.loads(mock_send.call_args_list[0][0][0].body.decode())
|
||
assert body['from'] == 'EO'
|
||
assert body['to'] == ['+336123456789']
|
||
|
||
body = json.loads(mock_send.call_args_list[5][0][0].body.decode())
|
||
assert body['from'] == 'EO'
|
||
assert set(body['to']) == {'+33111111111', '+33222222222', '+33333333333'}
|
||
|
||
body = json.loads(mock_send.call_args_list[6][0][0].body.decode())
|
||
assert body['from'] == 'EO'
|
||
assert set(body['to']) == {'+336123456789'}
|
||
|
||
|
||
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO')
|
||
def test_agenda_reminders_retry(freezer):
|
||
freezer.move_to('2020-01-01 14:00')
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
settings = AgendaReminderSettings.objects.create(agenda=agenda)
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
|
||
|
||
settings.days_before_email = 1
|
||
settings.save()
|
||
booking = Booking.objects.create(event=event, user_email='t@test.org')
|
||
freezer.move_to('2020-01-02 15:00')
|
||
|
||
def send_mail_error(*args, **kwargs):
|
||
raise smtplib.SMTPException
|
||
|
||
with mock.patch('chrono.agendas.management.commands.utils.send_mail') as mock_send:
|
||
mock_send.return_value = None
|
||
mock_send.side_effect = send_mail_error
|
||
call_command('send_booking_reminders')
|
||
assert mock_send.call_count == 1
|
||
booking.refresh_from_db()
|
||
assert not booking.email_reminder_datetime
|
||
assert not booking.sms_reminder_datetime
|
||
|
||
mock_send.side_effect = None
|
||
call_command('send_booking_reminders')
|
||
assert mock_send.call_count == 2
|
||
booking.refresh_from_db()
|
||
assert booking.email_reminder_datetime
|
||
assert not booking.sms_reminder_datetime
|
||
|
||
settings.days_before_email = None
|
||
settings.days_before_sms = 1
|
||
settings.save()
|
||
freezer.move_to('2020-01-01 14:00')
|
||
booking = Booking.objects.create(event=event, user_phone_number='+336123456789')
|
||
freezer.move_to('2020-01-02 15:00')
|
||
|
||
def mocked_requests_connection_error(*args, **kwargs):
|
||
raise requests.ConnectionError('unreachable')
|
||
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_send.side_effect = mocked_requests_connection_error
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
call_command('send_booking_reminders')
|
||
assert mock_send.call_count == 1
|
||
booking.refresh_from_db()
|
||
assert not booking.sms_reminder_datetime
|
||
assert not booking.email_reminder_datetime
|
||
|
||
mock_send.side_effect = None
|
||
call_command('send_booking_reminders')
|
||
assert mock_send.call_count == 2
|
||
booking.refresh_from_db()
|
||
assert booking.sms_reminder_datetime
|
||
assert not booking.email_reminder_datetime
|
||
|
||
settings.days_before_email = 1
|
||
settings.save()
|
||
freezer.move_to('2020-01-01 14:00')
|
||
booking = Booking.objects.create(event=event, user_phone_number='+336123456789', user_email='t@test.org')
|
||
freezer.move_to('2020-01-02 15:00')
|
||
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send, mock.patch(
|
||
'chrono.agendas.management.commands.utils.send_mail'
|
||
) as mock_send_mail:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
mock_send_mail.return_value = None
|
||
call_command('send_booking_reminders')
|
||
|
||
assert mock_send.call_count == 1
|
||
assert mock_send_mail.call_count == 1
|
||
booking.refresh_from_db()
|
||
assert booking.sms_reminder_datetime
|
||
assert booking.email_reminder_datetime
|
||
|
||
call_command('send_booking_reminders')
|
||
assert mock_send.call_count == 1
|
||
assert mock_send_mail.call_count == 1
|
||
|
||
|
||
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO')
|
||
def test_agenda_reminders_different_days_before(freezer):
|
||
freezer.move_to('2020-01-01 14:00')
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1, days_before_sms=2)
|
||
start_datetime = now() + datetime.timedelta(days=3)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
|
||
Booking.objects.create(event=event, user_email='t@test.org', user_phone_number='+336123456789')
|
||
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send_sms, mock.patch(
|
||
'chrono.agendas.management.commands.utils.send_mail'
|
||
) as mock_send_mail:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send_sms.return_value = mock_response
|
||
mock_send_mail.return_value = None
|
||
|
||
call_command('send_booking_reminders')
|
||
assert mock_send_sms.call_count == 0
|
||
assert mock_send_mail.call_count == 0
|
||
|
||
# two days before
|
||
freezer.move_to('2020-01-02 15:00')
|
||
call_command('send_booking_reminders')
|
||
assert mock_send_sms.call_count == 1
|
||
assert mock_send_mail.call_count == 0
|
||
|
||
# one day before
|
||
freezer.move_to('2020-01-03 15:00')
|
||
call_command('send_booking_reminders')
|
||
assert mock_send_sms.call_count == 1
|
||
assert mock_send_mail.call_count == 1
|
||
|
||
|
||
@override_settings(TIME_ZONE='UTC')
|
||
def test_agenda_reminders_email_content(mailoutbox, freezer):
|
||
freezer.move_to('2020-01-01 14:00')
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
AgendaReminderSettings.objects.create(
|
||
agenda=agenda, days_before_email=1, email_extra_info='Do no forget ID card.'
|
||
)
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=start_datetime,
|
||
places=10,
|
||
label='Pool party',
|
||
description='Come !',
|
||
url='https://example.party',
|
||
pricing='10€',
|
||
)
|
||
|
||
Booking.objects.create(event=event, user_email='t@test.org')
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
call_command('send_booking_reminders')
|
||
|
||
mail = mailoutbox[0]
|
||
assert mail.subject == 'Reminder for your booking tomorrow at 2 p.m.'
|
||
mail_bodies = (mail.body, mail.alternatives[0][0])
|
||
for body in mail_bodies:
|
||
assert 'Hi,' in body
|
||
assert 'You have booked event "Pool party", on Friday 3 January at 2 p.m..' in body
|
||
assert 'Do no forget ID card.' in body
|
||
assert 'Come !' in body
|
||
assert 'Pricing: 10€' in body
|
||
assert 'cancel' not in body
|
||
assert 'if present' not in body # assert separation with preview code
|
||
assert 'More information: https://example.party' in mail_bodies[0]
|
||
assert '<a href="https://example.party">More information</a>' in mail_bodies[1]
|
||
mailoutbox.clear()
|
||
|
||
freezer.move_to('2020-01-01 14:00')
|
||
Booking.objects.create(event=event, user_email='t@test.org', form_url='https://example.org/')
|
||
freezer.move_to('2020-01-02 15:00')
|
||
call_command('send_booking_reminders')
|
||
|
||
mail = mailoutbox[0]
|
||
assert 'If in need to cancel it, you can do so here: https://example.org/' in mail.body
|
||
assert 'Edit or cancel booking' in mail.alternatives[0][0]
|
||
assert 'href="https://example.org/"' in mail.alternatives[0][0]
|
||
|
||
# check url translation
|
||
Booking.objects.all().delete()
|
||
mailoutbox.clear()
|
||
freezer.move_to('2020-01-01 14:00')
|
||
Booking.objects.create(event=event, user_email='t@test.org', form_url='publik://default/someform/1/')
|
||
freezer.move_to('2020-01-02 15:00')
|
||
call_command('send_booking_reminders')
|
||
|
||
mail = mailoutbox[0]
|
||
assert 'If in need to cancel it, you can do so here: http://example.org/someform/1/' in mail.body
|
||
assert 'Edit or cancel booking' in mail.alternatives[0][0]
|
||
assert 'href="http://example.org/someform/1/"' in mail.alternatives[0][0]
|
||
|
||
|
||
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO', TIME_ZONE='UTC')
|
||
def test_agenda_reminders_sms_content(freezer):
|
||
freezer.move_to('2020-01-01 14:00')
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
AgendaReminderSettings.objects.create(
|
||
agenda=agenda, days_before_sms=1, sms_extra_info='Do no forget ID card.'
|
||
)
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party')
|
||
|
||
Booking.objects.create(event=event, user_phone_number='+336123456789')
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
call_command('send_booking_reminders')
|
||
|
||
body = json.loads(mock_send.call_args[0][0].body.decode())
|
||
assert (
|
||
body['message']
|
||
== 'Reminder: you have booked event "Pool party", on 03/01 at 2 p.m.. Do no forget ID card.'
|
||
)
|
||
|
||
|
||
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO', TIME_ZONE='UTC')
|
||
def test_agenda_reminders_templated_content(mailoutbox, freezer):
|
||
freezer.move_to('2020-01-01 14:00')
|
||
agenda = Agenda.objects.create(label='Main Center', kind='events')
|
||
AgendaReminderSettings.objects.create(
|
||
agenda=agenda,
|
||
days_before_email=1,
|
||
days_before_sms=1,
|
||
email_extra_info='Go to {{ booking.event.agenda.label }}.\nTake your {{ booking.extra_data.document_type }}.',
|
||
sms_extra_info='Take your {{ booking.extra_data.document_type }}.',
|
||
)
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party')
|
||
|
||
Booking.objects.create(
|
||
event=event,
|
||
user_email='t@test.org',
|
||
user_phone_number='+336123456789',
|
||
extra_data={'document_type': '"receipt"'},
|
||
)
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
call_command('send_booking_reminders')
|
||
|
||
mail = mailoutbox[0]
|
||
assert 'Go to Main Center.\nTake your "receipt".' in mail.body
|
||
assert '<p>Go to Main Center.<br>Take your "receipt".</p>' in mail.alternatives[0][0]
|
||
|
||
body = json.loads(mock_send.call_args[0][0].body.decode())
|
||
assert 'Take your "receipt".' in body['message']
|
||
|
||
# in case of invalid template, send anyway
|
||
freezer.move_to('2020-01-01 14:00')
|
||
Booking.objects.create(event=event, user_email='t@test.org', user_phone_number='+336123456789')
|
||
agenda.reminder_settings.email_extra_info = 'Take your {{ syntax error }}'
|
||
agenda.reminder_settings.sms_extra_info = 'Take your {{ syntax error }}'
|
||
agenda.reminder_settings.save()
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||
mock_response = mock.Mock(status_code=200)
|
||
mock_send.return_value = mock_response
|
||
call_command('send_booking_reminders')
|
||
|
||
assert len(mailoutbox) == 2
|
||
assert 'Take your' not in mailoutbox[1].body
|
||
|
||
body = json.loads(mock_send.call_args[0][0].body.decode())
|
||
assert 'Take your' not in body['message']
|
||
|
||
|
||
@override_settings(TIME_ZONE='UTC')
|
||
def test_agenda_reminders_meetings(mailoutbox, freezer):
|
||
freezer.move_to('2020-01-01 11:00')
|
||
agenda = Agenda.objects.create(label='Events', kind='meetings')
|
||
desk = Desk.objects.create(agenda=agenda, label='Desk')
|
||
meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30)
|
||
TimePeriod.objects.create(
|
||
desk=desk, weekday=now().weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
|
||
)
|
||
AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=2)
|
||
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
places=1,
|
||
desk=desk,
|
||
meeting_type=meetingtype,
|
||
start_datetime=now() + datetime.timedelta(days=5), # 06/01
|
||
)
|
||
Booking.objects.create(
|
||
event=event,
|
||
user_email='t@test.org',
|
||
user_display_label='Birth certificate',
|
||
form_url='publik://default/someform/1/',
|
||
)
|
||
|
||
freezer.move_to('2020-01-04 15:00')
|
||
call_command('send_booking_reminders')
|
||
assert len(mailoutbox) == 1
|
||
|
||
mail = mailoutbox[0]
|
||
assert mail.subject == 'Reminder for your meeting in 2 days at 11 a.m.'
|
||
assert 'Your meeting "Birth certificate" is scheduled on Monday 6 January at 11 a.m..' in mail.body
|
||
assert 'If in need to cancel it, you can do so here: http://example.org/someform/1/' in mail.body
|
||
assert 'href="http://example.org/someform/1/"' in mail.alternatives[0][0]
|
||
|
||
|
||
def test_agenda_reminders_waiting_list(mailoutbox, freezer):
|
||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||
|
||
freezer.move_to('2020-01-01 14:00')
|
||
# configure reminder the day before
|
||
AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1)
|
||
# event starts in 2 days
|
||
start_datetime = now() + datetime.timedelta(days=2)
|
||
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
|
||
|
||
for _ in range(5):
|
||
Booking.objects.create(event=event, user_email='t@test.org')
|
||
# extra booking in waiting list, should be ignored
|
||
Booking.objects.create(event=event, user_email='t@test.org', in_waiting_list=True)
|
||
|
||
freezer.move_to('2020-01-02 15:00')
|
||
call_command('send_booking_reminders')
|
||
assert len(mailoutbox) == 5
|
||
mailoutbox.clear()
|
||
|
||
|
||
def test_anonymize_bookings(freezer):
|
||
day = datetime.datetime(year=2020, month=1, day=1)
|
||
freezer.move_to(day)
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event'
|
||
)
|
||
|
||
for _ in range(5):
|
||
Booking.objects.create(
|
||
event=event,
|
||
extra_data={'test': True},
|
||
label='john',
|
||
user_display_label='john',
|
||
user_external_id='john',
|
||
user_first_name='john',
|
||
user_last_name='doe',
|
||
backoffice_url='https://example.org',
|
||
)
|
||
|
||
freezer.move_to(day + datetime.timedelta(days=50))
|
||
new_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event')
|
||
booking = Booking.objects.create(event=new_event, label='hop')
|
||
|
||
freezer.move_to(day + datetime.timedelta(days=101))
|
||
call_command('anonymize_bookings')
|
||
assert not Booking.objects.filter(anonymization_datetime__isnull=False).exists()
|
||
|
||
# now ask for anonymization
|
||
agenda.anonymize_delay = 100
|
||
agenda.save()
|
||
|
||
call_command('anonymize_bookings')
|
||
# bookings were placed more than 100 days ago but event was only 90 days ago
|
||
assert not Booking.objects.filter(anonymization_datetime__isnull=False).exists()
|
||
|
||
freezer.move_to(day + datetime.timedelta(days=111))
|
||
call_command('anonymize_bookings')
|
||
assert (
|
||
Booking.objects.filter(
|
||
label='',
|
||
user_display_label='',
|
||
user_external_id='',
|
||
user_first_name='',
|
||
user_last_name='',
|
||
backoffice_url='https://example.org',
|
||
extra_data={},
|
||
anonymization_datetime=now(),
|
||
).count()
|
||
== 5
|
||
)
|
||
|
||
booking.refresh_from_db()
|
||
assert booking.label == 'hop'
|
||
assert not booking.anonymization_datetime
|
||
|
||
|
||
def test_recurring_events(freezer):
|
||
freezer.move_to('2021-01-06 12:00') # Wednesday
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=[now().weekday()],
|
||
label='Event',
|
||
places=10,
|
||
waiting_list_places=10,
|
||
duration=10,
|
||
description='Description',
|
||
url='https://example.com',
|
||
pricing='10€',
|
||
)
|
||
event.refresh_from_db()
|
||
|
||
recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=15))
|
||
assert len(recurrences) == 3
|
||
|
||
first_event = recurrences[0]
|
||
assert first_event.slug == event.slug + '--2021-01-06-1300'
|
||
|
||
event_json = event.export_json()
|
||
first_event_json = first_event.export_json()
|
||
different_fields = ['slug', 'recurrence_days', 'recurrence_week_interval']
|
||
assert all(first_event_json[k] == event_json[k] for k in event_json if k not in different_fields)
|
||
|
||
second_event = recurrences[1]
|
||
assert second_event.start_datetime == first_event.start_datetime + datetime.timedelta(days=7)
|
||
assert second_event.start_datetime.weekday() == first_event.start_datetime.weekday()
|
||
assert second_event.slug == 'event--2021-01-13-1300'
|
||
|
||
different_fields = ['slug', 'start_datetime']
|
||
second_event_json = second_event.export_json()
|
||
assert all(first_event_json[k] == second_event_json[k] for k in event_json if k not in different_fields)
|
||
|
||
new_recurrences = event.get_recurrences(
|
||
localtime() + datetime.timedelta(days=15),
|
||
localtime() + datetime.timedelta(days=30),
|
||
)
|
||
assert len(recurrences) == 3
|
||
assert new_recurrences[0].start_datetime == recurrences[-1].start_datetime + datetime.timedelta(days=7)
|
||
|
||
|
||
def test_recurring_events_dst(freezer, settings):
|
||
freezer.move_to('2020-10-24 12:00')
|
||
settings.TIME_ZONE = 'Europe/Brussels'
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], places=5
|
||
)
|
||
event.refresh_from_db()
|
||
dt = localtime()
|
||
recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8))
|
||
event_before_dst, event_after_dst = recurrences
|
||
assert event_before_dst.start_datetime.hour + 1 == event_after_dst.start_datetime.hour
|
||
assert event_before_dst.slug == 'agenda-event--2020-10-24-1400'
|
||
assert event_after_dst.slug == 'agenda-event--2020-10-31-1400'
|
||
|
||
freezer.move_to('2020-11-24 12:00')
|
||
new_recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8))
|
||
new_event_before_dst, new_event_after_dst = new_recurrences
|
||
assert event_before_dst.start_datetime == new_event_before_dst.start_datetime
|
||
assert event_after_dst.start_datetime == new_event_after_dst.start_datetime
|
||
assert event_before_dst.slug == new_event_before_dst.slug
|
||
assert event_after_dst.slug == new_event_after_dst.slug
|
||
|
||
|
||
def test_recurring_events_repetition(freezer):
|
||
freezer.move_to('2021-01-06 12:00') # Wednesday
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=list(range(7)), # everyday
|
||
places=5,
|
||
)
|
||
event.refresh_from_db()
|
||
start_datetime = localtime(event.start_datetime)
|
||
|
||
freezer.move_to('2021-01-06 12:01') # recurrence on same day should not be returned
|
||
recurrences = event.get_recurrences(
|
||
localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7)
|
||
)
|
||
assert len(recurrences) == 6
|
||
assert recurrences[0].start_datetime == start_datetime + datetime.timedelta(days=2)
|
||
assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7)
|
||
for i in range(len(recurrences) - 1):
|
||
assert recurrences[i].start_datetime + datetime.timedelta(days=1) == recurrences[i + 1].start_datetime
|
||
|
||
event.recurrence_days = list(range(5)) # from Monday to Friday
|
||
event.save()
|
||
recurrences = event.get_recurrences(
|
||
localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7)
|
||
)
|
||
assert len(recurrences) == 4
|
||
assert recurrences[0].start_datetime == start_datetime + datetime.timedelta(days=2)
|
||
assert recurrences[1].start_datetime == start_datetime + datetime.timedelta(days=5)
|
||
assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7)
|
||
|
||
event.recurrence_days = [localtime(event.start_datetime).weekday()] # from Monday to Friday
|
||
event.recurrence_week_interval = 2
|
||
event.save()
|
||
recurrences = event.get_recurrences(
|
||
localtime() + datetime.timedelta(days=3), localtime() + datetime.timedelta(days=45)
|
||
)
|
||
assert len(recurrences) == 3
|
||
assert recurrences[0].start_datetime == start_datetime + datetime.timedelta(days=14)
|
||
assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=14) * len(recurrences)
|
||
for i in range(len(recurrences) - 1):
|
||
assert (
|
||
recurrences[i].start_datetime + datetime.timedelta(days=14) == recurrences[i + 1].start_datetime
|
||
)
|
||
|
||
event.recurrence_days = [3] # Tuesday but start_datetime is a Wednesday
|
||
event.recurrence_week_interval = 1
|
||
event.save()
|
||
recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=10))
|
||
assert len(recurrences) == 2
|
||
# no recurrence exist on Wednesday
|
||
assert all(localtime(r.start_datetime).weekday() == 3 for r in recurrences)
|
||
|
||
|
||
@pytest.mark.freeze_time('2021-01-06')
|
||
def test_recurring_events_with_end_date():
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
recurrence_end_date=(now() + datetime.timedelta(days=5)).date(),
|
||
)
|
||
event.refresh_from_db()
|
||
start_datetime = localtime(event.start_datetime)
|
||
|
||
recurrences = event.get_recurrences(
|
||
localtime(event.start_datetime), localtime(event.start_datetime) + datetime.timedelta(days=10)
|
||
)
|
||
assert len(recurrences) == 5
|
||
assert recurrences[0].start_datetime == start_datetime
|
||
assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=4)
|
||
|
||
|
||
@override_settings(
|
||
EXCEPTIONS_SOURCES={
|
||
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
|
||
}
|
||
)
|
||
def test_recurring_events_exceptions(freezer):
|
||
freezer.move_to('2021-05-01 12:00')
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||
desk.import_timeperiod_exceptions_from_settings()
|
||
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
)
|
||
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-01'
|
||
|
||
desk.import_timeperiod_exceptions_from_settings(enable=True)
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
# 05-01 is a holiday
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
|
||
first_event = recurrences[0]
|
||
|
||
# exception before first_event start_datetime
|
||
time_period_exception = TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=first_event.start_datetime - datetime.timedelta(hours=1),
|
||
end_datetime=first_event.start_datetime - datetime.timedelta(minutes=30),
|
||
)
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
|
||
|
||
# exception wraps around first_event start_datetime
|
||
time_period_exception.end_datetime = first_event.start_datetime + datetime.timedelta(minutes=30)
|
||
time_period_exception.save()
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-03'
|
||
|
||
# exception starts after first_event start_datetime
|
||
time_period_exception.start_datetime = first_event.start_datetime + datetime.timedelta(minutes=15)
|
||
time_period_exception.save()
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
|
||
assert recurrences[1].start_datetime.strftime('%m-%d') == '05-03'
|
||
|
||
# exception spans multiple days
|
||
time_period_exception.end_datetime = first_event.start_datetime + datetime.timedelta(days=3)
|
||
time_period_exception.save()
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
|
||
assert recurrences[1].start_datetime.strftime('%m-%d') == '05-06'
|
||
|
||
# move exception to unavailability calendar
|
||
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
time_period_exception.desk = None
|
||
time_period_exception.unavailability_calendar = unavailability_calendar
|
||
time_period_exception.save()
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
|
||
assert recurrences[1].start_datetime.strftime('%m-%d') == '05-03'
|
||
|
||
unavailability_calendar.desks.add(desk)
|
||
recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7))
|
||
assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02'
|
||
assert recurrences[1].start_datetime.strftime('%m-%d') == '05-06'
|
||
|
||
|
||
def test_recurring_events_exceptions_update_recurrences(freezer):
|
||
freezer.move_to('2021-05-01 12:00')
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||
|
||
daily_event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
recurrence_end_date=datetime.date(year=2021, month=5, day=8),
|
||
)
|
||
weekly_event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=[now().weekday()],
|
||
places=5,
|
||
recurrence_end_date=datetime.date(year=2021, month=6, day=1),
|
||
)
|
||
weekly_event_no_end_date = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now() + datetime.timedelta(hours=2),
|
||
recurrence_days=[now().weekday()],
|
||
places=5,
|
||
)
|
||
Event.create_events_recurrences([daily_event, weekly_event, weekly_event_no_end_date])
|
||
|
||
assert Event.objects.filter(primary_event=daily_event).count() == 7
|
||
assert Event.objects.filter(primary_event=weekly_event).count() == 5
|
||
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53
|
||
|
||
time_period_exception = TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=datetime.date(year=2021, month=5, day=5),
|
||
end_datetime=datetime.date(year=2021, month=5, day=10),
|
||
)
|
||
agenda.update_event_recurrences()
|
||
assert Event.objects.filter(primary_event=daily_event).count() == 4
|
||
assert Event.objects.filter(primary_event=weekly_event).count() == 4
|
||
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 52
|
||
|
||
time_period_exception.delete()
|
||
agenda.update_event_recurrences()
|
||
assert Event.objects.filter(primary_event=daily_event).count() == 7
|
||
assert Event.objects.filter(primary_event=weekly_event).count() == 5
|
||
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53
|
||
|
||
event = Event.objects.get(
|
||
primary_event=weekly_event_no_end_date, start_datetime=now() + datetime.timedelta(days=7, hours=2)
|
||
)
|
||
Booking.objects.create(event=event)
|
||
time_period_exception.save()
|
||
|
||
agenda.update_event_recurrences()
|
||
assert Booking.objects.count() == 1
|
||
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53
|
||
assert agenda.recurrence_exceptions_report.events.get() == event
|
||
|
||
# no recurrence end date means new events are created as time moves on
|
||
freezer.move_to('2021-06-01 12:00')
|
||
agenda.update_event_recurrences()
|
||
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 57
|
||
|
||
|
||
def test_recurring_events_exceptions_update_recurrences_start_datetime_modified(freezer):
|
||
freezer.move_to('2021-09-06 12:00') # Monday
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||
|
||
daily_event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
recurrence_end_date=datetime.date(year=2021, month=9, day=13),
|
||
)
|
||
daily_event.create_all_recurrences()
|
||
assert daily_event.recurrences.count() == 7
|
||
|
||
monday_recurrence = daily_event.recurrences.get(start_datetime__day=6)
|
||
monday_recurrence.start_datetime += datetime.timedelta(days=7)
|
||
monday_recurrence.save()
|
||
|
||
sunday_recurrence = daily_event.recurrences.get(start_datetime__day=12)
|
||
sunday_recurrence.start_datetime += datetime.timedelta(hours=1)
|
||
sunday_recurrence.save()
|
||
|
||
# exception from Monday 06/09 to Tuesday 07/09
|
||
TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=datetime.date(year=2021, month=9, day=6),
|
||
end_datetime=datetime.date(year=2021, month=9, day=8),
|
||
)
|
||
|
||
# exception on Sunday 12/09
|
||
TimePeriodException.objects.create(
|
||
desk=desk,
|
||
start_datetime=datetime.date(year=2021, month=9, day=12),
|
||
end_datetime=datetime.date(year=2021, month=9, day=13),
|
||
)
|
||
agenda.update_event_recurrences()
|
||
|
||
# Tuesday event was deleted
|
||
assert daily_event.recurrences.count() == 6
|
||
assert not Event.objects.filter(start_datetime__day=7).exists()
|
||
# Monday and Sunday still exist
|
||
assert Event.objects.filter(pk__in=[monday_recurrence.pk, sunday_recurrence.pk]).exists()
|
||
|
||
|
||
@pytest.mark.freeze_time('2022-02-22 14:00')
|
||
def test_recurring_events_update_recurrences_new_event(freezer):
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||
|
||
daily_event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
recurrence_end_date=now() + datetime.timedelta(days=7),
|
||
)
|
||
agenda.update_event_recurrences()
|
||
assert daily_event.recurrences.count() == 7
|
||
|
||
Event.objects.all().delete()
|
||
daily_event_no_end_date = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now(),
|
||
recurrence_days=[1],
|
||
recurrence_week_interval=3,
|
||
places=5,
|
||
)
|
||
agenda.update_event_recurrences()
|
||
assert daily_event_no_end_date.recurrences.count() == 18
|
||
|
||
daily_event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now().replace(hour=10),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
recurrence_end_date=now() + datetime.timedelta(days=7),
|
||
)
|
||
agenda.update_event_recurrences()
|
||
assert daily_event.recurrences.count() == 7
|
||
assert daily_event_no_end_date.recurrences.count() == 18
|
||
|
||
|
||
def test_recurring_events_display(freezer):
|
||
freezer.move_to('2021-01-06 12:30')
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now() + datetime.timedelta(days=1),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
)
|
||
|
||
assert event.get_recurrence_display() == 'Daily at 1:30 p.m., from Jan. 7, 2021'
|
||
|
||
freezer.move_to('2021-01-07 12:30')
|
||
assert event.get_recurrence_display() == 'Daily at 1:30 p.m.'
|
||
|
||
event.recurrence_days = [1, 2, 3, 4]
|
||
event.save()
|
||
assert event.get_recurrence_display() == 'From Tuesday to Friday at 1:30 p.m.'
|
||
|
||
event.recurrence_days = [4, 5, 6]
|
||
event.save()
|
||
assert event.get_recurrence_display() == 'From Friday to Sunday at 1:30 p.m.'
|
||
|
||
event.recurrence_days = [1, 4, 6]
|
||
event.save()
|
||
assert event.get_recurrence_display() == 'On Tuesdays, Fridays, Sundays at 1:30 p.m.'
|
||
|
||
event.recurrence_days = [0]
|
||
event.recurrence_week_interval = 2
|
||
event.save()
|
||
assert event.get_recurrence_display() == 'On Mondays at 1:30 p.m., once every two weeks'
|
||
|
||
event.recurrence_week_interval = 3
|
||
event.recurrence_end_date = now() + datetime.timedelta(days=7)
|
||
event.save()
|
||
assert (
|
||
event.get_recurrence_display()
|
||
== 'On Mondays at 1:30 p.m., once every three weeks, until Jan. 14, 2021'
|
||
)
|
||
|
||
freezer.move_to('2021-01-06 12:30')
|
||
assert (
|
||
event.get_recurrence_display()
|
||
== 'On Mondays at 1:30 p.m., once every three weeks, from Jan. 7, 2021, until Jan. 14, 2021'
|
||
)
|
||
|
||
|
||
def test_event_triggered_fields():
|
||
# alter event pk sequence to have a bigint
|
||
with connection.cursor() as cursor:
|
||
cursor.execute("SELECT nextval('agendas_event_id_seq')")
|
||
row = cursor.fetchone()
|
||
if row[0] < 2**31:
|
||
cursor.execute("ALTER SEQUENCE agendas_event_id_seq RESTART WITH %s;" % 2**31)
|
||
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
event = Event.objects.create(
|
||
agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event'
|
||
)
|
||
event2 = Event.objects.create(
|
||
agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event'
|
||
)
|
||
assert event.booked_places == 0
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is False
|
||
assert event.full is False
|
||
|
||
event.booked_places = 42
|
||
event.booked_waiting_list_places = 42
|
||
event.almost_full = True
|
||
event.full = True
|
||
event.save()
|
||
# computed by triggers
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 0
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is False
|
||
assert event.full is False
|
||
|
||
# add bookings for other event: no impact
|
||
for _ in range(10):
|
||
Booking.objects.create(event=event2)
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 0
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is False
|
||
assert event.full is False
|
||
|
||
# add bookings
|
||
for _ in range(9):
|
||
Booking.objects.create(event=event)
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 9
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
|
||
Booking.objects.create(event=event)
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is True
|
||
assert event.full is True
|
||
|
||
# cancel bookings for other event: no impact
|
||
event2.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is True
|
||
assert event.full is True
|
||
|
||
# cancel bookings
|
||
event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 9
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 8
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is False
|
||
assert event.full is False
|
||
|
||
# update places
|
||
event.places = 20
|
||
event.save()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 8
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is False
|
||
assert event.full is False
|
||
|
||
Booking.objects.create(event=event)
|
||
Booking.objects.create(event=event)
|
||
event.places = 10
|
||
event.save()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is True
|
||
assert event.full is True
|
||
|
||
# with a waiting list
|
||
event.waiting_list_places = 5
|
||
event.save()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
|
||
# add bookings for other event: no impact
|
||
for _ in range(10):
|
||
Booking.objects.create(event=event2, in_waiting_list=True)
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 0
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
|
||
# add bookings
|
||
Booking.objects.create(event=event, in_waiting_list=True)
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 1
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
for _ in range(1, 5):
|
||
Booking.objects.create(event=event, in_waiting_list=True)
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 5
|
||
assert event.almost_full is True
|
||
assert event.full is True
|
||
|
||
# cancel bookings for other event: no impact
|
||
event2.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 5
|
||
assert event.almost_full is True
|
||
assert event.full is True
|
||
|
||
# cancel bookings
|
||
event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 4
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
|
||
# update waiting list places
|
||
event.waiting_list_places = 4
|
||
event.save()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 4
|
||
assert event.almost_full is True
|
||
assert event.full is True
|
||
|
||
# delete bookings
|
||
event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().delete()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 10
|
||
assert event.booked_waiting_list_places == 3
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 9
|
||
assert event.booked_waiting_list_places == 3
|
||
assert event.almost_full is True
|
||
assert event.full is False
|
||
event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete()
|
||
event.refresh_from_db()
|
||
assert event.booked_places == 8
|
||
assert event.booked_waiting_list_places == 3
|
||
assert event.almost_full is False
|
||
assert event.full is False
|
||
|
||
|
||
def test_recurring_events_create_past_recurrences(freezer):
|
||
freezer.move_to('2021-09-06 12:00') # Monday
|
||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||
|
||
daily_event = Event.objects.create(
|
||
agenda=agenda,
|
||
start_datetime=now() - datetime.timedelta(days=3),
|
||
recurrence_days=list(range(7)),
|
||
places=5,
|
||
recurrence_end_date=now() + datetime.timedelta(days=3),
|
||
)
|
||
daily_event.create_all_recurrences()
|
||
assert daily_event.recurrences.count() == 6
|
||
|
||
|
||
@pytest.mark.freeze_time('2022-02-22 14:00') # Tuesday of 8th week
|
||
def test_shared_custody_agenda():
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
|
||
|
||
slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=30))
|
||
assert [x.date for x in slots] == [now().date() + datetime.timedelta(days=i) for i in range(30)]
|
||
assert all(x.guardian == father for x in slots if x.date.isocalendar()[1] % 2 == 0)
|
||
assert all(x.guardian == mother for x in slots if x.date.isocalendar()[1] % 2 == 1)
|
||
|
||
# add mother custody period on father's week, on 23/02 and 24/02
|
||
SharedCustodyPeriod.objects.create(
|
||
agenda=agenda,
|
||
guardian=mother,
|
||
date_start=datetime.date(year=2022, month=2, day=23),
|
||
date_end=datetime.date(year=2022, month=2, day=25),
|
||
)
|
||
slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=5))
|
||
slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots]
|
||
assert slots == [
|
||
('22/02', 'John Doe'),
|
||
('23/02', 'Jane Doe'),
|
||
('24/02', 'Jane Doe'),
|
||
('25/02', 'John Doe'),
|
||
('26/02', 'John Doe'),
|
||
]
|
||
|
||
# add father custody period on father's week, nothing should change
|
||
SharedCustodyPeriod.objects.create(
|
||
agenda=agenda,
|
||
guardian=father,
|
||
date_start=datetime.date(year=2022, month=2, day=25),
|
||
date_end=datetime.date(year=2022, month=3, day=3),
|
||
)
|
||
slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=5))
|
||
slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots]
|
||
assert slots == [
|
||
('22/02', 'John Doe'),
|
||
('23/02', 'Jane Doe'),
|
||
('24/02', 'Jane Doe'),
|
||
('25/02', 'John Doe'),
|
||
('26/02', 'John Doe'),
|
||
]
|
||
|
||
# check min_date/max_date
|
||
slots = agenda.get_custody_slots(
|
||
datetime.date(year=2022, month=2, day=27), datetime.date(year=2022, month=3, day=1)
|
||
)
|
||
slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots]
|
||
assert slots == [('27/02', 'John Doe'), ('28/02', 'John Doe')]
|
||
|
||
|
||
@pytest.mark.freeze_time('2022-02-22 14:00') # Tuesday
|
||
def test_shared_custody_agenda_different_periodicity():
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=[1, 2, 3], guardian=father)
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=[0, 4, 5, 6], guardian=mother)
|
||
slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=14))
|
||
assert [(x.date.strftime('%A %d/%m'), str(x.guardian)) for x in slots] == [
|
||
('Tuesday 22/02', 'John Doe'),
|
||
('Wednesday 23/02', 'John Doe'),
|
||
('Thursday 24/02', 'John Doe'),
|
||
('Friday 25/02', 'Jane Doe'),
|
||
('Saturday 26/02', 'Jane Doe'),
|
||
('Sunday 27/02', 'Jane Doe'),
|
||
('Monday 28/02', 'Jane Doe'),
|
||
('Tuesday 01/03', 'John Doe'),
|
||
('Wednesday 02/03', 'John Doe'),
|
||
('Thursday 03/03', 'John Doe'),
|
||
('Friday 04/03', 'Jane Doe'),
|
||
('Saturday 05/03', 'Jane Doe'),
|
||
('Sunday 06/03', 'Jane Doe'),
|
||
('Monday 07/03', 'Jane Doe'),
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'rules,complete',
|
||
(
|
||
([], False),
|
||
([{'days': [0]}], False),
|
||
([{'days': [0], 'weeks': 'odd'}], False),
|
||
([{'days': list(range(7))}], True),
|
||
([{'days': list(range(7)), 'weeks': 'odd'}], False),
|
||
([{'days': list(range(7)), 'weeks': 'odd'}, {'days': list(range(7)), 'weeks': 'even'}], True),
|
||
([{'days': [0, 1, 2]}, {'days': [3, 4, 5, 6]}], True),
|
||
([{'days': [0, 1, 2]}, {'days': [3, 4, 5]}], False),
|
||
([{'days': [0, 1, 2, 3]}, {'days': [3, 4, 5, 6]}], False), # overlapping rules, should not exist
|
||
(
|
||
[
|
||
{'days': [0, 1, 2]},
|
||
{'days': [3, 4, 5]},
|
||
{'days': [6], 'weeks': 'odd'},
|
||
{'days': [6], 'weeks': 'even'},
|
||
],
|
||
True,
|
||
),
|
||
),
|
||
)
|
||
def test_shared_custody_agenda_is_complete(rules, complete):
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
for i, rule in enumerate(rules):
|
||
guardian = father if i % 2 else mother
|
||
SharedCustodyRule.objects.create(agenda=agenda, guardian=guardian, **rule)
|
||
|
||
assert agenda.is_complete() is complete
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'rules,days,weeks,overlaps',
|
||
(
|
||
([], [1], '', False),
|
||
([{'days': [0]}], [1], '', False),
|
||
([{'days': [1]}], [1], '', True),
|
||
([{'days': [0]}, {'days': [1]}], [1], '', True),
|
||
([{'days': [0], 'weeks': 'odd'}, {'days': [1]}], [1], '', True),
|
||
([{'days': [0]}, {'days': [1], 'weeks': 'odd'}], [1], '', True),
|
||
([{'days': [0]}, {'days': [1]}], [1], 'odd', True),
|
||
([{'days': [0]}, {'days': [1], 'weeks': 'odd'}], [1], 'even', False),
|
||
([{'days': [0, 1], 'weeks': 'odd'}], [1], 'odd', True),
|
||
([{'days': [0, 1]}], [1], '', True),
|
||
([{'days': [0, 1], 'weeks': 'even'}], [1], 'odd', False),
|
||
([{'days': [0, 1]}], [0, 3, 4], '', True),
|
||
([{'days': [0, 1]}], [2, 3], '', False),
|
||
),
|
||
)
|
||
def test_shared_custody_agenda_rule_overlaps(rules, days, weeks, overlaps):
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
for i, rule in enumerate(rules):
|
||
guardian = father if i % 2 else mother
|
||
SharedCustodyRule.objects.create(agenda=agenda, guardian=guardian, **rule)
|
||
|
||
assert agenda.rule_overlaps(days, weeks) is overlaps
|
||
|
||
|
||
def test_shared_custody_agenda_holiday_rule_overlaps():
|
||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
summer_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Summer', slug='summer'
|
||
)
|
||
winter_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Winter', slug='winter'
|
||
)
|
||
|
||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='', instance=rule) is False
|
||
assert agenda.holiday_rule_overlaps(winter_holiday, years='', periodicity='') is False
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='first-half') is True
|
||
|
||
rule.years = 'odd'
|
||
rule.save()
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='') is False
|
||
|
||
rule.periodicity = 'first-half'
|
||
rule.save()
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is False
|
||
assert (
|
||
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters')
|
||
is True
|
||
)
|
||
assert (
|
||
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters')
|
||
is True
|
||
)
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False
|
||
|
||
rule.periodicity = 'second-and-fourth-quarters'
|
||
rule.save()
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is True
|
||
assert (
|
||
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters')
|
||
is False
|
||
)
|
||
assert (
|
||
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters')
|
||
is True
|
||
)
|
||
assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
'periods,date_start,date_end,overlaps',
|
||
(
|
||
([], '2022-02-03', '2022-02-04', False),
|
||
([('2022-02-03', '2022-02-04')], '2022-02-03', '2022-02-04', True),
|
||
([('2022-02-03', '2022-02-04')], '2022-02-01', '2022-02-04', True),
|
||
([('2022-02-03', '2022-02-04')], '2022-02-03', '2022-02-06', True),
|
||
([('2022-02-03', '2022-02-04')], '2022-02-04', '2022-02-06', False),
|
||
([('2022-02-03', '2022-02-04')], '2022-02-01', '2022-02-03', False),
|
||
([('2022-02-03', '2022-02-04'), ('2022-01-31', '2022-02-01')], '2022-02-01', '2022-02-03', False),
|
||
([('2022-02-03', '2022-02-04'), ('2022-01-31', '2022-02-01')], '2022-01-01', '2022-02-10', True),
|
||
([('2022-02-03', '2022-02-10')], '2022-02-05', '2022-02-06', True),
|
||
),
|
||
)
|
||
def test_shared_custody_agenda_period_overlaps(periods, date_start, date_end, overlaps):
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
for i, dates in enumerate(periods):
|
||
guardian = father if i % 2 else mother
|
||
SharedCustodyPeriod.objects.create(
|
||
agenda=agenda, guardian=guardian, date_start=dates[0], date_end=dates[1]
|
||
)
|
||
|
||
assert agenda.period_overlaps(date_start, date_end) is overlaps
|
||
|
||
|
||
def test_shared_custody_agenda_period_holiday_rule_no_overlaps():
|
||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
summer_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Summer', slug='summer'
|
||
)
|
||
|
||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
|
||
SharedCustodyPeriod.objects.create(
|
||
holiday_rule=rule, agenda=agenda, guardian=father, date_start='2022-02-03', date_end='2022-02-05'
|
||
)
|
||
|
||
assert agenda.period_overlaps('2022-02-03', '2022-02-05') is False
|
||
|
||
|
||
def test_shared_custody_agenda_rule_label():
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)))
|
||
assert rule.label == 'daily'
|
||
|
||
rule.days = [1, 2, 3, 4]
|
||
rule.save()
|
||
assert rule.label == 'from Tuesday to Friday'
|
||
|
||
rule.days = [4, 5, 6]
|
||
rule.save()
|
||
assert rule.label == 'from Friday to Sunday'
|
||
|
||
rule.days = [1, 4, 6]
|
||
rule.save()
|
||
assert rule.label == 'on Tuesdays, Fridays, Sundays'
|
||
|
||
rule.days = [0]
|
||
rule.weeks = 'even'
|
||
rule.save()
|
||
assert rule.label == 'on Mondays, on even weeks'
|
||
|
||
rule.weeks = 'odd'
|
||
rule.save()
|
||
assert rule.label == 'on Mondays, on odd weeks'
|
||
|
||
|
||
def test_shared_custody_agenda_holiday_rule_label():
|
||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
summer_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Summer Holidays', slug='summer'
|
||
)
|
||
|
||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
|
||
assert rule.label == 'Summer Holidays'
|
||
|
||
rule.years = 'even'
|
||
rule.save()
|
||
assert rule.label == 'Summer Holidays, on even years'
|
||
|
||
rule.years = 'odd'
|
||
rule.save()
|
||
assert rule.label == 'Summer Holidays, on odd years'
|
||
|
||
rule.periodicity = 'first-half'
|
||
rule.save()
|
||
assert rule.label == 'Summer Holidays, the first half, on odd years'
|
||
|
||
rule.years = ''
|
||
rule.periodicity = 'second-half'
|
||
rule.save()
|
||
assert rule.label == 'Summer Holidays, the second half'
|
||
|
||
rule.periodicity = 'first-and-third-quarters'
|
||
rule.save()
|
||
assert rule.label == 'Summer Holidays, the first and third quarters'
|
||
|
||
rule.periodicity = 'second-and-fourth-quarters'
|
||
rule.save()
|
||
assert rule.label == 'Summer Holidays, the second and fourth quarters'
|
||
|
||
|
||
def test_shared_custody_agenda_period_label(freezer):
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
period = SharedCustodyPeriod.objects.create(
|
||
agenda=agenda,
|
||
guardian=father,
|
||
date_start=datetime.date(2021, 7, 10),
|
||
date_end=datetime.date(2021, 7, 11),
|
||
)
|
||
assert str(period) == 'John Doe, 07/10/2021'
|
||
|
||
period.date_end = datetime.date(2021, 7, 13)
|
||
period.save()
|
||
assert str(period) == 'John Doe, 07/10/2021 → 07/13/2021'
|
||
|
||
|
||
def test_shared_custody_agenda_holiday_rule_create_periods():
|
||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
summer_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Summer', slug='summer'
|
||
)
|
||
TimePeriodException.objects.create(
|
||
unavailability_calendar=calendar,
|
||
start_datetime=make_aware(datetime.datetime(year=2021, month=7, day=6, hour=0, minute=0)),
|
||
end_datetime=make_aware(datetime.datetime(year=2021, month=9, day=2, hour=0, minute=0)),
|
||
group=summer_holiday,
|
||
)
|
||
TimePeriodException.objects.create(
|
||
unavailability_calendar=calendar,
|
||
start_datetime=make_aware(datetime.datetime(year=2022, month=7, day=7, hour=0, minute=0)),
|
||
end_datetime=make_aware(datetime.datetime(year=2022, month=9, day=1, hour=0, minute=0)),
|
||
group=summer_holiday,
|
||
)
|
||
|
||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
|
||
rule.update_or_create_periods()
|
||
period1, period2 = SharedCustodyPeriod.objects.all()
|
||
assert period1.holiday_rule == rule
|
||
assert period1.guardian == father
|
||
assert period1.agenda == agenda
|
||
assert period1.date_start == datetime.date(year=2021, month=7, day=6)
|
||
assert period1.date_end == datetime.date(year=2021, month=9, day=2)
|
||
assert period2.holiday_rule == rule
|
||
assert period2.guardian == father
|
||
assert period2.agenda == agenda
|
||
assert period2.date_start == datetime.date(year=2022, month=7, day=7)
|
||
assert period2.date_end == datetime.date(year=2022, month=9, day=1)
|
||
|
||
rule.years = 'odd'
|
||
rule.update_or_create_periods()
|
||
period = SharedCustodyPeriod.objects.get()
|
||
assert period.date_start == datetime.date(year=2021, month=7, day=6)
|
||
assert period.date_end == datetime.date(year=2021, month=9, day=2)
|
||
|
||
rule.years = 'even'
|
||
rule.update_or_create_periods()
|
||
period = SharedCustodyPeriod.objects.get()
|
||
assert period.date_start == datetime.date(year=2022, month=7, day=7)
|
||
assert period.date_end == datetime.date(year=2022, month=9, day=1)
|
||
|
||
rule.years = ''
|
||
rule.periodicity = 'first-half'
|
||
rule.update_or_create_periods()
|
||
period1, period2 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=7, day=6)
|
||
assert period1.date_end == datetime.date(year=2021, month=8, day=1)
|
||
assert period1.date_end.weekday() == 6
|
||
assert period2.date_start == datetime.date(year=2022, month=7, day=7)
|
||
assert period2.date_end == datetime.date(year=2022, month=7, day=31)
|
||
assert period2.date_end.weekday() == 6
|
||
|
||
rule.periodicity = 'second-half'
|
||
rule.update_or_create_periods()
|
||
period1, period2 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=8, day=1)
|
||
assert period1.date_start.weekday() == 6
|
||
assert period1.date_end == datetime.date(year=2021, month=9, day=2)
|
||
assert period2.date_start == datetime.date(year=2022, month=7, day=31)
|
||
assert period2.date_start.weekday() == 6
|
||
assert period2.date_end == datetime.date(year=2022, month=9, day=1)
|
||
|
||
rule.periodicity = 'first-and-third-quarters'
|
||
rule.update_or_create_periods()
|
||
period1, period2, period3, period4 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=7, day=6)
|
||
assert period1.date_end == datetime.date(year=2021, month=7, day=25)
|
||
assert period1.date_end.weekday() == 6
|
||
assert period2.date_start == datetime.date(year=2021, month=8, day=8)
|
||
assert period2.date_end == datetime.date(year=2021, month=8, day=22)
|
||
assert period2.date_end.weekday() == 6
|
||
assert period3.date_start == datetime.date(year=2022, month=7, day=7)
|
||
assert period3.date_end == datetime.date(year=2022, month=7, day=24)
|
||
assert period3.date_end.weekday() == 6
|
||
assert period4.date_start == datetime.date(year=2022, month=8, day=7)
|
||
assert period4.date_end == datetime.date(year=2022, month=8, day=21)
|
||
assert period4.date_end.weekday() == 6
|
||
|
||
rule.periodicity = 'second-and-fourth-quarters'
|
||
rule.update_or_create_periods()
|
||
period1, period2, period3, period4 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=7, day=25)
|
||
assert period1.date_start.weekday() == 6
|
||
assert period1.date_end == datetime.date(year=2021, month=8, day=8)
|
||
assert period2.date_start == datetime.date(year=2021, month=8, day=22)
|
||
assert period2.date_start.weekday() == 6
|
||
assert period2.date_end == datetime.date(year=2021, month=9, day=2)
|
||
assert period3.date_start == datetime.date(year=2022, month=7, day=24)
|
||
assert period3.date_start.weekday() == 6
|
||
assert period3.date_end == datetime.date(year=2022, month=8, day=7)
|
||
assert period4.date_start == datetime.date(year=2022, month=8, day=21)
|
||
assert period4.date_start.weekday() == 6
|
||
assert period4.date_end == datetime.date(year=2022, month=9, day=1)
|
||
|
||
|
||
def test_shared_custody_agenda_holiday_rule_create_periods_christmas_holidays():
|
||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
christmas_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Christmas', slug='christmas'
|
||
)
|
||
TimePeriodException.objects.create(
|
||
unavailability_calendar=calendar,
|
||
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
|
||
end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
|
||
group=christmas_holiday,
|
||
)
|
||
TimePeriodException.objects.create(
|
||
unavailability_calendar=calendar,
|
||
start_datetime=make_aware(datetime.datetime(year=2022, month=12, day=17, hour=0, minute=0)),
|
||
end_datetime=make_aware(datetime.datetime(year=2023, month=1, day=3, hour=0, minute=0)),
|
||
group=christmas_holiday,
|
||
)
|
||
|
||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
|
||
rule.update_or_create_periods()
|
||
period1, period2 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=12, day=18)
|
||
assert period1.date_end == datetime.date(year=2022, month=1, day=3)
|
||
assert period2.date_start == datetime.date(year=2022, month=12, day=17)
|
||
assert period2.date_end == datetime.date(year=2023, month=1, day=3)
|
||
|
||
rule.periodicity = 'first-half'
|
||
rule.update_or_create_periods()
|
||
period1, period2 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=12, day=18)
|
||
assert period1.date_end == datetime.date(year=2021, month=12, day=26)
|
||
assert period1.date_end.weekday() == 6
|
||
assert period2.date_start == datetime.date(year=2022, month=12, day=17)
|
||
assert period2.date_end == datetime.date(year=2022, month=12, day=25)
|
||
assert period2.date_end.weekday() == 6
|
||
|
||
rule.periodicity = 'second-half'
|
||
rule.update_or_create_periods()
|
||
period1, period2 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=12, day=26)
|
||
assert period1.date_start.weekday() == 6
|
||
assert period1.date_end == datetime.date(year=2022, month=1, day=3)
|
||
assert period2.date_start == datetime.date(year=2022, month=12, day=25)
|
||
assert period2.date_start.weekday() == 6
|
||
assert period2.date_end == datetime.date(year=2023, month=1, day=3)
|
||
|
||
rule.periodicity = 'first-and-third-quarters'
|
||
rule.update_or_create_periods()
|
||
assert not SharedCustodyPeriod.objects.exists()
|
||
|
||
rule.periodicity = 'second-and-fourth-quarters'
|
||
rule.update_or_create_periods()
|
||
assert not SharedCustodyPeriod.objects.exists()
|
||
|
||
|
||
def test_shared_custody_agenda_holiday_rules_application():
|
||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
christmas_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Christmas', slug='christmas'
|
||
)
|
||
TimePeriodException.objects.create(
|
||
unavailability_calendar=calendar,
|
||
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
|
||
end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
|
||
group=christmas_holiday,
|
||
)
|
||
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
|
||
|
||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
|
||
rule.update_or_create_periods()
|
||
|
||
date_start = datetime.date(year=2021, month=12, day=13) # Monday, even week
|
||
slots = agenda.get_custody_slots(date_start, date_start + datetime.timedelta(days=30))
|
||
guardians = [str(x.guardian) for x in slots]
|
||
assert all(name == 'John Doe' for name in guardians[:21])
|
||
assert all(name == 'Jane Doe' for name in guardians[21:28])
|
||
assert all(name == 'John Doe' for name in guardians[28:])
|
||
|
||
# check exceptional custody periods take precedence over holiday rules
|
||
SharedCustodyPeriod.objects.create(
|
||
agenda=agenda,
|
||
guardian=mother,
|
||
date_start=datetime.date(year=2021, month=12, day=27),
|
||
date_end=datetime.date(year=2021, month=12, day=29),
|
||
)
|
||
slots = agenda.get_custody_slots(date_start, date_start + datetime.timedelta(days=30))
|
||
slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots]
|
||
assert slots[13:17] == [
|
||
('26/12', 'John Doe'),
|
||
('27/12', 'Jane Doe'),
|
||
('28/12', 'Jane Doe'),
|
||
('29/12', 'John Doe'),
|
||
]
|
||
|
||
|
||
def test_shared_custody_agenda_update_holiday_rules_command():
|
||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||
)
|
||
|
||
christmas_holiday = TimePeriodExceptionGroup.objects.create(
|
||
unavailability_calendar=calendar, label='Christmas', slug='christmas'
|
||
)
|
||
exception = TimePeriodException.objects.create(
|
||
unavailability_calendar=calendar,
|
||
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
|
||
end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
|
||
group=christmas_holiday,
|
||
)
|
||
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
|
||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
|
||
|
||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
|
||
rule.update_or_create_periods()
|
||
|
||
period = SharedCustodyPeriod.objects.get()
|
||
assert period.date_start == datetime.date(year=2021, month=12, day=18)
|
||
assert period.date_end == datetime.date(year=2022, month=1, day=3)
|
||
|
||
exception.start_datetime += datetime.timedelta(days=1)
|
||
exception.save()
|
||
TimePeriodException.objects.create(
|
||
unavailability_calendar=calendar,
|
||
start_datetime=make_aware(datetime.datetime(year=2022, month=12, day=18, hour=0, minute=0)),
|
||
end_datetime=make_aware(datetime.datetime(year=2023, month=1, day=3, hour=0, minute=0)),
|
||
group=christmas_holiday,
|
||
)
|
||
|
||
call_command('update_shared_custody_holiday_rules')
|
||
period1, period2 = SharedCustodyPeriod.objects.all()
|
||
assert period1.date_start == datetime.date(year=2021, month=12, day=19)
|
||
assert period1.date_end == datetime.date(year=2022, month=1, day=3)
|
||
assert period2.date_start == datetime.date(year=2022, month=12, day=18)
|
||
assert period2.date_end == datetime.date(year=2023, month=1, day=3)
|
||
|
||
|
||
def test_shared_custody_agenda_unique_child_no_date_end():
|
||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||
child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe')
|
||
SharedCustodyAgenda.objects.create(
|
||
first_guardian=father,
|
||
second_guardian=mother,
|
||
child=child,
|
||
date_start=datetime.date(2020, 1, 1),
|
||
date_end=datetime.date(2021, 1, 1),
|
||
)
|
||
agenda = SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=datetime.date(2022, 1, 1)
|
||
)
|
||
|
||
with pytest.raises(IntegrityError):
|
||
with transaction.atomic():
|
||
SharedCustodyAgenda.objects.create(
|
||
first_guardian=father,
|
||
second_guardian=mother,
|
||
child=child,
|
||
date_start=datetime.date(2023, 1, 1),
|
||
)
|
||
|
||
agenda.date_end = datetime.date(2022, 6, 1)
|
||
agenda.save()
|
||
SharedCustodyAgenda.objects.create(
|
||
first_guardian=father, second_guardian=mother, child=child, date_start=datetime.date(2023, 1, 1)
|
||
)
|