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.models import Q from django.test import override_settings from django.utils.timezone import localtime, make_aware, now from chrono.agendas.models import ( Agenda, AgendaNotificationsSettings, AgendaReminderSettings, Booking, Category, Desk, Event, EventCancellationReport, ICSError, MeetingType, Person, Resource, SharedCustodyAgenda, SharedCustodyHolidayRule, SharedCustodyPeriod, SharedCustodyRule, TimePeriod, TimePeriodException, TimePeriodExceptionGroup, TimePeriodExceptionSource, UnavailabilityCalendar, VirtualMember, ) pytestmark = pytest.mark.django_db ICS_SAMPLE = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//foo.bar//EN BEGIN:VEVENT DTSTAMP:20170824T082855Z DTSTART:20170831T170800Z DTEND:20170831T203400Z SEQUENCE:1 SUMMARY:Événement 1 END:VEVENT BEGIN:VEVENT DTSTAMP:20170824T092855Z DTSTART:20170830T180800Z DTEND:20170831T223400Z SEQUENCE:2 END:VEVENT END:VCALENDAR""" ICS_SAMPLE_WITH_DURATION = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//foo.bar//EN BEGIN:VEVENT DTSTAMP:20170824T082855Z DTSTART:20170831T170800Z DURATION:PT3H26M SEQUENCE:1 SUMMARY:Event 1 END:VEVENT BEGIN:VEVENT DTSTAMP:20170824T092855Z DTSTART:20170830T180800Z DURATION:P1D4H26M SEQUENCE:2 SUMMARY:Event 2 END:VEVENT END:VCALENDAR""" ICS_SAMPLE_WITH_RECURRENT_EVENT = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//foo.bar//EN BEGIN:VEVENT DTSTAMP:20170720T145803Z DESCRIPTION:Vacances d'ete DTSTART;VALUE=DATE:20180101 DTEND;VALUE=DATE:20180101 SUMMARY:reccurent event END:VEVENT BEGIN:VEVENT DTSTAMP:20180824T082855Z DTSTART:20180101 DTEND:20180101 SUMMARY:New Year's Eve RRULE:FREQ=YEARLY END:VEVENT END:VCALENDAR""" ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//foo.bar//EN BEGIN:VEVENT DTSTAMP:20180824T082855Z DTSTART:20120101 DURATION:PT24H SUMMARY:New Year's Eve RRULE:FREQ=YEARLY END:VEVENT END:VCALENDAR""" ICS_SAMPLE_WITH_NO_EVENTS = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//foo.bar//EN END:VCALENDAR""" INVALID_ICS_SAMPLE = """content """ with open('tests/data/atreal.ics') as f: ICS_ATREAL = f.read() with open('tests/data/holidays.ics') as f: ICS_HOLIDAYS = f.read() def test_slug(): agenda = Agenda(label='Foo bar') agenda.save() assert agenda.slug == 'foo-bar' def test_existing_slug(): agenda = Agenda(label='Foo bar', slug='bar') agenda.save() assert agenda.slug == 'bar' def test_duplicate_slugs(): agenda = Agenda(label='Foo baz') agenda.save() assert agenda.slug == 'foo-baz' agenda = Agenda(label='Foo baz') agenda.save() assert agenda.slug == 'foo-baz-1' agenda = Agenda(label='Foo baz') agenda.save() assert agenda.slug == 'foo-baz-2' def test_resource_slug(): resource = Resource.objects.create(label='Foo bar') assert resource.slug == 'foo-bar' def test_resource_existing_slug(): resource = Resource.objects.create(label='Foo bar', slug='bar') assert resource.slug == 'bar' def test_resource_duplicate_slugs(): resource = Resource.objects.create(label='Foo baz') assert resource.slug == 'foo-baz' resource = Resource.objects.create(label='Foo baz') assert resource.slug == 'foo-baz-1' resource = Resource.objects.create(label='Foo baz') assert resource.slug == 'foo-baz-2' def test_category_slug(): category = Category.objects.create(label='Foo bar') assert category.slug == 'foo-bar' def test_category_existing_slug(): category = Category.objects.create(label='Foo bar', slug='bar') assert category.slug == 'bar' def test_category_duplicate_slugs(): category = Category.objects.create(label='Foo baz') assert category.slug == 'foo-baz' category = Category.objects.create(label='Foo baz') assert category.slug == 'foo-baz-1' category = Category.objects.create(label='Foo baz') assert category.slug == 'foo-baz-2' def test_agenda_minimal_booking_delay(freezer): freezer.move_to('2021-07-09') agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=4) assert agenda.min_booking_datetime == make_aware(datetime.datetime(2021, 7, 13, 0, 0, 0)) freezer.move_to('2022-03-18') del agenda.min_booking_datetime assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 22, 0, 0, 0)) # DST change on sunday freezer.move_to('2022-03-25') del agenda.min_booking_datetime assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 29, 0, 0, 0)) # DST change on sunday freezer.move_to('2021-10-29') del agenda.min_booking_datetime assert agenda.min_booking_datetime == make_aware(datetime.datetime(2021, 11, 2, 0, 0, 0)) def test_agenda_minimal_booking_delay_in_working_days(settings, freezer): freezer.move_to('2021-07-09') agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=1) settings.WORKING_DAY_CALENDAR = None agenda.minimal_booking_delay_in_working_days = True agenda.save() assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10) agenda.minimal_booking_delay_in_working_days = False agenda.save() del agenda.min_booking_datetime assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10) settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' agenda.minimal_booking_delay_in_working_days = True agenda.save() del agenda.min_booking_datetime assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 12) agenda.minimal_booking_delay_in_working_days = False agenda.save() del agenda.min_booking_datetime assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10) agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=2) agenda.minimal_booking_delay_in_working_days = True agenda.save() freezer.move_to('2022-03-18') assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 22, 0, 0, 0)) # DST change on sunday freezer.move_to('2022-03-25') del agenda.min_booking_datetime assert agenda.min_booking_datetime == make_aware(datetime.datetime(2022, 3, 29, 0, 0, 0)) # DST change on sunday, and first of november is off freezer.move_to('2021-10-29') del agenda.min_booking_datetime assert agenda.min_booking_datetime == make_aware(datetime.datetime(2021, 11, 3, 0, 0, 0)) def test_agenda_maximal_booking_delay(freezer): freezer.move_to('2021-07-09') agenda = Agenda.objects.create(label='Agenda', maximal_booking_delay=4) assert agenda.max_booking_datetime == make_aware(datetime.datetime(2021, 7, 13, 0, 0, 0)) freezer.move_to('2022-03-18') del agenda.max_booking_datetime assert agenda.max_booking_datetime == make_aware(datetime.datetime(2022, 3, 22, 0, 0, 0)) # DST change on sunday freezer.move_to('2022-03-25') del agenda.max_booking_datetime assert agenda.max_booking_datetime == make_aware(datetime.datetime(2022, 3, 29, 0, 0, 0)) # DST change on sunday freezer.move_to('2021-10-29') del agenda.max_booking_datetime assert agenda.max_booking_datetime == make_aware(datetime.datetime(2021, 11, 2, 0, 0, 0)) @pytest.mark.parametrize('with_prefetch', [True, False]) def test_agenda_is_available_for_simple_management(settings, with_prefetch): settings.EXCEPTIONS_SOURCES = { 'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'}, } def check_is_available(result, use_prefetch=True): agenda = Agenda.objects.get() if with_prefetch and use_prefetch: agenda.prefetch_desks_and_exceptions(with_sources=True, min_date=now()) assert agenda.is_available_for_simple_management() == result agenda = Agenda.objects.create(label='Agenda', kind='meetings') # no desks check_is_available(True) # check kind agenda.kind = 'events' agenda.save() check_is_available(False, use_prefetch=False) agenda.kind = 'virtual' agenda.save() check_is_available(False, use_prefetch=False) # only one desk agenda.kind = 'meetings' agenda.save() desk = Desk.objects.create(label='Desk', agenda=agenda) check_is_available(True) # create some related data for this desk time_period = TimePeriod.objects.create( weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) ) desk.import_timeperiod_exceptions_from_settings(enable=True) source1 = desk.timeperiodexceptionsource_set.get(settings_slug='holidays') source2 = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics') source3 = TimePeriodExceptionSource.objects.create( desk=desk, ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'), ) date_now = now() exception = TimePeriodException.objects.create( label='Exception', desk=desk, start_datetime=date_now + datetime.timedelta(days=1), end_datetime=date_now + datetime.timedelta(days=2), ) unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') unavailability_calendar.desks.add(desk) unavailability_calendar2 = UnavailabilityCalendar.objects.create(label='Calendar 2') # still ok check_is_available(True) # duplicate the desk twice desk2 = desk.duplicate() desk.duplicate() # still ok check_is_available(True) # changes on time periods for _desk in [desk, desk2]: time_period2 = TimePeriod.objects.create( weekday=2, desk=_desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) ) check_is_available(False) time_period2.delete() check_is_available(True) time_period.weekday = 2 time_period.save() check_is_available(False) time_period.weekday = 1 time_period.start_time = datetime.time(10, 1) time_period.save() check_is_available(False) time_period.start_time = datetime.time(10, 0) time_period.end_time = datetime.time(12, 1) time_period.save() check_is_available(False) time_period.end_time = datetime.time(12, 0) time_period.save() check_is_available(True) # changes on exceptions for _desk in [desk, desk2]: exception2 = TimePeriodException.objects.create( label='Exception', desk=_desk, start_datetime=date_now + datetime.timedelta(days=3), end_datetime=date_now + datetime.timedelta(days=4), ) check_is_available(False) exception2.delete() check_is_available(True) exception.label = 'Exception blah' exception.save() check_is_available(False) exception.label = 'Exception' exception.start_datetime = date_now + datetime.timedelta(days=3) exception.save() check_is_available(False) exception.start_datetime = date_now + datetime.timedelta(days=1) exception.end_datetime = date_now + datetime.timedelta(days=1) exception.save() check_is_available(False) exception.end_datetime = date_now + datetime.timedelta(days=2) exception.save() check_is_available(True) # exceptions from source are not checked exception3 = TimePeriodException.objects.create( label='Exception', desk=desk, start_datetime=date_now + datetime.timedelta(days=3), end_datetime=date_now + datetime.timedelta(days=4), source=source2, ) check_is_available(True) exception3.delete() # changes on sources - from settings for _desk in [desk, desk2]: source = TimePeriodExceptionSource.objects.create( desk=_desk, settings_slug='holidays-bis', enabled=True ) check_is_available(False) source.delete() check_is_available(True) source1.enabled = False source1.save() check_is_available(False) source1.enabled = True source1.settings_slug = 'holidays-bis' source1.save() check_is_available(False) source1.settings_slug = 'holidays' source1.save() check_is_available(True) # changes on sources - from url for _desk in [desk, desk2]: source = TimePeriodExceptionSource.objects.create( desk=_desk, ics_url='http://example.com/sample-bis.ics' ) check_is_available(False) source.delete() check_is_available(True) source2.ics_url = 'http://example.com/sample-bis.ics' source2.save() check_is_available(False) source2.ics_url = 'http://example.com/sample.ics' source2.save() check_is_available(True) # changes on sources - from file for _desk in [desk, desk2]: source = TimePeriodExceptionSource.objects.create( desk=_desk, ics_filename='sample-bis.ics', ics_file=ContentFile(ICS_SAMPLE, name='sample-bis.ics'), ) check_is_available(False) source.delete() check_is_available(True) source3.ics_filename = 'sample-bis.ics' source3.save() check_is_available(False) source3.ics_filename = 'sample.ics' source3.save() check_is_available(True) # ics_file content is not checked # changes on unavailability calendars for _desk in [desk, desk2]: unavailability_calendar2.desks.add(_desk) check_is_available(False) unavailability_calendar2.desks.remove(_desk) check_is_available(True) def test_event_slug(): other_agenda = Agenda.objects.create(label='Foo bar') Event.objects.create(agenda=other_agenda, places=42, start_datetime=now(), slug='foo-bar') agenda = Agenda.objects.create(label='Foo baz') event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar') assert event.slug == 'foo-bar' event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar') assert event.slug == 'foo-bar-1' event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar') assert event.slug == 'foo-bar-2' event = Event.objects.create(agenda=agenda, places=42, start_datetime=now()) assert event.slug == 'foo-baz-event' event = Event.objects.create(agenda=agenda, places=42, start_datetime=now()) assert event.slug == 'foo-baz-event-1' event = Event.objects.create(agenda=agenda, places=42, start_datetime=now()) assert event.slug == 'foo-baz-event-2' def test_event_existing_slug(): other_agenda = Agenda.objects.create(label='Foo bar') Event.objects.create(agenda=other_agenda, places=42, start_datetime=now(), slug='bar') agenda = Agenda.objects.create(label='Foo baz') event = Event.objects.create(agenda=agenda, places=42, start_datetime=now(), label='Foo bar', slug='bar') assert event.slug == 'bar' def test_event_manager(): agenda = Agenda(label='Foo baz') agenda.save() event = Event(start_datetime=now(), places=10, agenda=agenda) event.save() booking = Booking(event=event) booking.save() assert Event.objects.all()[0].booked_places == 1 booking.cancellation_datetime = now() booking.save() assert Event.objects.all()[0].booked_places == 0 @pytest.mark.parametrize( 'start_days, start_minutes, min_delay, max_delay, pub_days, expected', [ # no delay (10, 0, 0, 0, None, True), # test publication_datetime (10, 0, 0, 0, 1, False), (10, 0, 0, 0, 0, True), # test min and max delays (10, 0, 20, 0, None, False), (10, 0, 1, 5, None, False), (10, 0, 1, 20, None, True), # special case for events that happens today (0, 10, 0, 20, None, True), (0, -10, 0, 20, None, False), ], ) def test_event_bookable_period(start_days, start_minutes, min_delay, max_delay, pub_days, expected): agenda = Agenda.objects.create( label='Foo bar', minimal_booking_delay=min_delay, maximal_booking_delay=max_delay ) event = Event.objects.create( start_datetime=localtime() + datetime.timedelta(days=start_days, minutes=start_minutes), publication_datetime=(localtime() + datetime.timedelta(days=pub_days)) if pub_days else None, places=10, agenda=agenda, ) assert event.in_bookable_period() == expected def test_meeting_type_slugs(): agenda1 = Agenda(label='Foo bar') agenda1.save() agenda2 = Agenda(label='Foo bar second') agenda2.save() meeting_type1 = MeetingType(agenda=agenda1, label='Baz') meeting_type1.save() assert meeting_type1.slug == 'baz' meeting_type2 = MeetingType(agenda=agenda1, label='Baz') meeting_type2.save() assert meeting_type2.slug == 'baz-1' meeting_type3 = MeetingType(agenda=agenda2, label='Baz') meeting_type3.save() assert meeting_type3.slug == 'baz' def test_timeperiodexception_creation_from_ics(): agenda = Agenda.objects.create(label='Test 1 agenda') desk = Desk.objects.create(label='Test 1 desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE, name='sample.ics') ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 2 def test_timeperiodexception_creation_from_ics_without_startdt(): agenda = Agenda.objects.create(label='Test 2 agenda') desk = Desk.objects.create(label='Test 2 desk', agenda=agenda) lines = [] # remove start datetimes from ics for line in ICS_SAMPLE.splitlines(): if line.startswith('DTSTART:'): continue lines.append(line) ics_sample = ContentFile("\n".join(lines), name='sample.ics') source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample) with pytest.raises(ICSError) as e: source._check_ics_content() assert str(e.value) == 'Event "Événement 1" has no start date.' def test_timeperiodexception_creation_from_ics_without_enddt(): agenda = Agenda.objects.create(label='Test 3 agenda') desk = Desk.objects.create(label='Test 3 desk', agenda=agenda) lines = [] # remove end datetimes from ics for line in ICS_SAMPLE.splitlines(): if line.startswith('DTEND:'): continue lines.append(line) ics_sample = ContentFile("\n".join(lines), name='sample.ics') source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample) source.refresh_timeperiod_exceptions_from_ics() for exception in TimePeriodException.objects.filter(desk=desk): end_time = localtime(exception.end_datetime).time() assert end_time == datetime.time(23, 59, 59, 999999) @pytest.mark.freeze_time('2017-12-01') def test_timeperiodexception_creation_from_ics_with_recurrences(): agenda = Agenda.objects.create(label='Test 4 agenda') desk = Desk.objects.create(label='Test 4 desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_RECURRENT_EVENT, name='sample.ics') ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 3 def test_timeexception_creation_from_ics_with_dates(): agenda = Agenda.objects.create(label='Test 5 agenda') desk = Desk.objects.create(label='Test 5 desk', agenda=agenda) lines = [] # remove end datetimes from ics for line in ICS_SAMPLE_WITH_RECURRENT_EVENT.splitlines(): if line.startswith('RRULE:'): continue lines.append(line) ics_sample = ContentFile("\n".join(lines), name='sample.ics') source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 2 for exception in TimePeriodException.objects.filter(desk=desk): assert localtime(exception.start_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0)) assert localtime(exception.end_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0)) def test_timeexception_create_from_invalid_ics(): agenda = Agenda.objects.create(label='Test 6 agenda') desk = Desk.objects.create(label='Test 6 desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(INVALID_ICS_SAMPLE, name='sample.ics') ) with pytest.raises(ICSError) as e: source._check_ics_content() assert str(e.value) == 'File format is invalid.' def test_timeexception_create_from_ics_with_no_events(): agenda = Agenda.objects.create(label='Test 7 agenda') desk = Desk.objects.create(label='Test 7 desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_NO_EVENTS, name='sample.ics') ) with pytest.raises(ICSError) as e: source._check_ics_content() assert str(e.value) == "The file doesn't contain any events." @mock.patch('chrono.agendas.models.requests.get') def test_timeperiodexception_creation_from_remote_ics(mocked_get): agenda = Agenda.objects.create(label='Test 8 agenda') desk = Desk.objects.create(label='Test 8 desk', agenda=agenda) mocked_response = mock.Mock() mocked_response.text = ICS_SAMPLE mocked_get.return_value = mocked_response source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics') source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 2 assert 'Événement 1' in [x.label for x in desk.timeperiodexception_set.all()] mocked_response.text = ICS_SAMPLE_WITH_NO_EVENTS mocked_get.return_value = mocked_response with pytest.raises(ICSError) as e: source._check_ics_content() assert str(e.value) == "The file doesn't contain any events." @mock.patch('chrono.agendas.models.requests.get') def test_timeperiodexception_remote_ics_encoding(mocked_get): agenda = Agenda.objects.create(label='Test 8 agenda') desk = Desk.objects.create(label='Test 8 desk', agenda=agenda) mocked_response = mock.Mock() mocked_response.content = ICS_SAMPLE.encode('iso-8859-15') mocked_response.text = ICS_SAMPLE mocked_get.return_value = mocked_response source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics') source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 2 assert 'Événement 1' in [x.label for x in desk.timeperiodexception_set.all()] @mock.patch('chrono.agendas.models.requests.get') def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get): agenda = Agenda.objects.create(label='Test 9 agenda') desk = Desk.objects.create(label='Test 9 desk', agenda=agenda) mocked_response = mock.Mock() mocked_response.text = ICS_SAMPLE mocked_get.return_value = mocked_response def mocked_requests_connection_error(*args, **kwargs): raise requests.ConnectionError('unreachable') source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics') mocked_get.side_effect = mocked_requests_connection_error with pytest.raises(ICSError) as e: source._check_ics_content() assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, unreachable)." @mock.patch('chrono.agendas.models.requests.get') def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get): agenda = Agenda.objects.create(label='Test 10 agenda') desk = Desk.objects.create(label='Test 10 desk', agenda=agenda) mocked_response = mock.Mock() mocked_response.status_code = 403 mocked_get.return_value = mocked_response def mocked_requests_http_forbidden_error(*args, **kwargs): raise requests.HTTPError(response=mocked_response) source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics') mocked_get.side_effect = mocked_requests_http_forbidden_error with pytest.raises(ICSError) as e: source._check_ics_content() assert ( str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403)." ) @mock.patch('chrono.agendas.models.requests.get') def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys): agenda = Agenda.objects.create(label='Test 11 agenda') desk = Desk.objects.create(label='Test 11 desk', agenda=agenda) source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics') mocked_response = mock.Mock() mocked_response.status_code = 403 mocked_get.return_value = mocked_response def mocked_requests_http_forbidden_error(*args, **kwargs): raise requests.HTTPError(response=mocked_response) mocked_get.side_effect = mocked_requests_http_forbidden_error call_command('sync_desks_timeperiod_exceptions') err = capsys.readouterr()[1] assert ( err == 'unable to create timeperiod exceptions for "Test 11 desk": Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403).\n' ) assert source.ics_url is not None assert source.ics_filename is None assert source.ics_file.name is None with mock.patch( 'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics' ) as refresh: call_command('sync_desks_timeperiod_exceptions') assert refresh.call_args_list == [mock.call()] source.ics_url = None source.ics_filename = 'sample.ics' source.ics_file = ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics') source.save() with mock.patch( 'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics' ) as refresh: call_command('sync_desks_timeperiod_exceptions') assert refresh.call_args_list == [mock.call()] TimePeriodExceptionSource.objects.update(ics_file='') with mock.patch( 'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics' ) as refresh: call_command('sync_desks_timeperiod_exceptions') assert refresh.call_args_list == [] TimePeriodExceptionSource.objects.update(ics_file=None) with mock.patch( 'chrono.agendas.models.TimePeriodExceptionSource.refresh_timeperiod_exceptions_from_ics' ) as refresh: call_command('sync_desks_timeperiod_exceptions') assert refresh.call_args_list == [] @override_settings( EXCEPTIONS_SOURCES={ 'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'}, } ) def test_timeperiodexception_from_settings(): agenda = Agenda(label='Test 1 agenda') agenda.save() desk = Desk(label='Test 1 desk', agenda=agenda) desk.save() desk.import_timeperiod_exceptions_from_settings(enable=True) source = TimePeriodExceptionSource.objects.get(desk=desk) assert source.settings_slug == 'holidays' assert source.enabled assert TimePeriodException.objects.filter(desk=desk, source=source).exists() exception = TimePeriodException.objects.first() from workalendar.europe import France date, label = France().holidays()[0] exception = TimePeriodException.objects.filter(label=label).first() assert exception.end_datetime - exception.start_datetime == datetime.timedelta(days=1) assert localtime(exception.start_datetime).date() == date source.disable() assert not source.enabled assert not TimePeriodException.objects.filter(desk=desk, source=source).exists() source.enable() assert source.enabled assert TimePeriodException.objects.filter(desk=desk, source=source).exists() def test_timeperiodexception_from_settings_command(): setting = { 'EXCEPTIONS_SOURCES': { 'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'}, } } agenda = Agenda(label='Test 1 agenda') agenda.save() desk1 = Desk(label='Test 1 desk', agenda=agenda) desk1.save() assert not TimePeriodExceptionSource.objects.filter(desk=desk1).exists() with override_settings(**setting): desk2 = Desk(label='Test 2 desk', agenda=agenda) desk2.save() desk2.import_timeperiod_exceptions_from_settings(enable=True) desk3 = Desk(label='Test 3 desk', agenda=agenda) desk3.save() desk3.import_timeperiod_exceptions_from_settings(enable=False) call_command('sync_desks_timeperiod_exceptions_from_settings') assert not TimePeriodExceptionSource.objects.get(desk=desk1).enabled source2 = TimePeriodExceptionSource.objects.get(desk=desk2) assert source2.enabled source3 = TimePeriodExceptionSource.objects.get(desk=desk3) assert not source3.enabled exceptions_count = source2.timeperiodexception_set.count() # Alsace Moselle has more holidays setting['EXCEPTIONS_SOURCES']['holidays']['class'] = 'workalendar.europe.FranceAlsaceMoselle' with override_settings(**setting): call_command('sync_desks_timeperiod_exceptions_from_settings') source2.refresh_from_db() assert exceptions_count < source2.timeperiodexception_set.count() setting['LANGUAGE_CODE'] = 'fr-fr' with override_settings(**setting): call_command('sync_desks_timeperiod_exceptions_from_settings') assert not TimePeriodException.objects.filter(label='All Saints Day').exists() assert TimePeriodException.objects.filter(label='Toussaint').exists() setting['EXCEPTIONS_SOURCES'] = {} with override_settings(**setting): call_command('sync_desks_timeperiod_exceptions_from_settings') assert not TimePeriodExceptionSource.objects.exists() def test_timeperiodexception_groups(): unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') source = unavailability_calendar.timeperiodexceptionsource_set.create( ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics') ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.count() == 11 group1, group2 = TimePeriodExceptionGroup.objects.all() assert group1.label == 'Vacances de Noël' assert group1.slug == 'christmas_holidays' assert group2.label == 'Vacances d’Été' assert group2.slug == 'summer_holidays' assert group1.exceptions.count() == 6 assert group2.exceptions.count() == 5 unavailability_calendar.delete() assert not TimePeriodException.objects.exists() assert not TimePeriodExceptionGroup.objects.exists() # check no groups are created for desks agenda = Agenda.objects.create(label='Test 1 agenda') desk = Desk.objects.create(label='Test 1 desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics') ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.count() == 11 assert not TimePeriodExceptionGroup.objects.exists() def test_base_meeting_duration(): agenda = Agenda(label='Meeting', kind='meetings') agenda.save() with pytest.raises(ValueError): agenda.get_base_meeting_duration() meeting_type = MeetingType(agenda=agenda, label='Foo', duration=0) meeting_type.save() with pytest.raises(ValueError): agenda.get_base_meeting_duration() meeting_type = MeetingType(agenda=agenda, label='Foo', duration=30) meeting_type.save() del agenda.__dict__['cached_meetingtypes'] assert agenda.get_base_meeting_duration() == 30 meeting_type = MeetingType(agenda=agenda, label='Bar', duration=60) meeting_type.save() del agenda.__dict__['cached_meetingtypes'] assert agenda.get_base_meeting_duration() == 30 meeting_type = MeetingType(agenda=agenda, label='Bar', duration=45) meeting_type.save() del agenda.__dict__['cached_meetingtypes'] assert agenda.get_base_meeting_duration() == 15 def test_timeperiodexception_creation_from_ics_with_duration(): # test that event defined using duration works and give the same start and # end dates agenda = Agenda.objects.create(label='Test 1 agenda') desk = Desk.objects.create(label='Test 1 desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics') ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 2 assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == { make_aware(datetime.datetime(2017, 8, 31, 19, 8, 0)), make_aware(datetime.datetime(2017, 8, 30, 20, 8, 0)), } assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == { make_aware(datetime.datetime(2017, 8, 31, 22, 34, 0)), make_aware(datetime.datetime(2017, 9, 1, 0, 34, 0)), } @pytest.mark.freeze_time('2017-12-01') def test_timeperiodexception_creation_from_ics_with_recurrences_in_the_past(): # test that recurrent events before today are not created # also test that duration + recurrent events works agenda = Agenda.objects.create(label='Test 4 agenda') desk = Desk.objects.create(label='Test 4 desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST, name='sample.ics'), ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 2 assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == { make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1)), } def test_timeperiodexception_creation_from_ics_with_recurrences_atreal(): agenda = Agenda.objects.create(label='Test atreal agenda') desk = Desk.objects.create(label='Test atreal desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(ICS_ATREAL, name='sample.ics') ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).exists() def test_management_role_deletion(): group = Group(name='Group') group.save() agenda = Agenda(label='Test agenda', edit_role=group, view_role=group) agenda.save() Group.objects.all().delete() assert Agenda.objects.get(id=agenda.id).view_role is None assert Agenda.objects.get(id=agenda.id).edit_role is None def test_virtual_agenda_init(): agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings') agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings') virt_agenda = Agenda.objects.create(label='Virtual agenda', kind='virtual') VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1) VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2) virt_agenda.save() assert virt_agenda.real_agendas.count() == 2 assert virt_agenda.real_agendas.get(pk=agenda1.pk) assert virt_agenda.real_agendas.get(pk=agenda2.pk) for agenda in (agenda1, agenda2): assert agenda.virtual_agendas.count() == 1 assert agenda.virtual_agendas.get() == virt_agenda def test_virtual_agenda_base_meeting_duration(): virt_agenda = Agenda.objects.create(label='Virtual agenda', kind='virtual') with pytest.raises(ValueError): virt_agenda.get_base_meeting_duration() agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings') VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1) with pytest.raises(ValueError): virt_agenda.get_base_meeting_duration() meeting_type = MeetingType(agenda=agenda1, label='Foo', duration=30) meeting_type.save() del virt_agenda.__dict__['cached_meetingtypes'] assert virt_agenda.get_base_meeting_duration() == 30 meeting_type = MeetingType(agenda=agenda1, label='Bar', duration=60) meeting_type.save() del virt_agenda.__dict__['cached_meetingtypes'] assert virt_agenda.get_base_meeting_duration() == 30 agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings') VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2) virt_agenda.save() meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60) meeting_type.save() del virt_agenda.__dict__['cached_meetingtypes'] assert virt_agenda.get_base_meeting_duration() == 60 def test_agenda_get_effective_time_periods(db): real_agenda = Agenda.objects.create(label='Real Agenda', kind='meetings') MeetingType.objects.create(agenda=real_agenda, label='MT1') desk = Desk.objects.create(label='Real Agenda Desk1', agenda=real_agenda) time_period = TimePeriod.objects.create( weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0), desk=desk ) virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=real_agenda) # empty exclusion set common_timeperiods = list(virtual_agenda.get_effective_time_periods()) assert len(common_timeperiods) == 1 common_timeperiod = common_timeperiods[0] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == time_period.start_time assert common_timeperiod.end_time == time_period.end_time # exclusions are on a different day def exclude_time_periods(time_periods): virtual_agenda.excluded_timeperiods.clear() for time_period in time_periods: time_period.agenda = virtual_agenda time_period.save() exclude_time_periods( [ TimePeriod(weekday=1, start_time=datetime.time(17, 0), end_time=datetime.time(18, 0)), TimePeriod(weekday=2, start_time=datetime.time(17, 0), end_time=datetime.time(18, 0)), ] ) common_timeperiods = list(virtual_agenda.get_effective_time_periods()) assert len(common_timeperiods) == 1 common_timeperiod = common_timeperiods[0] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == time_period.start_time assert common_timeperiod.end_time == time_period.end_time # one exclusion, end_time should be earlier exclude_time_periods( [TimePeriod(weekday=0, start_time=datetime.time(17, 0), end_time=datetime.time(18, 0))] ) common_timeperiods = list(virtual_agenda.get_effective_time_periods()) assert len(common_timeperiods) == 1 common_timeperiod = common_timeperiods[0] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(10, 0) assert common_timeperiod.end_time == datetime.time(17, 0) # one exclusion, start_time should be later exclude_time_periods( [TimePeriod(weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(16, 0))] ) common_timeperiods = list(virtual_agenda.get_effective_time_periods()) assert len(common_timeperiods) == 1 common_timeperiod = common_timeperiods[0] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(16, 0) assert common_timeperiod.end_time == datetime.time(18, 0) # one exclusion, splits effective timeperiod in two exclude_time_periods( [TimePeriod(weekday=0, start_time=datetime.time(12, 0), end_time=datetime.time(16, 0))] ) common_timeperiods = list(virtual_agenda.get_effective_time_periods()) assert len(common_timeperiods) == 2 common_timeperiod = common_timeperiods[0] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(10, 0) assert common_timeperiod.end_time == datetime.time(12, 0) common_timeperiod = common_timeperiods[1] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(16, 0) assert common_timeperiod.end_time == datetime.time(18, 0) # several exclusion, splits effective timeperiod into pieces exclude_time_periods( [ TimePeriod(weekday=0, start_time=datetime.time(12, 0), end_time=datetime.time(13, 0)), TimePeriod(weekday=0, start_time=datetime.time(10, 30), end_time=datetime.time(11, 30)), TimePeriod(weekday=0, start_time=datetime.time(16, 30), end_time=datetime.time(17, 00)), ] ) common_timeperiods = list(virtual_agenda.get_effective_time_periods()) assert len(common_timeperiods) == 4 common_timeperiod = common_timeperiods[0] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(10, 0) assert common_timeperiod.end_time == datetime.time(10, 30) common_timeperiod = common_timeperiods[1] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(11, 30) assert common_timeperiod.end_time == datetime.time(12, 0) common_timeperiod = common_timeperiods[2] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(13, 0) assert common_timeperiod.end_time == datetime.time(16, 30) common_timeperiod = common_timeperiods[3] assert common_timeperiod.weekday == time_period.weekday assert common_timeperiod.start_time == datetime.time(17, 0) assert common_timeperiod.end_time == datetime.time(18, 0) def test_exception_read_only(): agenda = Agenda.objects.create(label='Agenda', kind='meetings') desk = Desk.objects.create(agenda=agenda, label='Desk') unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') source1 = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics') source2 = TimePeriodExceptionSource.objects.create( desk=desk, ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'), ) source3 = TimePeriodExceptionSource.objects.create( desk=desk, settings_slug='slug', settings_label='Foo Bar', enabled=True ) exception = TimePeriodException.objects.create( desk=desk, start_datetime=now() - datetime.timedelta(days=2), end_datetime=now() - datetime.timedelta(days=1), ) assert exception.read_only is False for source in [source1, source2, source3]: exception.source = source exception.save() assert exception.read_only is True exception.source = None exception.desk = None exception.unavailability_calendar = unavailability_calendar exception.save() assert exception.read_only is True def test_desk_exceptions_within_two_weeks(): def set_prefetched_exceptions(desk): desk.prefetched_exceptions = TimePeriodException.objects.filter( Q(desk=desk) | Q(unavailability_calendar__desks=desk) ) agenda = Agenda.objects.create(label='Agenda', kind='meetings') desk = Desk.objects.create(agenda=agenda, label='Desk') # no exception set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [] # exception ends in the past exception = TimePeriodException.objects.create( desk=desk, start_datetime=now() - datetime.timedelta(days=2), end_datetime=now() - datetime.timedelta(days=1), ) set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [] # exception ends in the future - 14 days exception.end_datetime = now() + datetime.timedelta(days=10) # but starts in the past exception.start_datetime = now() - datetime.timedelta(days=2) exception.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception] # exception ends in the future - 14 days exception.end_datetime = now() + datetime.timedelta(days=10) # but starts in the future exception.start_datetime = now() + datetime.timedelta(days=2) exception.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception] # exception ends in the future + 14 days exception.end_datetime = now() + datetime.timedelta(days=20) # but starts in the past exception.start_datetime = now() - datetime.timedelta(days=2) exception.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception] # create another one, very far way from now exception2 = TimePeriodException.objects.create( desk=desk, start_datetime=now() + datetime.timedelta(days=200), end_datetime=now() + datetime.timedelta(days=201), ) # exception ends in the future + 14 days exception.end_datetime = now() + datetime.timedelta(days=20) # but starts in the future - 14 days exception.start_datetime = now() + datetime.timedelta(days=2) exception.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception] # exception ends in the future + 14 days exception.end_datetime = now() + datetime.timedelta(days=20) # but starts in the future + 14 days exception.start_datetime = now() + datetime.timedelta(days=21) exception.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception] # exception in the past exception.end_datetime = now() - datetime.timedelta(days=20) exception.start_datetime = now() - datetime.timedelta(days=21) exception.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception2] # check ordering of the queryset: exception is after exception2 exception.start_datetime = now() + datetime.timedelta(days=10) exception.end_datetime = now() + datetime.timedelta(days=11) exception.save() exception2.start_datetime = now() + datetime.timedelta(days=5) exception2.end_datetime = now() + datetime.timedelta(days=6) exception2.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception2, exception] # add an exception from unavailability calendar unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') exception3 = TimePeriodException.objects.create( unavailability_calendar=unavailability_calendar, start_datetime=now() + datetime.timedelta(days=2), end_datetime=now() + datetime.timedelta(days=3), ) unavailability_calendar.desks.add(desk) set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception3, exception2, exception] # change it exception3.start_datetime = now() + datetime.timedelta(days=7) exception3.end_datetime = now() + datetime.timedelta(days=8) exception3.save() set_prefetched_exceptions(desk) assert list(desk.get_exceptions_within_two_weeks()) == [exception2, exception3, exception] def test_desk_duplicate(): agenda = Agenda.objects.create(label='Agenda') desk = Desk.objects.create(label='Desk', agenda=agenda) time_period = TimePeriod.objects.create( weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) ) TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics') source2 = TimePeriodExceptionSource.objects.create( desk=desk, ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'), ) time_period_exception = TimePeriodException.objects.create( label='Exception', desk=desk, start_datetime=now() + datetime.timedelta(days=1), end_datetime=now() + datetime.timedelta(days=2), ) unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') unavailability_calendar.desks.add(desk) new_desk = desk.duplicate(label="New Desk") assert new_desk.pk != desk.pk assert new_desk.label == 'New Desk' assert new_desk.slug == 'new-desk' assert new_desk.timeperiod_set.count() == 1 new_time_period = TimePeriod.objects.get(desk=new_desk) assert new_time_period.weekday == time_period.weekday assert new_time_period.start_time == time_period.start_time assert new_time_period.end_time == time_period.end_time assert new_desk.timeperiodexception_set.count() == 1 new_time_period_exception = TimePeriodException.objects.get(desk=new_desk) assert new_time_period_exception.label == time_period_exception.label assert new_time_period_exception.start_datetime == time_period_exception.start_datetime assert new_time_period_exception.end_datetime == time_period_exception.end_datetime assert new_desk.timeperiodexceptionsource_set.count() == 2 assert TimePeriodExceptionSource.objects.filter( desk=new_desk, ics_url='http://example.com/sample.ics' ).exists() new_source2 = TimePeriodExceptionSource.objects.get(desk=new_desk, ics_filename='sample.ics') assert new_source2.ics_file.path != source2.ics_file.path assert new_desk.unavailability_calendars.count() == 1 assert new_desk.unavailability_calendars.get() == unavailability_calendar # duplicate again ! new_desk = desk.duplicate(label="New Desk") assert new_desk.slug == 'new-desk-1' def test_desk_duplicate_exception_sources(): agenda = Agenda.objects.create(label='Agenda') desk = Desk.objects.create(label='Desk', agenda=agenda) source = desk.timeperiodexceptionsource_set.create( ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE, name='sample.ics') ) source.refresh_timeperiod_exceptions_from_ics() assert TimePeriodException.objects.filter(desk=desk).count() == 2 new_desk = desk.duplicate(label="New Desk") new_source = new_desk.timeperiodexceptionsource_set.get(ics_filename='sample.ics') assert new_desk.timeperiodexception_set.count() == 2 source.delete() assert new_desk.timeperiodexception_set.count() == 2 new_source.delete() assert not new_desk.timeperiodexception_set.exists() @override_settings( EXCEPTIONS_SOURCES={ 'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'}, } ) def test_desk_duplicate_exception_source_from_settings(): agenda = Agenda.objects.create(label='Agenda') desk = Desk.objects.create(label='Desk', agenda=agenda) desk.import_timeperiod_exceptions_from_settings(enable=True) source = desk.timeperiodexceptionsource_set.get(settings_slug='holidays') assert source.enabled exceptions_count = desk.timeperiodexception_set.count() new_desk = desk.duplicate(label="New Desk") assert new_desk.timeperiodexceptionsource_set.filter(settings_slug='holidays').count() == 1 assert new_desk.timeperiodexceptionsource_set.get(settings_slug='holidays').enabled assert new_desk.timeperiodexception_set.count() == exceptions_count source.disable() new_desk = desk.duplicate(label="New Desk") assert not new_desk.timeperiodexceptionsource_set.get(settings_slug='holidays').enabled assert not new_desk.timeperiodexception_set.exists() def test_agenda_meetings_duplicate(): group = Group(name='Group') group.save() agenda = Agenda.objects.create(label='Agenda', kind='meetings', view_role=group) desk = Desk.objects.create(label='Desk', agenda=agenda) meeting_type = MeetingType.objects.create(agenda=agenda, label='meeting', duration=30) time_period = TimePeriod.objects.create( weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) ) TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics') source2 = TimePeriodExceptionSource.objects.create( desk=desk, ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE_WITH_DURATION, name='sample.ics'), ) time_period_exception = TimePeriodException.objects.create( label='Exception', desk=desk, start_datetime=now() + datetime.timedelta(days=1), end_datetime=now() + datetime.timedelta(days=2), ) resource = Resource.objects.create(label='Foo bar') agenda.resources.add(resource) new_agenda = agenda.duplicate() assert new_agenda.pk != agenda.pk assert new_agenda.label == 'Copy of Agenda' assert new_agenda.slug == 'copy-of-agenda' assert new_agenda.kind == 'meetings' assert new_agenda.view_role == group assert new_agenda.resources.first() == resource new_meeting_type = new_agenda.meetingtype_set.first() assert new_meeting_type.pk != meeting_type.pk assert new_meeting_type.label == meeting_type.label assert new_meeting_type.duration == meeting_type.duration assert new_meeting_type.slug == meeting_type.slug new_desk = new_agenda.desk_set.first() assert new_desk.pk != desk.pk assert new_desk.label == desk.label assert new_desk.slug == desk.slug assert new_desk.timeperiod_set.count() == 1 new_time_period = TimePeriod.objects.get(desk=new_desk) assert new_time_period.weekday == time_period.weekday assert new_time_period.start_time == time_period.start_time assert new_time_period.end_time == time_period.end_time assert new_desk.timeperiodexception_set.count() == 1 new_time_period_exception = TimePeriodException.objects.get(desk=new_desk) assert new_time_period_exception.label == time_period_exception.label assert new_time_period_exception.start_datetime == time_period_exception.start_datetime assert new_time_period_exception.end_datetime == time_period_exception.end_datetime assert new_desk.timeperiodexceptionsource_set.count() == 2 assert TimePeriodExceptionSource.objects.filter( desk=new_desk, ics_url='http://example.com/sample.ics' ).exists() new_source2 = TimePeriodExceptionSource.objects.get(desk=new_desk, ics_filename='sample.ics') assert new_source2.ics_file.path != source2.ics_file.path # duplicate again ! new_agenda = agenda.duplicate() assert new_agenda.slug == 'copy-of-agenda-1' @override_settings( EXCEPTIONS_SOURCES={ 'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'}, } ) def test_agenda_events_duplicate(): agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now() + datetime.timedelta(days=1), duration=10, places=10, label='event', slug='event', ) desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder') desk.import_timeperiod_exceptions_from_settings(enable=True) TimePeriodException.objects.create( desk=desk, start_datetime=now(), end_datetime=now() + datetime.timedelta(minutes=30), ) assert desk.timeperiodexception_set.count() == 34 AgendaNotificationsSettings.objects.create( agenda=agenda, full_event=AgendaNotificationsSettings.EMAIL_FIELD, full_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'], ) AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1, email_extra_info='top') new_agenda = agenda.duplicate() assert new_agenda.pk != agenda.pk assert new_agenda.kind == 'events' assert new_agenda.notifications_settings.full_event == AgendaNotificationsSettings.EMAIL_FIELD assert new_agenda.notifications_settings.full_event_emails == ['hop@entrouvert.com', 'top@entrouvert.com'] assert new_agenda.reminder_settings.days_before_email == 1 assert new_agenda.reminder_settings.email_extra_info == 'top' new_desk = new_agenda.desk_set.get() assert new_desk.slug == '_exceptions_holder' assert new_desk.timeperiodexception_set.count() == 34 assert new_desk.timeperiodexception_set.filter(source__isnull=True).count() == 1 source = new_desk.timeperiodexceptionsource_set.get() assert source.enabled is True assert source.settings_slug == 'holidays' new_event = new_agenda.event_set.first() assert new_event.pk != event.pk assert new_event.label == event.label assert new_event.duration == event.duration assert new_event.duration == event.duration assert new_event.places == event.places assert new_event.start_datetime == event.start_datetime def test_agenda_events_recurrence_duplicate(freezer): freezer.move_to('2021-01-06 12:00') # Wednesday now_ = now() end = now_ + datetime.timedelta(days=15) orig_agenda = Agenda.objects.create(label='Agenda', kind='events') Desk.objects.create(agenda=orig_agenda, slug='_exceptions_holder') event = Event.objects.create( agenda=orig_agenda, start_datetime=now_, recurrence_days=[now_.weekday()], label='Event', places=10, recurrence_end_date=end, ) event.create_all_recurrences() dup_agenda = orig_agenda.duplicate() for agenda in (orig_agenda, dup_agenda): assert agenda.event_set.count() == 4 # one recurring event, 3 real events assert agenda.event_set.filter(recurrence_days__isnull=False).count() == 1 rec_event = agenda.event_set.filter(recurrence_days__isnull=False).first() for event in agenda.event_set.filter(recurrence_days__isnull=True): assert event.primary_event == rec_event def test_agenda_virtual_duplicate(): agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings') agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings') virt_agenda = Agenda.objects.create(label='Virtual agenda', kind='virtual') VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1) VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2) virt_agenda.save() excluded_timeperiod = TimePeriod.objects.create( weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), agenda=virt_agenda ) new_agenda = virt_agenda.duplicate() assert new_agenda.pk != virt_agenda.pk assert new_agenda.kind == 'virtual' new_time_period = new_agenda.excluded_timeperiods.first() assert new_time_period.pk != excluded_timeperiod.pk assert new_time_period.agenda == new_agenda assert new_time_period.weekday == excluded_timeperiod.weekday assert new_time_period.start_time == excluded_timeperiod.start_time assert new_time_period.end_time == excluded_timeperiod.end_time assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda1).exists() assert VirtualMember.objects.filter(virtual_agenda=new_agenda, real_agenda=agenda2).exists() def test_agendas_cancel_events_command(): agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') for _ in range(5): Booking.objects.create(event=event, cancel_callback_url='http://example.org/jump/trigger/') event.cancellation_scheduled = True event.save() with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response call_command('cancel_events') assert mock_send.call_count == 5 assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 5 event.refresh_from_db() assert not event.cancellation_scheduled assert event.cancelled def test_agendas_cancel_events_command_network_error(freezer): freezer.move_to('2020-01-01') agenda = Agenda.objects.create(label='Events', kind='events') event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') def mocked_requests_connection_error(request, **kwargs): if 'good' in request.url: return mock.Mock(status_code=200) raise requests.exceptions.ConnectionError('unreachable') booking_good_url = Booking.objects.create(event=event, cancel_callback_url='http://good.org/') for _ in range(5): Booking.objects.create(event=event, cancel_callback_url='http://example.org/jump/trigger/') event.cancellation_scheduled = True event.save() with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response mock_send.side_effect = mocked_requests_connection_error call_command('cancel_events') assert mock_send.call_count == 6 booking_good_url.refresh_from_db() assert booking_good_url.cancellation_datetime assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 5 event.refresh_from_db() assert not event.cancellation_scheduled assert not event.cancelled report = EventCancellationReport.objects.get(event=event) assert report.bookings.count() == 5 assert len(report.booking_errors) == 5 for booking in report.bookings.all(): assert report.booking_errors[str(booking.pk)] == 'unreachable' # old reports are automatically removed freezer.move_to('2020-03-01') call_command('cancel_events') assert not EventCancellationReport.objects.exists() @mock.patch('django.contrib.auth.models.Group.role', create=True) @pytest.mark.parametrize( 'emails_to_members,emails', [ (False, []), (False, ['test@entrouvert.com']), (True, []), (True, ['test@entrouvert.com']), ], ) def test_agenda_notifications_role_email(mocked_role, emails_to_members, emails, mailoutbox): group = Group.objects.create(name='group') user = User.objects.create(username='user', email='user@entrouvert.com') user.groups.add(group) mocked_role.emails_to_members = emails_to_members mocked_role.emails = emails expected_recipients = emails if emails_to_members: expected_recipients.append(user.email) expected_email_count = 1 if emails else 0 agenda = Agenda.objects.create(label='Foo bar', kind='event', edit_role=group) event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') settings = AgendaNotificationsSettings.objects.create(agenda=agenda) settings.almost_full_event = AgendaNotificationsSettings.EDIT_ROLE settings.save() # book 9/10 places to reach almost full state for _ in range(9): Booking.objects.create(event=event) event.refresh_from_db() assert event.almost_full call_command('send_email_notifications') assert len(mailoutbox) == expected_email_count if mailoutbox: assert mailoutbox[0].recipients() == expected_recipients assert mailoutbox[0].subject == 'Alert: event "Hop" is almost full (90%)' assert 'manage/agendas/%s/events/%s/' % (agenda.id, event.id) in mailoutbox[0].body assert 'manage/agendas/%s/events/%s/' % (agenda.id, event.id) in mailoutbox[0].alternatives[0][0] # no new email on subsequent run call_command('send_email_notifications') assert len(mailoutbox) == expected_email_count def test_agenda_notifications_email_list(mailoutbox): agenda = Agenda.objects.create(label='Foo bar', kind='event') event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') settings = AgendaNotificationsSettings.objects.create(agenda=agenda) settings.full_event = AgendaNotificationsSettings.EMAIL_FIELD settings.full_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] settings.save() for _ in range(10): Booking.objects.create(event=event) event.refresh_from_db() assert event.full call_command('send_email_notifications') assert len(mailoutbox) == 1 assert mailoutbox[0].recipients() == recipients assert mailoutbox[0].subject == 'Alert: event "Hop" is full' assert ( 'view it here: https://example.com/manage/agendas/%s/events/%s/' % ( agenda.pk, event.pk, ) in mailoutbox[0].body ) # no new email on subsequent run call_command('send_email_notifications') assert len(mailoutbox) == 1 def test_agenda_notifications_cancelled(mailoutbox): agenda = Agenda.objects.create(label='Foo bar', kind='event') event = Event.objects.create(agenda=agenda, places=10, start_datetime=now(), label='Hop') settings = AgendaNotificationsSettings.objects.create(agenda=agenda) settings.cancelled_event = AgendaNotificationsSettings.EMAIL_FIELD settings.cancelled_event_emails = recipients = ['hop@entrouvert.com', 'top@entrouvert.com'] settings.save() event.cancelled = True event.save() call_command('send_email_notifications') assert len(mailoutbox) == 1 assert mailoutbox[0].recipients() == recipients assert mailoutbox[0].subject == 'Alert: event "Hop" is cancelled' # no new email on subsequent run call_command('send_email_notifications') assert len(mailoutbox) == 1 def test_agenda_reminders(mailoutbox, freezer): agenda = Agenda.objects.create(label='Events', kind='events') # add some old event with booking freezer.move_to('2019-01-01') old_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') Booking.objects.create(event=old_event, user_email='old@test.org') # no reminder configured call_command('send_booking_reminders') assert len(mailoutbox) == 0 # move to present day freezer.move_to('2020-01-01 14:00') # configure reminder the day before AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1) # event starts in 2 days start_datetime = now() + datetime.timedelta(days=2) event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event') for _ in range(5): Booking.objects.create(event=event, user_email='t@test.org') # extra booking with no email, should be ignored Booking.objects.create(event=event) freezer.move_to('2020-01-02 10:00') # not time to send reminders yet call_command('send_booking_reminders') assert len(mailoutbox) == 0 # one of the booking is cancelled Booking.objects.filter(user_email='t@test.org').first().cancel() freezer.move_to('2020-01-02 15:00') call_command('send_booking_reminders') assert len(mailoutbox) == 4 mailoutbox.clear() call_command('send_booking_reminders') assert len(mailoutbox) == 0 # booking is placed the day of the event, notfication should no be sent freezer.move_to('2020-01-03 08:00') Booking.objects.create(event=event, user_email='t@test.org') call_command('send_booking_reminders') assert len(mailoutbox) == 0 @pytest.mark.freeze_time('2020-01-01 14:00') def test_agenda_reminders_extra_emails(mailoutbox, freezer): agenda = Agenda.objects.create(label='Events', kind='events') AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1) start_datetime = now() + datetime.timedelta(days=2) event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event') Booking.objects.create( event=event, user_email='t@test.org', extra_emails=['t@test.org', 'u@test.org', 'v@test.org'], ) Booking.objects.create(event=event, extra_emails=['w@test.org']) freezer.move_to('2020-01-02 15:00') with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response call_command('send_booking_reminders') assert len(mailoutbox) == 4 assert all(len(x.to) == 1 for x in mailoutbox) assert {x.to[0] for x in mailoutbox} == {'t@test.org', 'u@test.org', 'v@test.org', 'w@test.org'} @override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO') def test_agenda_reminders_sms(freezer): freezer.move_to('2020-01-01 14:00') agenda = Agenda.objects.create(label='Events', kind='events') AgendaReminderSettings.objects.create(agenda=agenda, days_before_sms=1) start_datetime = now() + datetime.timedelta(days=2) event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event') for _ in range(5): Booking.objects.create(event=event, user_phone_number='+336123456789') Booking.objects.create(event=event) Booking.objects.create( event=event, user_phone_number='+33111111111', extra_phone_numbers=['+33111111111', '+33222222222', '+33333333333'], ) Booking.objects.create(event=event, extra_phone_numbers=['+336123456789']) freezer.move_to('2020-01-02 15:00') with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response call_command('send_booking_reminders') assert mock_send.call_count == 7 body = json.loads(mock_send.call_args_list[0][0][0].body.decode()) assert body['from'] == 'EO' assert body['to'] == ['+336123456789'] body = json.loads(mock_send.call_args_list[5][0][0].body.decode()) assert body['from'] == 'EO' assert set(body['to']) == {'+33111111111', '+33222222222', '+33333333333'} body = json.loads(mock_send.call_args_list[6][0][0].body.decode()) assert body['from'] == 'EO' assert set(body['to']) == {'+336123456789'} @override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO') def test_agenda_reminders_retry(freezer): freezer.move_to('2020-01-01 14:00') agenda = Agenda.objects.create(label='Events', kind='events') settings = AgendaReminderSettings.objects.create(agenda=agenda) start_datetime = now() + datetime.timedelta(days=2) event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event') settings.days_before_email = 1 settings.save() booking = Booking.objects.create(event=event, user_email='t@test.org') freezer.move_to('2020-01-02 15:00') def send_mail_error(*args, **kwargs): raise smtplib.SMTPException with mock.patch('chrono.agendas.management.commands.utils.send_mail') as mock_send: mock_send.return_value = None mock_send.side_effect = send_mail_error call_command('send_booking_reminders') assert mock_send.call_count == 1 booking.refresh_from_db() assert not booking.email_reminder_datetime assert not booking.sms_reminder_datetime mock_send.side_effect = None call_command('send_booking_reminders') assert mock_send.call_count == 2 booking.refresh_from_db() assert booking.email_reminder_datetime assert not booking.sms_reminder_datetime settings.days_before_email = None settings.days_before_sms = 1 settings.save() freezer.move_to('2020-01-01 14:00') booking = Booking.objects.create(event=event, user_phone_number='+336123456789') freezer.move_to('2020-01-02 15:00') def mocked_requests_connection_error(*args, **kwargs): raise requests.ConnectionError('unreachable') with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_send.side_effect = mocked_requests_connection_error mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response call_command('send_booking_reminders') assert mock_send.call_count == 1 booking.refresh_from_db() assert not booking.sms_reminder_datetime assert not booking.email_reminder_datetime mock_send.side_effect = None call_command('send_booking_reminders') assert mock_send.call_count == 2 booking.refresh_from_db() assert booking.sms_reminder_datetime assert not booking.email_reminder_datetime settings.days_before_email = 1 settings.save() freezer.move_to('2020-01-01 14:00') booking = Booking.objects.create(event=event, user_phone_number='+336123456789', user_email='t@test.org') freezer.move_to('2020-01-02 15:00') with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send, mock.patch( 'chrono.agendas.management.commands.utils.send_mail' ) as mock_send_mail: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response mock_send_mail.return_value = None call_command('send_booking_reminders') assert mock_send.call_count == 1 assert mock_send_mail.call_count == 1 booking.refresh_from_db() assert booking.sms_reminder_datetime assert booking.email_reminder_datetime call_command('send_booking_reminders') assert mock_send.call_count == 1 assert mock_send_mail.call_count == 1 @override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO') def test_agenda_reminders_different_days_before(freezer): freezer.move_to('2020-01-01 14:00') agenda = Agenda.objects.create(label='Events', kind='events') AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1, days_before_sms=2) start_datetime = now() + datetime.timedelta(days=3) event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event') Booking.objects.create(event=event, user_email='t@test.org', user_phone_number='+336123456789') with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send_sms, mock.patch( 'chrono.agendas.management.commands.utils.send_mail' ) as mock_send_mail: mock_response = mock.Mock(status_code=200) mock_send_sms.return_value = mock_response mock_send_mail.return_value = None call_command('send_booking_reminders') assert mock_send_sms.call_count == 0 assert mock_send_mail.call_count == 0 # two days before freezer.move_to('2020-01-02 15:00') call_command('send_booking_reminders') assert mock_send_sms.call_count == 1 assert mock_send_mail.call_count == 0 # one day before freezer.move_to('2020-01-03 15:00') call_command('send_booking_reminders') assert mock_send_sms.call_count == 1 assert mock_send_mail.call_count == 1 @override_settings(TIME_ZONE='UTC') def test_agenda_reminders_email_content(mailoutbox, freezer): freezer.move_to('2020-01-01 14:00') agenda = Agenda.objects.create(label='Events', kind='events') AgendaReminderSettings.objects.create( agenda=agenda, days_before_email=1, email_extra_info='Do no forget ID card.' ) start_datetime = now() + datetime.timedelta(days=2) event = Event.objects.create( agenda=agenda, start_datetime=start_datetime, places=10, label='Pool party', description='Come !', url='https://example.party', pricing='10€', ) Booking.objects.create(event=event, user_email='t@test.org') freezer.move_to('2020-01-02 15:00') call_command('send_booking_reminders') mail = mailoutbox[0] assert mail.subject == 'Reminder for your booking tomorrow at 2 p.m.' mail_bodies = (mail.body, mail.alternatives[0][0]) for body in mail_bodies: assert 'Hi,' in body assert 'You have booked event "Pool party", on Friday 3 January at 2 p.m..' in body assert 'Do no forget ID card.' in body assert 'Come !' in body assert 'Pricing: 10€' in body assert 'cancel' not in body assert 'if present' not in body # assert separation with preview code assert 'More information: https://example.party' in mail_bodies[0] assert 'More information' 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 '

