chrono/tests/test_agendas.py

4408 lines
176 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 django.test.utils import CaptureQueriesContext
from chrono.agendas.models import (
Agenda,
AgendaNotificationsSettings,
AgendaReminderSettings,
Booking,
BookingCheck,
Category,
Desk,
Event,
EventCancellationReport,
ICSError,
MeetingType,
Person,
Resource,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
TimePeriod,
TimePeriodException,
TimePeriodExceptionGroup,
TimePeriodExceptionSource,
UnavailabilityCalendar,
VirtualMember,
)
from chrono.utils.interval import IntervalSet
from chrono.utils.timezone import localtime, make_aware, make_naive, now
from .utils import add_day_timeperiod, add_exception, add_meeting, build_agendas, paris, utc
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 = build_agendas('meetings 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, 0, 0)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 0, 0)),
# summer DST change on sunday 28th
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 29, 0, 0)),
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 0, 0)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 0, 0)),
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 0, 0)),
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 0, 0)),
# winter DST change on sunday 31th
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 0, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 0, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 0, 0)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 0, 0)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 0, 0)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 0, 0)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 0, 0)),
],
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 = build_agendas('meetings 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, 0, 0)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 24, 0, 0)),
# summer DST change on sunday 28th
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 31, 0, 0)),
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 31, 0, 0)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 31, 0, 0)),
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 0, 0)),
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 0, 0)),
# winter DST change on sunday 31th
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 5, 0, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 5, 0, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 5, 0, 0)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 5, 0, 0)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 5, 0, 0)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 5, 0, 0)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 5, 0, 0)),
],
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, 0, 0)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 0, 0)),
# summer DST change on sunday
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 0, 0)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 0, 0)),
# winter DST change on sunday
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 0, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 0, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 0, 0)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 0, 0)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 0, 0)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 0, 0)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 0, 0)),
],
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, 12, 8)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 21, 7)),
# summer DST change on sunday 28th
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 28, 1, 30)),
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 28, 3, 30)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 28, 3, 30)),
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 3, 31, 1, 30)),
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 3, 31, 3, 30)),
# winter DST change on sunday 31th
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 1, 1, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 1, 2, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 1, 2, 30)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 3, 1, 30)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 3, 2, 30)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 3, 2, 30)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 3, 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, 0)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 21, 0)),
# summer DST change on sunday
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 28, 0)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 28, 0)),
# winter DST change on sunday
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 1, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 1, 0)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 1, 0)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 3, 0)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 3, 0)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 3, 0)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 3, 0)),
],
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 = build_agendas(
'''\
meetings "Test 1 agenda"
desk "Test 1 desk"
exception-source sample.ics ICS_SAMPLE'''
)
agenda._test_1_desk._sample_ics.refresh_timeperiod_exceptions_from_ics()
assert TimePeriodException.objects.filter(desk=agenda._test_1_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 = build_agendas(
'''\
meetings Agenda
desk Desk
'''
)
desk = agenda._desk
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_.isoweekday()],
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].bcc == expected_recipients
for addr in expected_recipients:
assert addr not in mailoutbox[0].to
assert addr in mailoutbox[0].recipients()
assert len(mailoutbox[0].recipients()) == len(expected_recipients) + 1
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].bcc == recipients
for addr in recipients:
assert addr not in mailoutbox[0].to
assert addr in mailoutbox[0].recipients()
assert len(mailoutbox[0].recipients()) == len(recipients) + 1
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
)
html, mime = mailoutbox[0].alternatives[0]
assert mime == 'text/html'
assert '<a href="https://example.com/manage/agendas/%s/events/%s/">' % (agenda.pk, event.pk) in html
# 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].bcc == recipients
for addr in recipients:
assert addr not in mailoutbox[0].to
assert addr in mailoutbox[0].recipients()
assert len(mailoutbox[0].recipients()) == len(recipients) + 1
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 = Booking.objects.create(event=event, user_email='t@test.org')
# extra booking with no email, should be ignored
Booking.objects.create(event=event)
# secondary booking, should be ignored
Booking.objects.create(event=event, user_email='t@test.org', primary_booking=booking)
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 &quot;receipt&quot;.</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(utc('2020-01-01 11:00'))
agenda = build_agendas(
'''\
meetings Events
desk Desk
timeperiod wednesday 10:00-18:00
meeting-type Bar 30
reminder-setting days_before_email=2
'''
)
add_meeting(
agenda,
start_datetime=utc('2020-01-06T11:00:00'),
user_email='t@test.org',
user_display_label='Birth certificate',
form_url='publik://default/someform/1/',
)
freezer.move_to(utc('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().isoweekday()],
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().isoweekday()], 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(1, 8)), # 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(1, 6)) # 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).isoweekday()]
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 = [4] # 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(1, 8)),
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(1, 8)),
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(1, 8)),
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().isoweekday()],
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().isoweekday()],
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(1, 8)),
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(1, 8)),
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=[2],
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(1, 8)),
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(1, 8)),
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 = [2, 3, 4, 5]
event.save()
assert event.get_recurrence_display() == 'From Tuesday to Friday at 1:30 p.m.'
event.recurrence_days = [5, 6, 7]
event.save()
assert event.get_recurrence_display() == 'From Friday to Sunday at 1:30 p.m.'
event.recurrence_days = [2, 5, 7]
event.save()
assert event.get_recurrence_display() == 'On Tuesdays, Fridays, Sundays at 1:30 p.m.'
event.recurrence_days = [1]
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'
)
@pytest.mark.parametrize('partial_bookings', [True, False])
def test_event_triggered_fields(partial_bookings):
full_flags_true_value = not (partial_bookings)
# 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', partial_bookings=partial_bookings)
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 == full_flags_true_value
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 == full_flags_true_value
assert event.full == full_flags_true_value
# 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 == full_flags_true_value
assert event.full == full_flags_true_value
# 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 == full_flags_true_value
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 == full_flags_true_value
assert event.full == full_flags_true_value
# 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 == full_flags_true_value
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 == full_flags_true_value
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 == full_flags_true_value
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 == full_flags_true_value
assert event.full == full_flags_true_value
# 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 == full_flags_true_value
assert event.full == full_flags_true_value
# 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 == full_flags_true_value
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 == full_flags_true_value
assert event.full == full_flags_true_value
# 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 == full_flags_true_value
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 == full_flags_true_value
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(1, 8)),
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(1, 8)), weeks='even', guardian=father)
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), 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=[2, 3, 4], guardian=father)
SharedCustodyRule.objects.create(agenda=agenda, days=[1, 5, 6, 7], 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(1, 8)))
assert rule.label == 'daily'
rule.days = [2, 3, 4, 5]
rule.save()
assert rule.label == 'from Tuesday to Friday'
rule.days = [5, 6, 7]
rule.save()
assert rule.label == 'from Friday to Sunday'
rule.days = [2, 5, 7]
rule.save()
assert rule.label == 'on Tuesdays, Fridays, Sundays'
rule.days = [1]
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(1, 8)), weeks='even', guardian=father)
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), 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(1, 8)), weeks='even', guardian=father)
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), 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)
)
def test_meetings_agenda_get_free_time(db, freezer):
freezer.move_to(paris('2023-04-02 19:10'))
agenda = build_agendas(
'''
meetings Agenda 30
desk "Desk 1" monday-friday 08:00-12:00 14:00-17:00
unavailability-calendar Congés
exception Avril paris('2023-04-05 00:00:00 24h')
desk "Desk 2" monday-friday 09:00-12:00
unavailability-calendar Congés
desk "Desk 3" monday-friday 15:00-17:00
unavailability-calendar Congés
'''
)
full_free_time = IntervalSet(
[
paris('2023-04-03 08:00 4h'),
paris('2023-04-03 14:00 3h'),
paris('2023-04-04 08:00 4h'),
paris('2023-04-04 14:00 3h'),
paris('2023-04-06 08:00 4h'),
paris('2023-04-06 14:00 3h'),
paris('2023-04-07 08:00 4h'),
paris('2023-04-07 14:00 3h'),
paris('2023-04-08 08:00 2h'),
]
)
def closed():
return full_free_time - agenda.get_free_time(end_datetime=paris('2023-04-09 19:10'))
add_day_timeperiod(agenda._desk_1, paris('2023-04-08 08:00 2h'))
assert agenda.get_free_time(end_datetime=paris('2023-04-09 19:10')) == full_free_time
assert full_free_time - agenda.get_free_time(end_datetime=paris('2023-04-09 19:10')) == []
assert closed() == []
assert (
agenda.get_free_time(start_datetime=paris('2023-04-03 13:00'), end_datetime=paris('2023-04-09 19:10'))
== list(full_free_time)[1:]
)
# desk1 is closed on monday 3th april form 8' to 12' but
# desk2 is still open from 9' to 12', so free time is only
# diminished of the 8' to 9' hour.
add_exception(agenda._desk_1, paris('2023-04-03 08:00 4h'))
assert closed() == [paris('2023-04-03 08:00 1h')]
# now close desk2 from 11' to 12' on monday 3th
add_exception(agenda._desk_2, paris('2023-04-03 11:00 1h'))
assert closed() == [paris('2023-04-03 08:00 1h'), paris('2023-04-03 11:00 1h')]
# add a meeting on tuesday 4th april on desk1 at 14'30
add_meeting(agenda._desk_1, paris('2023-04-04 14:30'))
# an event on 5th at 15' but slot should still be open on desk3
add_meeting(agenda._desk_1, paris('2023-04-05 15:00'))
# two events on 6th at 15', slot is closed
add_meeting(agenda._desk_1, paris('2023-04-06 15:00'))
add_meeting(agenda._desk_3, paris('2023-04-06 15:00'))
assert closed() == [
paris('2023-04-03 08:00 1h'),
paris('2023-04-03 11:00 1h'),
paris('2023-04-04 14:30 30m'),
paris('2023-04-06 15:00 30m'),
]
def test_virtual_agenda_get_free_time(db, freezer):
freezer.move_to(paris('2023-04-02 19:10'))
# context:
# * a virtual agenda, containing:
# * three real agendas, with timperiods of monday to friday and a 30 minutes meeting type
# - agenda 1. 8-12h/14-17h
# - agenda 2. 9-12h
# - agenda 3. 15-17h
# * looking at week from monday 3th april 2023 to friday 7th april 2023
agenda = build_agendas(
'''
virtual Agenda 30
meetings Agenda-1
desk Desk-1 monday-friday 08:00-12:00 14:00-17:00
unavailability-calendar Congés
exception Avril paris('2023-04-05 00:00:00 24h')
meetings Agenda-2
desk Desk-1 monday-friday 09:00-12:00
unavailability-calendar Congés
meetings Agenda-3
desk Desk-1 monday-friday 15:00-17:00
unavailability-calendar Congés
'''
)
full_free_time = IntervalSet(
[
paris('2023-04-03 08:00 4h'),
paris('2023-04-03 14:00 3h'),
paris('2023-04-04 08:00 4h'),
paris('2023-04-04 14:00 3h'),
paris('2023-04-06 08:00 4h'),
paris('2023-04-06 14:00 3h'),
paris('2023-04-07 08:00 4h'),
paris('2023-04-07 14:00 3h'),
paris('2023-04-08 08:00 2h'),
]
)
def closed():
return full_free_time - agenda.get_free_time(end_datetime=paris('2023-04-09 19:10'))
add_day_timeperiod(agenda._agenda_1, paris('2023-04-08 08:00 2h'))
assert agenda.get_free_time(end_datetime=paris('2023-04-09 19:10')) == full_free_time
assert (
agenda.get_free_time(start_datetime=paris('2023-04-03 13:00'), end_datetime=paris('2023-04-09 19:10'))
== list(full_free_time)[1:]
)
# agenda1.desk is closed on monday 3th april form 8' to 12' but
# agenda2.desk is still open from 9' to 12', so free time is only
# diminished of the 8' to 9' hour.
add_exception(agenda._agenda_1, paris('2023-04-03 08:00 4h'))
assert closed() == [paris('2023-04-03 08:00 1h')]
# now close agenda2.desk from 11' to 12' on monday 3th
add_exception(agenda._agenda_2, paris('2023-04-03 11:00 1h'))
assert closed() == [paris('2023-04-03 08:00 1h'), paris('2023-04-03 11:00 1h')]
# add a meeting on tuesday 4th april on agenda1.desk at 14'30
add_meeting(agenda._agenda_1, paris('2023-04-04 14:30'))
# an event on 5th at 15' but slot should still be open on agenda3
add_meeting(agenda._agenda_1, paris('2023-04-05 15:00'))
# two events on 6th at 15', slot is closed
add_meeting(agenda._agenda_1, paris('2023-04-06 15:00'))
add_meeting(agenda._agenda_3, paris('2023-04-06 15:00'))
assert closed() == [
paris('2023-04-03 08:00 1h'),
paris('2023-04-03 11:00 1h'),
paris('2023-04-04 14:30 30m'),
paris('2023-04-06 15:00 30m'),
]
def test_agenda_event_overlaps():
agenda = Agenda.objects.create(label='Agenda', kind='events')
start_datetime = make_aware(datetime.datetime(2023, 5, 1, 14, 00))
event_kwargs = {
'start_datetime': start_datetime,
'recurrence_days': None,
'recurrence_end_date': None,
}
assert agenda.event_overlaps(**event_kwargs) is False
event = Event.objects.create(start_datetime=start_datetime, agenda=agenda, places=1)
assert agenda.event_overlaps(**event_kwargs) is True
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
event_kwargs['start_datetime'] = start_datetime + datetime.timedelta(days=1)
assert agenda.event_overlaps(**event_kwargs) is False
event_kwargs['start_datetime'] = start_datetime.replace(month=2)
assert agenda.event_overlaps(**event_kwargs) is False
# recurring event
event_kwargs = {
'start_datetime': start_datetime,
'recurrence_days': [1, 2],
'recurrence_end_date': None,
}
assert agenda.event_overlaps(**event_kwargs) is True
# same weekday, starts after
event_kwargs['start_datetime'] = start_datetime + datetime.timedelta(days=7)
assert agenda.event_overlaps(**event_kwargs) is False
# same weekday, starts before
event_kwargs['start_datetime'] = start_datetime - datetime.timedelta(days=7)
assert agenda.event_overlaps(**event_kwargs) is True
# same weekday, starts before
event_kwargs['start_datetime'] = start_datetime - datetime.timedelta(days=6)
assert agenda.event_overlaps(**event_kwargs) is True
# different weekday, starts before
event_kwargs['recurrence_days'] = [3]
assert agenda.event_overlaps(**event_kwargs) is False
# same weekday, starts before, ends before
event_kwargs = {
'start_datetime': start_datetime - datetime.timedelta(days=30),
'recurrence_days': [1, 2],
'recurrence_end_date': start_datetime - datetime.timedelta(days=15),
}
assert agenda.event_overlaps(**event_kwargs) is False
# same weekday, starts before, ends after
event_kwargs['recurrence_end_date'] = start_datetime + datetime.timedelta(days=15)
assert agenda.event_overlaps(**event_kwargs) is True
def test_agenda_event_overlaps_recurring():
agenda = Agenda.objects.create(label='Agenda', kind='events')
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
'recurrence_days': [1, 2],
'recurrence_end_date': datetime.date(2023, 6, 1),
}
assert agenda.event_overlaps(**event_kwargs) is False
event = Event.objects.create(agenda=agenda, places=1, **event_kwargs)
event.create_all_recurrences()
assert agenda.event_overlaps(**event_kwargs) is True
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
# starts before, ends during existing event
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 4, 1, 14, 00)),
'recurrence_days': [1],
'recurrence_end_date': datetime.date(2023, 5, 15),
}
assert agenda.event_overlaps(**event_kwargs) is True
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
# starts during, ends after existing event
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 5, 15, 14, 00)),
'recurrence_days': [2],
'recurrence_end_date': datetime.date(2023, 6, 15),
}
assert agenda.event_overlaps(**event_kwargs) is True
# not the same day
event_kwargs['recurrence_days'] = [3, 4]
assert agenda.event_overlaps(**event_kwargs) is False
# starts after
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 6, 15, 14, 00)),
'recurrence_days': [2],
'recurrence_end_date': datetime.date(2023, 7, 15),
}
assert agenda.event_overlaps(**event_kwargs) is False
# remove recurrence_end_date from event
event.recurrence_end_date = None
event.save()
assert agenda.event_overlaps(**event_kwargs) is True
# starts before, ends before
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 4, 1, 14, 00)),
'recurrence_days': [2],
'recurrence_end_date': datetime.date(2023, 4, 20),
}
assert agenda.event_overlaps(**event_kwargs) is False
# starts before, not end
event_kwargs['recurrence_end_date'] = None
assert agenda.event_overlaps(**event_kwargs) is True
# normal event, before
event_kwargs['recurrence_days'] = None
assert agenda.event_overlaps(**event_kwargs) is False
# normal event, after on a recurrence day
event_kwargs['start_datetime'] = make_aware(datetime.datetime(2023, 5, 9, 14, 00))
assert agenda.event_overlaps(**event_kwargs) is True
# normal event, after not on a recurrence day
event_kwargs['start_datetime'] = make_aware(datetime.datetime(2023, 5, 10, 14, 00))
assert agenda.event_overlaps(**event_kwargs) is False
@pytest.mark.parametrize(
'start_time, user_check_start_time, tolerance, unit, expected',
[
# no check
(None, None, 0, 'hour', None),
(None, None, 0, 'half_hour', None),
(None, None, 0, 'quarter', None),
(None, None, 0, 'minutes', None),
# hour unit - no booking
(None, datetime.time(7, 50), 10, 'hour', datetime.time(8, 0)),
(None, datetime.time(7, 49), 10, 'hour', datetime.time(7, 0)),
(None, datetime.time(7, 50), 0, 'hour', datetime.time(7, 0)),
# hour unit - with booking
(datetime.time(8, 0), datetime.time(7, 50), 10, 'hour', datetime.time(8, 0)),
(datetime.time(8, 0), datetime.time(7, 49), 10, 'hour', datetime.time(7, 0)),
(datetime.time(8, 0), datetime.time(8, 30), 10, 'hour', datetime.time(8, 0)),
(datetime.time(8, 0), datetime.time(7, 50), 0, 'hour', datetime.time(7, 0)),
# half_hour unit - no booking
(None, datetime.time(7, 50), 10, 'half_hour', datetime.time(8, 0)),
(None, datetime.time(7, 49), 10, 'half_hour', datetime.time(7, 30)),
(None, datetime.time(7, 50), 0, 'half_hour', datetime.time(7, 30)),
# half_hour unit - with booking
(datetime.time(8, 0), datetime.time(7, 50), 10, 'half_hour', datetime.time(8, 0)),
(datetime.time(8, 0), datetime.time(7, 49), 10, 'half_hour', datetime.time(7, 30)),
(datetime.time(8, 0), datetime.time(8, 30), 10, 'half_hour', datetime.time(8, 0)),
(datetime.time(8, 0), datetime.time(7, 50), 0, 'half_hour', datetime.time(7, 30)),
# quarter unit - no booking
(None, datetime.time(7, 50), 10, 'quarter', datetime.time(8, 0)),
(None, datetime.time(7, 49), 10, 'quarter', datetime.time(7, 45)),
(None, datetime.time(7, 50), 0, 'quarter', datetime.time(7, 45)),
# quarter unit - with booking
(datetime.time(8, 0), datetime.time(7, 50), 10, 'quarter', datetime.time(8, 0)),
(datetime.time(8, 0), datetime.time(7, 49), 10, 'quarter', datetime.time(7, 45)),
(datetime.time(8, 0), datetime.time(8, 30), 10, 'quarter', datetime.time(8, 0)),
(datetime.time(8, 0), datetime.time(7, 50), 0, 'quarter', datetime.time(7, 45)),
# minute unit - no booking
(None, datetime.time(7, 50), 0, 'minute', datetime.time(7, 50)),
# minute unit - with booking
(datetime.time(8, 0), datetime.time(7, 50), 0, 'minute', datetime.time(7, 50)),
(datetime.time(8, 0), datetime.time(8, 5), 0, 'minute', datetime.time(8, 0)),
],
)
def test_booking_get_computed_start_time(start_time, user_check_start_time, tolerance, unit, expected):
agenda = Agenda.objects.create(
label='Agenda', kind='events', invoicing_unit=unit, invoicing_tolerance=tolerance
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(event=event, start_time=start_time)
booking.mark_user_presence(start_time=user_check_start_time)
assert booking.user_check.get_computed_start_time() == expected
@pytest.mark.parametrize(
'end_time, user_check_end_time, tolerance, unit, expected',
[
# no check
(None, None, 0, 'hour', None),
(None, None, 0, 'half_hour', None),
(None, None, 0, 'quarter', None),
(None, None, 0, 'minutes', None),
# hour unit - no booking
(None, datetime.time(17, 10), 10, 'hour', datetime.time(17, 0)),
(None, datetime.time(17, 11), 10, 'hour', datetime.time(18, 0)),
(None, datetime.time(17, 10), 0, 'hour', datetime.time(18, 0)),
# hour unit - with booking
(datetime.time(17, 0), datetime.time(17, 10), 10, 'hour', datetime.time(17, 0)),
(datetime.time(17, 0), datetime.time(17, 11), 10, 'hour', datetime.time(18, 0)),
(datetime.time(17, 0), datetime.time(16, 30), 10, 'hour', datetime.time(17, 0)),
(datetime.time(17, 0), datetime.time(17, 10), 0, 'hour', datetime.time(18, 0)),
# half_hour unit - no booking
(None, datetime.time(17, 10), 10, 'half_hour', datetime.time(17, 0)),
(None, datetime.time(17, 11), 10, 'half_hour', datetime.time(17, 30)),
(None, datetime.time(17, 10), 0, 'half_hour', datetime.time(17, 30)),
# half_hour unit - with booking
(datetime.time(17, 0), datetime.time(17, 10), 10, 'half_hour', datetime.time(17, 0)),
(datetime.time(17, 0), datetime.time(17, 11), 10, 'half_hour', datetime.time(17, 30)),
(datetime.time(17, 0), datetime.time(16, 30), 10, 'half_hour', datetime.time(17, 0)),
(datetime.time(17, 0), datetime.time(17, 10), 0, 'half_hour', datetime.time(17, 30)),
# quarter unit - no booking
(None, datetime.time(17, 10), 10, 'quarter', datetime.time(17, 0)),
(None, datetime.time(17, 11), 10, 'quarter', datetime.time(17, 15)),
(None, datetime.time(17, 10), 0, 'quarter', datetime.time(17, 15)),
# quarter unit - with booking
(datetime.time(17, 0), datetime.time(17, 10), 10, 'quarter', datetime.time(17, 0)),
(datetime.time(17, 0), datetime.time(17, 11), 10, 'quarter', datetime.time(17, 15)),
(datetime.time(17, 0), datetime.time(16, 30), 10, 'quarter', datetime.time(17, 0)),
(datetime.time(17, 0), datetime.time(17, 10), 0, 'quarter', datetime.time(17, 15)),
# minute unit - no booking
(None, datetime.time(17, 10), 0, 'minute', datetime.time(17, 10)),
# minute unit - with booking
(datetime.time(17, 0), datetime.time(17, 10), 0, 'minute', datetime.time(17, 10)),
(datetime.time(17, 0), datetime.time(16, 50), 0, 'minute', datetime.time(17, 0)),
],
)
def test_booking_get_computed_end_time(end_time, user_check_end_time, tolerance, unit, expected):
agenda = Agenda.objects.create(
label='Agenda', kind='events', invoicing_unit=unit, invoicing_tolerance=tolerance
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(event=event, end_time=end_time)
booking.mark_user_presence(end_time=user_check_end_time)
assert booking.user_check.get_computed_end_time() == expected
def test_booking_refresh_computed_times_only_one_user_check():
agenda = Agenda.objects.create(
label='Agenda', kind='events', invoicing_unit='half_hour', invoicing_tolerance=10
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(
event=event, start_time=datetime.time(8, 0), end_time=datetime.time(17, 0)
)
# only one user_check, presence or absence, same behavior
booking_check = BookingCheck.objects.create(
booking=booking, presence=True, start_time=datetime.time(7, 45), end_time=datetime.time(17, 15)
)
booking.refresh_computed_times(commit=True)
booking_check.refresh_from_db()
assert booking_check.computed_start_time == datetime.time(7, 30)
assert booking_check.computed_end_time == datetime.time(17, 30)
booking_check.presence = False
booking.refresh_computed_times(commit=True)
booking_check.refresh_from_db()
assert booking_check.computed_start_time == datetime.time(7, 30)
assert booking_check.computed_end_time == datetime.time(17, 30)
@pytest.mark.parametrize(
'presence1, start_time1, end_time1, presence2, start_time2, end_time2, expected_start1, expected_end1, expected_start2, expected_end2',
[
# first presence, then absence
(
True,
datetime.time(7, 45),
datetime.time(12, 15),
False,
datetime.time(13, 0), # no overlapping
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30),
datetime.time(17, 0),
),
(
True,
datetime.time(7, 45),
datetime.time(12, 15),
False,
datetime.time(12, 15), # computed start should be 12H00
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30), # but it is adjusted to 12H30
datetime.time(17, 0),
),
# first absence, then presence
(
False,
datetime.time(7, 45),
datetime.time(12, 0), # no overlapping
True,
datetime.time(13, 15),
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(13, 0),
datetime.time(13, 0),
datetime.time(17, 0),
),
(
False,
datetime.time(7, 45),
datetime.time(12, 45), # computed end should be 13H00
True,
datetime.time(12, 45),
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30), # but it is adjusted to 12H30
datetime.time(12, 30),
datetime.time(17, 0),
),
],
)
def test_booking_refresh_computed_times_two_user_check(
presence1,
start_time1,
end_time1,
presence2,
start_time2,
end_time2,
expected_start1,
expected_end1,
expected_start2,
expected_end2,
):
agenda = Agenda.objects.create(
label='Agenda', kind='events', invoicing_unit='half_hour', invoicing_tolerance=10
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(
event=event, start_time=datetime.time(8, 0), end_time=datetime.time(17, 0)
)
user_check1 = BookingCheck.objects.create(
booking=booking, presence=presence1, start_time=start_time1, end_time=end_time1
)
user_check2 = BookingCheck.objects.create(
booking=booking, presence=presence2, start_time=start_time2, end_time=end_time2
)
booking.refresh_computed_times(commit=True)
user_check1.refresh_from_db()
user_check2.refresh_from_db()
assert user_check1.computed_start_time == expected_start1
assert user_check1.computed_end_time == expected_end1
assert user_check2.computed_start_time == expected_start2
assert user_check2.computed_end_time == expected_end2
def test_agenda_refresh_booking_computed_times():
agenda = Agenda.objects.create(
label='Agenda',
kind='events',
partial_bookings=True,
)
agenda2 = Agenda.objects.create(label='other')
assert agenda.invoicing_unit == 'hour'
assert agenda.invoicing_tolerance == 0
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(
event=event,
start_time=datetime.time(8, 0),
end_time=datetime.time(17, 0),
)
booking.mark_user_presence(start_time=datetime.time(7, 55), end_time=datetime.time(17, 15))
def reset_booking():
booking.user_check.computed_start_time = None
booking.user_check.computed_end_time = None
booking.user_check.save()
def test_booking(success):
with CaptureQueriesContext(connection) as ctx:
agenda.refresh_booking_computed_times()
assert len(ctx.captured_queries) in [1, 3]
booking.refresh_from_db()
if success is True:
assert booking.user_check.computed_start_time == datetime.time(7, 0)
assert booking.user_check.computed_end_time == datetime.time(18, 0)
reset_booking()
else:
assert booking.user_check.computed_start_time is booking.user_check.computed_end_time is None
test_booking(True)
# wrong agenda kind
agenda.partial_bookings = False
agenda.save()
test_booking(False)
agenda.partial_bookings = True
agenda.kind = 'meetings'
agenda.save()
test_booking(False)
agenda.kind = 'virtual'
agenda.save()
test_booking(False)
# reset kind
agenda.kind = 'events'
agenda.save()
test_booking(True)
# wrong agenda
event.agenda = agenda2
event.save()
test_booking(False)
# event check locked
event.agenda = agenda
event.check_locked = True
event.save()
test_booking(False)
# event invoiced
event.check_locked = False
event.invoiced = True
event.save()
test_booking(False)
# event cancelled
event.invoiced = False
event.cancelled = True
event.save()
test_booking(False)
# booking cancelled
event.cancelled = False
event.save()
booking.cancellation_datetime = now()
booking.save()
test_booking(False)
# ok
booking.cancellation_datetime = None
booking.save()
test_booking(True)
def test_event_refresh_booking_computed_times():
agenda = Agenda.objects.create(
label='Agenda',
kind='events',
partial_bookings=True,
)
assert agenda.invoicing_unit == 'hour'
assert agenda.invoicing_tolerance == 0
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
event2 = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(
event=event,
start_time=datetime.time(8, 0),
end_time=datetime.time(17, 0),
)
booking.mark_user_presence(start_time=datetime.time(7, 55), end_time=datetime.time(17, 15))
def reset_booking():
booking.user_check.computed_start_time = None
booking.user_check.computed_end_time = None
booking.user_check.save()
def test_booking(success):
with CaptureQueriesContext(connection) as ctx:
event.refresh_booking_computed_times()
assert len(ctx.captured_queries) in [1, 3]
booking.refresh_from_db()
if success is True:
assert booking.user_check.computed_start_time == datetime.time(7, 0)
assert booking.user_check.computed_end_time == datetime.time(18, 0)
reset_booking()
else:
assert booking.user_check.computed_start_time is booking.user_check.computed_end_time is None
test_booking(True)
# wrong agenda kind
agenda.partial_bookings = False
agenda.save()
test_booking(False)
agenda.partial_bookings = True
agenda.kind = 'meetings'
agenda.save()
test_booking(False)
agenda.kind = 'virtual'
agenda.save()
test_booking(False)
# reset kind
agenda.kind = 'events'
agenda.save()
test_booking(True)
# wrong event
booking.event = event2
booking.save()
test_booking(False)
# event check locked
booking.event = event
booking.save()
event.check_locked = True
event.save()
test_booking(False)
# event invoiced
event.check_locked = False
event.invoiced = True
event.save()
test_booking(False)
# event cancelled
event.invoiced = False
event.cancelled = True
event.save()
test_booking(False)
# booking cancelled
event.cancelled = False
event.save()
booking.cancellation_datetime = now()
booking.save()
test_booking(False)
# ok
booking.cancellation_datetime = None
booking.save()
test_booking(True)
def test_agenda_booking_user_check_property():
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
booking = Booking.objects.create(event=event)
booking_check = BookingCheck.objects.create(booking=booking, presence=True)
assert booking.user_check == booking_check
BookingCheck.objects.create(booking=booking, presence=False)
booking.refresh_from_db()
with pytest.raises(AttributeError):
# pylint: disable=pointless-statement
booking.user_check
def test_agenda_booking_max_two_user_checks():
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
booking = Booking.objects.create(event=event)
BookingCheck.objects.create(booking=booking, presence=True)
# cannot create second check with presence=True
with pytest.raises(IntegrityError):
with transaction.atomic():
BookingCheck.objects.create(booking=booking, presence=True)
# allow to create second check with presence=False
BookingCheck.objects.create(booking=booking, presence=False)
# cannot create second check with presence=False
with pytest.raises(IntegrityError):
with transaction.atomic():
BookingCheck.objects.create(booking=booking, presence=False)