Go to Main Center.
Take your "receipt".

' in mail.alternatives[0][0] body = json.loads(mock_send.call_args[0][0].body.decode()) assert 'Take your "receipt".' in body['message'] # in case of invalid template, send anyway freezer.move_to('2020-01-01 14:00') Booking.objects.create(event=event, user_email='t@test.org', user_phone_number='+336123456789') agenda.reminder_settings.email_extra_info = 'Take your {{ syntax error }}' agenda.reminder_settings.sms_extra_info = 'Take your {{ syntax error }}' agenda.reminder_settings.save() freezer.move_to('2020-01-02 15:00') with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send: mock_response = mock.Mock(status_code=200) mock_send.return_value = mock_response call_command('send_booking_reminders') assert len(mailoutbox) == 2 assert 'Take your' not in mailoutbox[1].body body = json.loads(mock_send.call_args[0][0].body.decode()) assert 'Take your' not in body['message'] @override_settings(TIME_ZONE='UTC') def test_agenda_reminders_meetings(mailoutbox, freezer): freezer.move_to('2020-01-01 11:00') agenda = Agenda.objects.create(label='Events', kind='meetings') desk = Desk.objects.create(agenda=agenda, label='Desk') meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30) TimePeriod.objects.create( desk=desk, weekday=now().weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) ) AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=2) event = Event.objects.create( agenda=agenda, places=1, desk=desk, meeting_type=meetingtype, start_datetime=now() + datetime.timedelta(days=5), # 06/01 ) Booking.objects.create( event=event, user_email='t@test.org', user_display_label='Birth certificate', form_url='publik://default/someform/1/', ) freezer.move_to('2020-01-04 15:00') call_command('send_booking_reminders') assert len(mailoutbox) == 1 mail = mailoutbox[0] assert mail.subject == 'Reminder for your meeting in 2 days at 11 a.m.' assert 'Your meeting "Birth certificate" is scheduled on Monday 6 January at 11 a.m..' in mail.body assert 'If in need to cancel it, you can do so here: http://example.org/someform/1/' in mail.body assert 'href="http://example.org/someform/1/"' in mail.alternatives[0][0] def test_agenda_reminders_waiting_list(mailoutbox, freezer): agenda = Agenda.objects.create(label='Events', kind='events') freezer.move_to('2020-01-01 14:00') # configure reminder the day before AgendaReminderSettings.objects.create(agenda=agenda, days_before_email=1) # event starts in 2 days start_datetime = now() + datetime.timedelta(days=2) event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event') for _ in range(5): Booking.objects.create(event=event, user_email='t@test.org') # extra booking in waiting list, should be ignored Booking.objects.create(event=event, user_email='t@test.org', in_waiting_list=True) freezer.move_to('2020-01-02 15:00') call_command('send_booking_reminders') assert len(mailoutbox) == 5 mailoutbox.clear() def test_anonymize_bookings(freezer): day = datetime.datetime(year=2020, month=1, day=1) freezer.move_to(day) agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event' ) for _ in range(5): Booking.objects.create( event=event, extra_data={'test': True}, label='john', user_display_label='john', user_external_id='john', user_first_name='john', user_last_name='doe', backoffice_url='https://example.org', ) freezer.move_to(day + datetime.timedelta(days=50)) new_event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10, label='Event') booking = Booking.objects.create(event=new_event, label='hop') freezer.move_to(day + datetime.timedelta(days=101)) call_command('anonymize_bookings') assert not Booking.objects.filter(anonymization_datetime__isnull=False).exists() # now ask for anonymization agenda.anonymize_delay = 100 agenda.save() call_command('anonymize_bookings') # bookings were placed more than 100 days ago but event was only 90 days ago assert not Booking.objects.filter(anonymization_datetime__isnull=False).exists() freezer.move_to(day + datetime.timedelta(days=111)) call_command('anonymize_bookings') assert ( Booking.objects.filter( label='', user_display_label='', user_external_id='', user_first_name='', user_last_name='', backoffice_url='https://example.org', extra_data={}, anonymization_datetime=now(), ).count() == 5 ) booking.refresh_from_db() assert booking.label == 'hop' assert not booking.anonymization_datetime def test_recurring_events(freezer): freezer.move_to('2021-01-06 12:00') # Wednesday agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], label='Event', places=10, waiting_list_places=10, duration=10, description='Description', url='https://example.com', pricing='10€', ) event.refresh_from_db() recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=15)) assert len(recurrences) == 3 first_event = recurrences[0] assert first_event.slug == event.slug + '--2021-01-06-1300' event_json = event.export_json() first_event_json = first_event.export_json() different_fields = ['slug', 'recurrence_days', 'recurrence_week_interval'] assert all(first_event_json[k] == event_json[k] for k in event_json if k not in different_fields) second_event = recurrences[1] assert second_event.start_datetime == first_event.start_datetime + datetime.timedelta(days=7) assert second_event.start_datetime.weekday() == first_event.start_datetime.weekday() assert second_event.slug == 'event--2021-01-13-1300' different_fields = ['slug', 'start_datetime'] second_event_json = second_event.export_json() assert all(first_event_json[k] == second_event_json[k] for k in event_json if k not in different_fields) new_recurrences = event.get_recurrences( localtime() + datetime.timedelta(days=15), localtime() + datetime.timedelta(days=30), ) assert len(recurrences) == 3 assert new_recurrences[0].start_datetime == recurrences[-1].start_datetime + datetime.timedelta(days=7) def test_recurring_events_dst(freezer, settings): freezer.move_to('2020-10-24 12:00') settings.TIME_ZONE = 'Europe/Brussels' agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], places=5 ) event.refresh_from_db() dt = localtime() recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8)) event_before_dst, event_after_dst = recurrences assert event_before_dst.start_datetime.hour + 1 == event_after_dst.start_datetime.hour assert event_before_dst.slug == 'agenda-event--2020-10-24-1400' assert event_after_dst.slug == 'agenda-event--2020-10-31-1400' freezer.move_to('2020-11-24 12:00') new_recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8)) new_event_before_dst, new_event_after_dst = new_recurrences assert event_before_dst.start_datetime == new_event_before_dst.start_datetime assert event_after_dst.start_datetime == new_event_after_dst.start_datetime assert event_before_dst.slug == new_event_before_dst.slug assert event_after_dst.slug == new_event_after_dst.slug def test_recurring_events_repetition(freezer): freezer.move_to('2021-01-06 12:00') # Wednesday agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), # everyday places=5, ) event.refresh_from_db() start_datetime = localtime(event.start_datetime) freezer.move_to('2021-01-06 12:01') # recurrence on same day should not be returned recurrences = event.get_recurrences( localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7) ) assert len(recurrences) == 6 assert recurrences[0].start_datetime == start_datetime + datetime.timedelta(days=2) assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7) for i in range(len(recurrences) - 1): assert recurrences[i].start_datetime + datetime.timedelta(days=1) == recurrences[i + 1].start_datetime event.recurrence_days = list(range(5)) # from Monday to Friday event.save() recurrences = event.get_recurrences( localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7) ) assert len(recurrences) == 4 assert recurrences[0].start_datetime == start_datetime + datetime.timedelta(days=2) assert recurrences[1].start_datetime == start_datetime + datetime.timedelta(days=5) assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7) event.recurrence_days = [localtime(event.start_datetime).weekday()] # from Monday to Friday event.recurrence_week_interval = 2 event.save() recurrences = event.get_recurrences( localtime() + datetime.timedelta(days=3), localtime() + datetime.timedelta(days=45) ) assert len(recurrences) == 3 assert recurrences[0].start_datetime == start_datetime + datetime.timedelta(days=14) assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=14) * len(recurrences) for i in range(len(recurrences) - 1): assert ( recurrences[i].start_datetime + datetime.timedelta(days=14) == recurrences[i + 1].start_datetime ) event.recurrence_days = [3] # Tuesday but start_datetime is a Wednesday event.recurrence_week_interval = 1 event.save() recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=10)) assert len(recurrences) == 2 # no recurrence exist on Wednesday assert all(localtime(r.start_datetime).weekday() == 3 for r in recurrences) @pytest.mark.freeze_time('2021-01-06') def test_recurring_events_with_end_date(): agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5, recurrence_end_date=(now() + datetime.timedelta(days=5)).date(), ) event.refresh_from_db() start_datetime = localtime(event.start_datetime) recurrences = event.get_recurrences( localtime(event.start_datetime), localtime(event.start_datetime) + datetime.timedelta(days=10) ) assert len(recurrences) == 5 assert recurrences[0].start_datetime == start_datetime assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=4) @override_settings( EXCEPTIONS_SOURCES={ 'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'}, } ) def test_recurring_events_exceptions(freezer): freezer.move_to('2021-05-01 12:00') agenda = Agenda.objects.create(label='Agenda', kind='events') desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder') desk.import_timeperiod_exceptions_from_settings() event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5, ) recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) assert recurrences[0].start_datetime.strftime('%m-%d') == '05-01' desk.import_timeperiod_exceptions_from_settings(enable=True) recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) # 05-01 is a holiday assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02' first_event = recurrences[0] # exception before first_event start_datetime time_period_exception = TimePeriodException.objects.create( desk=desk, start_datetime=first_event.start_datetime - datetime.timedelta(hours=1), end_datetime=first_event.start_datetime - datetime.timedelta(minutes=30), ) recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02' # exception wraps around first_event start_datetime time_period_exception.end_datetime = first_event.start_datetime + datetime.timedelta(minutes=30) time_period_exception.save() recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) assert recurrences[0].start_datetime.strftime('%m-%d') == '05-03' # exception starts after first_event start_datetime time_period_exception.start_datetime = first_event.start_datetime + datetime.timedelta(minutes=15) time_period_exception.save() recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02' assert recurrences[1].start_datetime.strftime('%m-%d') == '05-03' # exception spans multiple days time_period_exception.end_datetime = first_event.start_datetime + datetime.timedelta(days=3) time_period_exception.save() recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02' assert recurrences[1].start_datetime.strftime('%m-%d') == '05-06' # move exception to unavailability calendar unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') time_period_exception.desk = None time_period_exception.unavailability_calendar = unavailability_calendar time_period_exception.save() recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02' assert recurrences[1].start_datetime.strftime('%m-%d') == '05-03' unavailability_calendar.desks.add(desk) recurrences = event.get_recurrences(now(), now() + datetime.timedelta(days=7)) assert recurrences[0].start_datetime.strftime('%m-%d') == '05-02' assert recurrences[1].start_datetime.strftime('%m-%d') == '05-06' def test_recurring_events_exceptions_update_recurrences(freezer): freezer.move_to('2021-05-01 12:00') agenda = Agenda.objects.create(label='Agenda', kind='events') desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder') daily_event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5, recurrence_end_date=datetime.date(year=2021, month=5, day=8), ) weekly_event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], places=5, recurrence_end_date=datetime.date(year=2021, month=6, day=1), ) weekly_event_no_end_date = Event.objects.create( agenda=agenda, start_datetime=now() + datetime.timedelta(hours=2), recurrence_days=[now().weekday()], places=5, ) Event.create_events_recurrences([daily_event, weekly_event, weekly_event_no_end_date]) assert Event.objects.filter(primary_event=daily_event).count() == 7 assert Event.objects.filter(primary_event=weekly_event).count() == 5 assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53 time_period_exception = TimePeriodException.objects.create( desk=desk, start_datetime=datetime.date(year=2021, month=5, day=5), end_datetime=datetime.date(year=2021, month=5, day=10), ) agenda.update_event_recurrences() assert Event.objects.filter(primary_event=daily_event).count() == 4 assert Event.objects.filter(primary_event=weekly_event).count() == 4 assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 52 time_period_exception.delete() agenda.update_event_recurrences() assert Event.objects.filter(primary_event=daily_event).count() == 7 assert Event.objects.filter(primary_event=weekly_event).count() == 5 assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53 event = Event.objects.get( primary_event=weekly_event_no_end_date, start_datetime=now() + datetime.timedelta(days=7, hours=2) ) Booking.objects.create(event=event) time_period_exception.save() agenda.update_event_recurrences() assert Booking.objects.count() == 1 assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53 assert agenda.recurrence_exceptions_report.events.get() == event # no recurrence end date means new events are created as time moves on freezer.move_to('2021-06-01 12:00') agenda.update_event_recurrences() assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 57 def test_recurring_events_exceptions_update_recurrences_start_datetime_modified(freezer): freezer.move_to('2021-09-06 12:00') # Monday agenda = Agenda.objects.create(label='Agenda', kind='events') desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder') daily_event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5, recurrence_end_date=datetime.date(year=2021, month=9, day=13), ) daily_event.create_all_recurrences() assert daily_event.recurrences.count() == 7 monday_recurrence = daily_event.recurrences.get(start_datetime__day=6) monday_recurrence.start_datetime += datetime.timedelta(days=7) monday_recurrence.save() sunday_recurrence = daily_event.recurrences.get(start_datetime__day=12) sunday_recurrence.start_datetime += datetime.timedelta(hours=1) sunday_recurrence.save() # exception from Monday 06/09 to Tuesday 07/09 TimePeriodException.objects.create( desk=desk, start_datetime=datetime.date(year=2021, month=9, day=6), end_datetime=datetime.date(year=2021, month=9, day=8), ) # exception on Sunday 12/09 TimePeriodException.objects.create( desk=desk, start_datetime=datetime.date(year=2021, month=9, day=12), end_datetime=datetime.date(year=2021, month=9, day=13), ) agenda.update_event_recurrences() # Tuesday event was deleted assert daily_event.recurrences.count() == 6 assert not Event.objects.filter(start_datetime__day=7).exists() # Monday and Sunday still exist assert Event.objects.filter(pk__in=[monday_recurrence.pk, sunday_recurrence.pk]).exists() @pytest.mark.freeze_time('2022-02-22 14:00') def test_recurring_events_update_recurrences_new_event(freezer): agenda = Agenda.objects.create(label='Agenda', kind='events') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') daily_event = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5, recurrence_end_date=now() + datetime.timedelta(days=7), ) agenda.update_event_recurrences() assert daily_event.recurrences.count() == 7 Event.objects.all().delete() daily_event_no_end_date = Event.objects.create( agenda=agenda, start_datetime=now(), recurrence_days=[1], recurrence_week_interval=3, places=5, ) agenda.update_event_recurrences() assert daily_event_no_end_date.recurrences.count() == 18 daily_event = Event.objects.create( agenda=agenda, start_datetime=now().replace(hour=10), recurrence_days=list(range(7)), places=5, recurrence_end_date=now() + datetime.timedelta(days=7), ) agenda.update_event_recurrences() assert daily_event.recurrences.count() == 7 assert daily_event_no_end_date.recurrences.count() == 18 def test_recurring_events_display(freezer): freezer.move_to('2021-01-06 12:30') agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now() + datetime.timedelta(days=1), recurrence_days=list(range(7)), places=5, ) assert event.get_recurrence_display() == 'Daily at 1:30 p.m., from Jan. 7, 2021' freezer.move_to('2021-01-07 12:30') assert event.get_recurrence_display() == 'Daily at 1:30 p.m.' event.recurrence_days = [1, 2, 3, 4] event.save() assert event.get_recurrence_display() == 'From Tuesday to Friday at 1:30 p.m.' event.recurrence_days = [4, 5, 6] event.save() assert event.get_recurrence_display() == 'From Friday to Sunday at 1:30 p.m.' event.recurrence_days = [1, 4, 6] event.save() assert event.get_recurrence_display() == 'On Tuesdays, Fridays, Sundays at 1:30 p.m.' event.recurrence_days = [0] event.recurrence_week_interval = 2 event.save() assert event.get_recurrence_display() == 'On Mondays at 1:30 p.m., once every two weeks' event.recurrence_week_interval = 3 event.recurrence_end_date = now() + datetime.timedelta(days=7) event.save() assert ( event.get_recurrence_display() == 'On Mondays at 1:30 p.m., once every three weeks, until Jan. 14, 2021' ) freezer.move_to('2021-01-06 12:30') assert ( event.get_recurrence_display() == 'On Mondays at 1:30 p.m., once every three weeks, from Jan. 7, 2021, until Jan. 14, 2021' ) def test_event_triggered_fields(): agenda = Agenda.objects.create(label='Agenda', kind='events') event = Event.objects.create( agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event' ) event2 = Event.objects.create( agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event' ) assert event.booked_places == 0 assert event.booked_waiting_list_places == 0 assert event.almost_full is False assert event.full is False event.booked_places = 42 event.booked_waiting_list_places = 42 event.almost_full = True event.full = True event.save() # computed by triggers event.refresh_from_db() assert event.booked_places == 0 assert event.booked_waiting_list_places == 0 assert event.almost_full is False assert event.full is False # add bookings for other event: no impact for _ in range(10): Booking.objects.create(event=event2) event.refresh_from_db() assert event.booked_places == 0 assert event.booked_waiting_list_places == 0 assert event.almost_full is False assert event.full is False # add bookings for _ in range(9): Booking.objects.create(event=event) event.refresh_from_db() assert event.booked_places == 9 assert event.booked_waiting_list_places == 0 assert event.almost_full is True assert event.full is False Booking.objects.create(event=event) event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 0 assert event.almost_full is True assert event.full is True # cancel bookings for other event: no impact event2.booking_set.filter(cancellation_datetime__isnull=True).first().cancel() event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 0 assert event.almost_full is True assert event.full is True # cancel bookings event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel() event.refresh_from_db() assert event.booked_places == 9 assert event.booked_waiting_list_places == 0 assert event.almost_full is True assert event.full is False event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel() event.refresh_from_db() assert event.booked_places == 8 assert event.booked_waiting_list_places == 0 assert event.almost_full is False assert event.full is False # update places event.places = 20 event.save() event.refresh_from_db() assert event.booked_places == 8 assert event.booked_waiting_list_places == 0 assert event.almost_full is False assert event.full is False Booking.objects.create(event=event) Booking.objects.create(event=event) event.places = 10 event.save() event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 0 assert event.almost_full is True assert event.full is True # with a waiting list event.waiting_list_places = 5 event.save() event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 0 assert event.almost_full is True assert event.full is False # add bookings for other event: no impact for _ in range(10): Booking.objects.create(event=event2, in_waiting_list=True) event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 0 assert event.almost_full is True assert event.full is False # add bookings Booking.objects.create(event=event, in_waiting_list=True) event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 1 assert event.almost_full is True assert event.full is False for _ in range(1, 5): Booking.objects.create(event=event, in_waiting_list=True) event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 5 assert event.almost_full is True assert event.full is True # cancel bookings for other event: no impact event2.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel() event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 5 assert event.almost_full is True assert event.full is True # cancel bookings event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel() event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 4 assert event.almost_full is True assert event.full is False # update waiting list places event.waiting_list_places = 4 event.save() event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 4 assert event.almost_full is True assert event.full is True # delete bookings event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().delete() event.refresh_from_db() assert event.booked_places == 10 assert event.booked_waiting_list_places == 3 assert event.almost_full is True assert event.full is False event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete() event.refresh_from_db() assert event.booked_places == 9 assert event.booked_waiting_list_places == 3 assert event.almost_full is True assert event.full is False event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete() event.refresh_from_db() assert event.booked_places == 8 assert event.booked_waiting_list_places == 3 assert event.almost_full is False assert event.full is False def test_recurring_events_create_past_recurrences(freezer): freezer.move_to('2021-09-06 12:00') # Monday agenda = Agenda.objects.create(label='Agenda', kind='events') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') daily_event = Event.objects.create( agenda=agenda, start_datetime=now() - datetime.timedelta(days=3), recurrence_days=list(range(7)), places=5, recurrence_end_date=now() + datetime.timedelta(days=3), ) daily_event.create_all_recurrences() assert daily_event.recurrences.count() == 6 @pytest.mark.freeze_time('2022-02-22 14:00') # Tuesday of 8th week def test_shared_custody_agenda(): father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father) SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother) slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=30)) assert [x.date for x in slots] == [now().date() + datetime.timedelta(days=i) for i in range(30)] assert all(x.guardian == father for x in slots if x.date.isocalendar()[1] % 2 == 0) assert all(x.guardian == mother for x in slots if x.date.isocalendar()[1] % 2 == 1) # add mother custody period on father's week, on 23/02 and 24/02 SharedCustodyPeriod.objects.create( agenda=agenda, guardian=mother, date_start=datetime.date(year=2022, month=2, day=23), date_end=datetime.date(year=2022, month=2, day=25), ) slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=5)) slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots] assert slots == [ ('22/02', 'John Doe'), ('23/02', 'Jane Doe'), ('24/02', 'Jane Doe'), ('25/02', 'John Doe'), ('26/02', 'John Doe'), ] # add father custody period on father's week, nothing should change SharedCustodyPeriod.objects.create( agenda=agenda, guardian=father, date_start=datetime.date(year=2022, month=2, day=25), date_end=datetime.date(year=2022, month=3, day=3), ) slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=5)) slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots] assert slots == [ ('22/02', 'John Doe'), ('23/02', 'Jane Doe'), ('24/02', 'Jane Doe'), ('25/02', 'John Doe'), ('26/02', 'John Doe'), ] # check min_date/max_date slots = agenda.get_custody_slots( datetime.date(year=2022, month=2, day=27), datetime.date(year=2022, month=3, day=1) ) slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots] assert slots == [('27/02', 'John Doe'), ('28/02', 'John Doe')] @pytest.mark.freeze_time('2022-02-22 14:00') # Tuesday def test_shared_custody_agenda_different_periodicity(): father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) SharedCustodyRule.objects.create(agenda=agenda, days=[1, 2, 3], guardian=father) SharedCustodyRule.objects.create(agenda=agenda, days=[0, 4, 5, 6], guardian=mother) slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=14)) assert [(x.date.strftime('%A %d/%m'), str(x.guardian)) for x in slots] == [ ('Tuesday 22/02', 'John Doe'), ('Wednesday 23/02', 'John Doe'), ('Thursday 24/02', 'John Doe'), ('Friday 25/02', 'Jane Doe'), ('Saturday 26/02', 'Jane Doe'), ('Sunday 27/02', 'Jane Doe'), ('Monday 28/02', 'Jane Doe'), ('Tuesday 01/03', 'John Doe'), ('Wednesday 02/03', 'John Doe'), ('Thursday 03/03', 'John Doe'), ('Friday 04/03', 'Jane Doe'), ('Saturday 05/03', 'Jane Doe'), ('Sunday 06/03', 'Jane Doe'), ('Monday 07/03', 'Jane Doe'), ] @pytest.mark.parametrize( 'rules,complete', ( ([], False), ([{'days': [0]}], False), ([{'days': [0], 'weeks': 'odd'}], False), ([{'days': list(range(7))}], True), ([{'days': list(range(7)), 'weeks': 'odd'}], False), ([{'days': list(range(7)), 'weeks': 'odd'}, {'days': list(range(7)), 'weeks': 'even'}], True), ([{'days': [0, 1, 2]}, {'days': [3, 4, 5, 6]}], True), ([{'days': [0, 1, 2]}, {'days': [3, 4, 5]}], False), ([{'days': [0, 1, 2, 3]}, {'days': [3, 4, 5, 6]}], False), # overlapping rules, should not exist ( [ {'days': [0, 1, 2]}, {'days': [3, 4, 5]}, {'days': [6], 'weeks': 'odd'}, {'days': [6], 'weeks': 'even'}, ], True, ), ), ) def test_shared_custody_agenda_is_complete(rules, complete): father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) for i, rule in enumerate(rules): guardian = father if i % 2 else mother SharedCustodyRule.objects.create(agenda=agenda, guardian=guardian, **rule) assert agenda.is_complete() is complete @pytest.mark.parametrize( 'rules,days,weeks,overlaps', ( ([], [1], '', False), ([{'days': [0]}], [1], '', False), ([{'days': [1]}], [1], '', True), ([{'days': [0]}, {'days': [1]}], [1], '', True), ([{'days': [0], 'weeks': 'odd'}, {'days': [1]}], [1], '', True), ([{'days': [0]}, {'days': [1], 'weeks': 'odd'}], [1], '', True), ([{'days': [0]}, {'days': [1]}], [1], 'odd', True), ([{'days': [0]}, {'days': [1], 'weeks': 'odd'}], [1], 'even', False), ([{'days': [0, 1], 'weeks': 'odd'}], [1], 'odd', True), ([{'days': [0, 1]}], [1], '', True), ([{'days': [0, 1], 'weeks': 'even'}], [1], 'odd', False), ([{'days': [0, 1]}], [0, 3, 4], '', True), ([{'days': [0, 1]}], [2, 3], '', False), ), ) def test_shared_custody_agenda_rule_overlaps(rules, days, weeks, overlaps): father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) for i, rule in enumerate(rules): guardian = father if i % 2 else mother SharedCustodyRule.objects.create(agenda=agenda, guardian=guardian, **rule) assert agenda.rule_overlaps(days, weeks) is overlaps def test_shared_custody_agenda_holiday_rule_overlaps(): calendar = UnavailabilityCalendar.objects.create(label='Calendar') father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) summer_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Summer', slug='summer' ) winter_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Winter', slug='winter' ) rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday) assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='', instance=rule) is False assert agenda.holiday_rule_overlaps(winter_holiday, years='', periodicity='') is False assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='first-half') is True rule.years = 'odd' rule.save() assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='') is False rule.periodicity = 'first-half' rule.save() assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is False assert ( agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters') is True ) assert ( agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters') is True ) assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False rule.periodicity = 'second-and-fourth-quarters' rule.save() assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is True assert ( agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters') is False ) assert ( agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters') is True ) assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False @pytest.mark.parametrize( 'periods,date_start,date_end,overlaps', ( ([], '2022-02-03', '2022-02-04', False), ([('2022-02-03', '2022-02-04')], '2022-02-03', '2022-02-04', True), ([('2022-02-03', '2022-02-04')], '2022-02-01', '2022-02-04', True), ([('2022-02-03', '2022-02-04')], '2022-02-03', '2022-02-06', True), ([('2022-02-03', '2022-02-04')], '2022-02-04', '2022-02-06', False), ([('2022-02-03', '2022-02-04')], '2022-02-01', '2022-02-03', False), ([('2022-02-03', '2022-02-04'), ('2022-01-31', '2022-02-01')], '2022-02-01', '2022-02-03', False), ([('2022-02-03', '2022-02-04'), ('2022-01-31', '2022-02-01')], '2022-01-01', '2022-02-10', True), ([('2022-02-03', '2022-02-10')], '2022-02-05', '2022-02-06', True), ), ) def test_shared_custody_agenda_period_overlaps(periods, date_start, date_end, overlaps): father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) for i, dates in enumerate(periods): guardian = father if i % 2 else mother SharedCustodyPeriod.objects.create( agenda=agenda, guardian=guardian, date_start=dates[0], date_end=dates[1] ) assert agenda.period_overlaps(date_start, date_end) is overlaps def test_shared_custody_agenda_period_holiday_rule_no_overlaps(): calendar = UnavailabilityCalendar.objects.create(label='Calendar') father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) summer_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Summer', slug='summer' ) rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday) SharedCustodyPeriod.objects.create( holiday_rule=rule, agenda=agenda, guardian=father, date_start='2022-02-03', date_end='2022-02-05' ) assert agenda.period_overlaps('2022-02-03', '2022-02-05') is False def test_shared_custody_agenda_rule_label(): father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7))) assert rule.label == 'daily' rule.days = [1, 2, 3, 4] rule.save() assert rule.label == 'from Tuesday to Friday' rule.days = [4, 5, 6] rule.save() assert rule.label == 'from Friday to Sunday' rule.days = [1, 4, 6] rule.save() assert rule.label == 'on Tuesdays, Fridays, Sundays' rule.days = [0] rule.weeks = 'even' rule.save() assert rule.label == 'on Mondays, on even weeks' rule.weeks = 'odd' rule.save() assert rule.label == 'on Mondays, on odd weeks' def test_shared_custody_agenda_holiday_rule_label(): calendar = UnavailabilityCalendar.objects.create(label='Calendar') father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) summer_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Summer Holidays', slug='summer' ) rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday) assert rule.label == 'Summer Holidays' rule.years = 'even' rule.save() assert rule.label == 'Summer Holidays, on even years' rule.years = 'odd' rule.save() assert rule.label == 'Summer Holidays, on odd years' rule.periodicity = 'first-half' rule.save() assert rule.label == 'Summer Holidays, the first half, on odd years' rule.years = '' rule.periodicity = 'second-half' rule.save() assert rule.label == 'Summer Holidays, the second half' rule.periodicity = 'first-and-third-quarters' rule.save() assert rule.label == 'Summer Holidays, the first and third quarters' rule.periodicity = 'second-and-fourth-quarters' rule.save() assert rule.label == 'Summer Holidays, the second and fourth quarters' def test_shared_custody_agenda_period_label(freezer): father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) period = SharedCustodyPeriod.objects.create( agenda=agenda, guardian=father, date_start=datetime.date(2021, 7, 10), date_end=datetime.date(2021, 7, 11), ) assert str(period) == 'John Doe, 07/10/2021' period.date_end = datetime.date(2021, 7, 13) period.save() assert str(period) == 'John Doe, 07/10/2021 → 07/13/2021' def test_shared_custody_agenda_holiday_rule_create_periods(): calendar = UnavailabilityCalendar.objects.create(label='Calendar') father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) summer_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Summer', slug='summer' ) TimePeriodException.objects.create( unavailability_calendar=calendar, start_datetime=make_aware(datetime.datetime(year=2021, month=7, day=6, hour=0, minute=0)), end_datetime=make_aware(datetime.datetime(year=2021, month=9, day=2, hour=0, minute=0)), group=summer_holiday, ) TimePeriodException.objects.create( unavailability_calendar=calendar, start_datetime=make_aware(datetime.datetime(year=2022, month=7, day=7, hour=0, minute=0)), end_datetime=make_aware(datetime.datetime(year=2022, month=9, day=1, hour=0, minute=0)), group=summer_holiday, ) rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday) rule.update_or_create_periods() period1, period2 = SharedCustodyPeriod.objects.all() assert period1.holiday_rule == rule assert period1.guardian == father assert period1.agenda == agenda assert period1.date_start == datetime.date(year=2021, month=7, day=6) assert period1.date_end == datetime.date(year=2021, month=9, day=2) assert period2.holiday_rule == rule assert period2.guardian == father assert period2.agenda == agenda assert period2.date_start == datetime.date(year=2022, month=7, day=7) assert period2.date_end == datetime.date(year=2022, month=9, day=1) rule.years = 'odd' rule.update_or_create_periods() period = SharedCustodyPeriod.objects.get() assert period.date_start == datetime.date(year=2021, month=7, day=6) assert period.date_end == datetime.date(year=2021, month=9, day=2) rule.years = 'even' rule.update_or_create_periods() period = SharedCustodyPeriod.objects.get() assert period.date_start == datetime.date(year=2022, month=7, day=7) assert period.date_end == datetime.date(year=2022, month=9, day=1) rule.years = '' rule.periodicity = 'first-half' rule.update_or_create_periods() period1, period2 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=7, day=6) assert period1.date_end == datetime.date(year=2021, month=8, day=1) assert period1.date_end.weekday() == 6 assert period2.date_start == datetime.date(year=2022, month=7, day=7) assert period2.date_end == datetime.date(year=2022, month=7, day=31) assert period2.date_end.weekday() == 6 rule.periodicity = 'second-half' rule.update_or_create_periods() period1, period2 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=8, day=1) assert period1.date_start.weekday() == 6 assert period1.date_end == datetime.date(year=2021, month=9, day=2) assert period2.date_start == datetime.date(year=2022, month=7, day=31) assert period2.date_start.weekday() == 6 assert period2.date_end == datetime.date(year=2022, month=9, day=1) rule.periodicity = 'first-and-third-quarters' rule.update_or_create_periods() period1, period2, period3, period4 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=7, day=6) assert period1.date_end == datetime.date(year=2021, month=7, day=25) assert period1.date_end.weekday() == 6 assert period2.date_start == datetime.date(year=2021, month=8, day=8) assert period2.date_end == datetime.date(year=2021, month=8, day=22) assert period2.date_end.weekday() == 6 assert period3.date_start == datetime.date(year=2022, month=7, day=7) assert period3.date_end == datetime.date(year=2022, month=7, day=24) assert period3.date_end.weekday() == 6 assert period4.date_start == datetime.date(year=2022, month=8, day=7) assert period4.date_end == datetime.date(year=2022, month=8, day=21) assert period4.date_end.weekday() == 6 rule.periodicity = 'second-and-fourth-quarters' rule.update_or_create_periods() period1, period2, period3, period4 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=7, day=25) assert period1.date_start.weekday() == 6 assert period1.date_end == datetime.date(year=2021, month=8, day=8) assert period2.date_start == datetime.date(year=2021, month=8, day=22) assert period2.date_start.weekday() == 6 assert period2.date_end == datetime.date(year=2021, month=9, day=2) assert period3.date_start == datetime.date(year=2022, month=7, day=24) assert period3.date_start.weekday() == 6 assert period3.date_end == datetime.date(year=2022, month=8, day=7) assert period4.date_start == datetime.date(year=2022, month=8, day=21) assert period4.date_start.weekday() == 6 assert period4.date_end == datetime.date(year=2022, month=9, day=1) def test_shared_custody_agenda_holiday_rule_create_periods_christmas_holidays(): calendar = UnavailabilityCalendar.objects.create(label='Calendar') father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) christmas_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Christmas', slug='christmas' ) TimePeriodException.objects.create( unavailability_calendar=calendar, start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)), end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)), group=christmas_holiday, ) TimePeriodException.objects.create( unavailability_calendar=calendar, start_datetime=make_aware(datetime.datetime(year=2022, month=12, day=17, hour=0, minute=0)), end_datetime=make_aware(datetime.datetime(year=2023, month=1, day=3, hour=0, minute=0)), group=christmas_holiday, ) rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday) rule.update_or_create_periods() period1, period2 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=12, day=18) assert period1.date_end == datetime.date(year=2022, month=1, day=3) assert period2.date_start == datetime.date(year=2022, month=12, day=17) assert period2.date_end == datetime.date(year=2023, month=1, day=3) rule.periodicity = 'first-half' rule.update_or_create_periods() period1, period2 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=12, day=18) assert period1.date_end == datetime.date(year=2021, month=12, day=26) assert period1.date_end.weekday() == 6 assert period2.date_start == datetime.date(year=2022, month=12, day=17) assert period2.date_end == datetime.date(year=2022, month=12, day=25) assert period2.date_end.weekday() == 6 rule.periodicity = 'second-half' rule.update_or_create_periods() period1, period2 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=12, day=26) assert period1.date_start.weekday() == 6 assert period1.date_end == datetime.date(year=2022, month=1, day=3) assert period2.date_start == datetime.date(year=2022, month=12, day=25) assert period2.date_start.weekday() == 6 assert period2.date_end == datetime.date(year=2023, month=1, day=3) rule.periodicity = 'first-and-third-quarters' rule.update_or_create_periods() assert not SharedCustodyPeriod.objects.exists() rule.periodicity = 'second-and-fourth-quarters' rule.update_or_create_periods() assert not SharedCustodyPeriod.objects.exists() def test_shared_custody_agenda_holiday_rules_application(): calendar = UnavailabilityCalendar.objects.create(label='Calendar') father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) christmas_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Christmas', slug='christmas' ) TimePeriodException.objects.create( unavailability_calendar=calendar, start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)), end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)), group=christmas_holiday, ) SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father) SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother) rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday) rule.update_or_create_periods() date_start = datetime.date(year=2021, month=12, day=13) # Monday, even week slots = agenda.get_custody_slots(date_start, date_start + datetime.timedelta(days=30)) guardians = [str(x.guardian) for x in slots] assert all(name == 'John Doe' for name in guardians[:21]) assert all(name == 'Jane Doe' for name in guardians[21:28]) assert all(name == 'John Doe' for name in guardians[28:]) # check exceptional custody periods take precedence over holiday rules SharedCustodyPeriod.objects.create( agenda=agenda, guardian=mother, date_start=datetime.date(year=2021, month=12, day=27), date_end=datetime.date(year=2021, month=12, day=29), ) slots = agenda.get_custody_slots(date_start, date_start + datetime.timedelta(days=30)) slots = [(x.date.strftime('%d/%m'), str(x.guardian)) for x in slots] assert slots[13:17] == [ ('26/12', 'John Doe'), ('27/12', 'Jane Doe'), ('28/12', 'Jane Doe'), ('29/12', 'John Doe'), ] def test_shared_custody_agenda_update_holiday_rules_command(): calendar = UnavailabilityCalendar.objects.create(label='Calendar') father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe') mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe') child = Person.objects.create(user_external_id='child_id', first_name='James', last_name='Doe') agenda = SharedCustodyAgenda.objects.create( first_guardian=father, second_guardian=mother, child=child, date_start=now() ) christmas_holiday = TimePeriodExceptionGroup.objects.create( unavailability_calendar=calendar, label='Christmas', slug='christmas' ) exception = TimePeriodException.objects.create( unavailability_calendar=calendar, start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)), end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)), group=christmas_holiday, ) SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father) SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother) rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday) rule.update_or_create_periods() period = SharedCustodyPeriod.objects.get() assert period.date_start == datetime.date(year=2021, month=12, day=18) assert period.date_end == datetime.date(year=2022, month=1, day=3) exception.start_datetime += datetime.timedelta(days=1) exception.save() TimePeriodException.objects.create( unavailability_calendar=calendar, start_datetime=make_aware(datetime.datetime(year=2022, month=12, day=18, hour=0, minute=0)), end_datetime=make_aware(datetime.datetime(year=2023, month=1, day=3, hour=0, minute=0)), group=christmas_holiday, ) call_command('update_shared_custody_holiday_rules') period1, period2 = SharedCustodyPeriod.objects.all() assert period1.date_start == datetime.date(year=2021, month=12, day=19) assert period1.date_end == datetime.date(year=2022, month=1, day=3) assert period2.date_start == datetime.date(year=2022, month=12, day=18) assert period2.date_end == datetime.date(year=2023, month=1, day=3)