chrono/tests/api/datetimes/test_recurring_events.py

564 lines
23 KiB
Python

import datetime
import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.utils.timezone import make_aware, now
from chrono.agendas.models import (
Agenda,
Category,
Desk,
Event,
EventsType,
Person,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
Subscription,
TimePeriodException,
TimePeriodExceptionGroup,
UnavailabilityCalendar,
)
pytestmark = pytest.mark.django_db
def test_recurring_events_api_list(app, freezer):
freezer.move_to('2021-09-06 12:00')
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
Event.objects.create(label='Normal event', start_datetime=now(), places=5, agenda=agenda)
event = Event.objects.create(
label='Example Event',
start_datetime=now(),
recurrence_days=[0, 3, 4], # Monday, Thursday, Friday
places=2,
agenda=agenda,
)
resp = app.get('/api/agendas/recurring-events/', status=400)
start_datetime = now() + datetime.timedelta(days=15)
Event.objects.create(
label='Other',
start_datetime=start_datetime,
recurrence_days=[start_datetime.weekday()],
places=2,
agenda=agenda,
recurrence_end_date=now() + datetime.timedelta(days=45),
)
resp = app.get('/api/agendas/recurring-events/?agendas=%s&sort=day' % agenda.slug)
assert len(resp.json['data']) == 4
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:0'
assert resp.json['data'][0]['text'] == 'Monday: Example Event'
assert resp.json['data'][0]['label'] == 'Example Event'
assert resp.json['data'][0]['day'] == 'Monday'
assert resp.json['data'][0]['slug'] == 'example-event'
assert resp.json['data'][1]['id'] == 'foo-bar@other:1'
assert resp.json['data'][1]['text'] == 'Tuesday: Other'
assert resp.json['data'][1]['label'] == 'Other'
assert resp.json['data'][1]['day'] == 'Tuesday'
assert resp.json['data'][1]['slug'] == 'other'
assert resp.json['data'][2]['id'] == 'foo-bar@example-event:3'
assert resp.json['data'][2]['text'] == 'Thursday: Example Event'
assert resp.json['data'][2]['label'] == 'Example Event'
assert resp.json['data'][2]['day'] == 'Thursday'
assert resp.json['data'][2]['slug'] == 'example-event'
assert resp.json['data'][3]['id'] == 'foo-bar@example-event:4'
assert resp.json['data'][3]['text'] == 'Friday: Example Event'
assert resp.json['data'][3]['label'] == 'Example Event'
assert resp.json['data'][3]['day'] == 'Friday'
assert resp.json['data'][3]['slug'] == 'example-event'
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
assert len(resp.json['data']) == 4
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:0'
assert resp.json['data'][1]['id'] == 'foo-bar@example-event:3'
assert resp.json['data'][2]['id'] == 'foo-bar@example-event:4'
assert resp.json['data'][3]['id'] == 'foo-bar@other:1'
resp = app.get('/api/agendas/recurring-events/?agendas=%s&sort=invalid' % agenda.slug, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['sort'][0] == '"invalid" is not a valid choice.'
new_event = Event.objects.create(
label='New event one hour before',
slug='one-hour-before',
start_datetime=now() - datetime.timedelta(hours=1),
recurrence_days=[3], # Thursday
places=2,
agenda=agenda,
recurrence_end_date=now() + datetime.timedelta(days=30),
)
Event.objects.create(
label='New event two hours before but one week later',
slug='two-hours-before',
start_datetime=now() + datetime.timedelta(days=6, hours=22),
recurrence_days=[3], # Thursday
places=2,
agenda=agenda,
)
resp = app.get('/api/agendas/recurring-events/?agendas=%s&sort=day' % agenda.slug)
assert len(resp.json['data']) == 6
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:0'
assert resp.json['data'][1]['id'] == 'foo-bar@other:1'
assert resp.json['data'][2]['id'] == 'foo-bar@two-hours-before:3'
assert resp.json['data'][3]['id'] == 'foo-bar@one-hour-before:3'
assert resp.json['data'][4]['id'] == 'foo-bar@example-event:3'
assert resp.json['data'][5]['id'] == 'foo-bar@example-event:4'
freezer.move_to(new_event.recurrence_end_date)
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
assert len(resp.json['data']) == 5
assert not any('one-hour-before' in x['id'] for x in resp.json['data'])
event.publication_datetime = now() + datetime.timedelta(days=2)
event.save()
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
assert len(resp.json['data']) == 2
assert not any('example_event' in x['id'] for x in resp.json['data'])
@pytest.mark.freeze_time('2021-12-13 14:00') # Monday of 50th week
def test_recurring_events_api_list_shared_custody(app):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30
)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
slug='event',
start_datetime=now(),
recurrence_days=[0, 1, 2],
recurrence_end_date=now() + datetime.timedelta(days=30),
places=5,
agenda=agenda,
)
event.create_all_recurrences()
resp = app.get('/api/agendas/recurring-events/', params={'agendas': agenda.slug})
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
# add 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')
custody_agenda = SharedCustodyAgenda.objects.create(
first_guardian=father, second_guardian=mother, child=child, date_start=now()
)
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=[0], weeks='even')
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=mother, days=[1, 2], weeks='odd')
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0']
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2']
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'guardian_external_id': 'mother_id'},
status=400,
)
assert resp.json['err'] == 1
assert (
resp.json['errors']['user_external_id'][0]
== 'This field is required when using "guardian_external_id" parameter.'
)
# add custody period
SharedCustodyPeriod.objects.create(
agenda=custody_agenda,
guardian=mother,
date_start=datetime.date(2021, 12, 13), # Monday
date_end=datetime.date(2021, 12, 14),
)
# check mother sees Monday
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
# nothing changed for father
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0']
# add father custody during holidays
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
christmas_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Christmas', slug='christmas'
)
TimePeriodException.objects.create(
unavailability_calendar=calendar,
# Monday to Sunday
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=13, hour=0, minute=0)),
end_datetime=make_aware(datetime.datetime(year=2021, month=12, day=20, hour=0, minute=0)),
group=christmas_holiday,
)
rule = SharedCustodyHolidayRule.objects.create(
agenda=custody_agenda, guardian=father, holiday=christmas_holiday
)
rule.update_or_create_periods()
# check father sees all days
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
# nothing changed for mother
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
# check exceptional custody periods take precedence over holiday rules
SharedCustodyPeriod.objects.create(
agenda=custody_agenda,
guardian=mother,
date_start=datetime.date(2021, 12, 14), # Tuesday
date_end=datetime.date(2021, 12, 15),
)
# check father doesn't see Tuesday
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:2']
# nothing changed for mother
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
@pytest.mark.freeze_time('2021-12-13 14:00') # Monday of 50th week
def test_recurring_events_api_list_shared_custody_start_date(app):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30
)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
slug='event',
start_datetime=now(),
recurrence_days=[0, 1, 2],
recurrence_end_date=now() + datetime.timedelta(days=30),
places=5,
agenda=agenda,
)
event.create_all_recurrences()
resp = app.get('/api/agendas/recurring-events/', params={'agendas': agenda.slug})
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
# add shared two custody agendas
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')
custody_agenda = SharedCustodyAgenda.objects.create(
first_guardian=father, second_guardian=mother, child=child, date_start=now()
)
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=[0], weeks='even')
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=mother, days=[1, 2], weeks='odd')
custody_agenda2 = SharedCustodyAgenda.objects.create(
first_guardian=father,
second_guardian=mother,
child=child,
date_start=now() + datetime.timedelta(days=15),
)
SharedCustodyRule.objects.create(agenda=custody_agenda2, guardian=father, days=[1], weeks='even')
SharedCustodyRule.objects.create(agenda=custody_agenda2, guardian=mother, days=[0, 2], weeks='odd')
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1']
resp = app.get(
'/api/agendas/recurring-events/',
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_list_multiple_agendas(app):
agenda = Agenda.objects.create(label='First Agenda', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
Event.objects.create(
label='A',
start_datetime=start,
places=2,
recurrence_end_date=end,
recurrence_days=[0, 2, 5],
agenda=agenda,
)
Event.objects.create(
label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1], agenda=agenda
)
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
Desk.objects.create(agenda=agenda2, slug='_exceptions_holder')
Event.objects.create(
label='C',
start_datetime=start,
places=2,
recurrence_end_date=end,
recurrence_days=[2, 3],
agenda=agenda2,
)
resp = app.get('/api/agendas/recurring-events/?agendas=first-agenda,second-agenda&sort=day')
event_ids = [x['id'] for x in resp.json['data']]
assert event_ids == [
'first-agenda@a:0',
'first-agenda@b:1',
'first-agenda@a:2',
'second-agenda@c:2',
'second-agenda@c:3',
'first-agenda@a:5',
]
assert event_ids.index('first-agenda@a:2') < event_ids.index('second-agenda@c:2')
# sorting depends on querystring order
resp = app.get('/api/agendas/recurring-events/?agendas=second-agenda,first-agenda&sort=day')
event_ids = [x['id'] for x in resp.json['data']]
assert event_ids.index('first-agenda@a:2') > event_ids.index('second-agenda@c:2')
resp = app.get('/api/agendas/recurring-events/?agendas=second-agenda')
assert [x['id'] for x in resp.json['data']] == ['second-agenda@c:2', 'second-agenda@c:3']
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_list_multiple_agendas_queries(app):
events_type = EventsType.objects.create(label='Foo')
category = Category.objects.create(label='Category A')
for i in range(20):
agenda = Agenda.objects.create(slug=f'{i}', kind='events', category=category, events_type=events_type)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
event = Event.objects.create(
start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1, 2], agenda=agenda
)
event.create_all_recurrences()
Subscription.objects.create(
agenda=agenda,
user_external_id='xxx',
date_start=now(),
date_end=now() + datetime.timedelta(days=60),
)
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % ','.join(str(i) for i in range(20)))
assert len(resp.json['data']) == 40
assert len(ctx.captured_queries) == 3
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agendas/recurring-events/?subscribed=category-a&user_external_id=xxx')
assert len(resp.json['data']) == 40
assert len(ctx.captured_queries) == 3
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='xxx', 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, guardian=father, days=list(range(7)), weeks='even')
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd')
with CaptureQueriesContext(connection) as ctx:
resp = app.get(
'/api/agendas/recurring-events/?subscribed=category-a&user_external_id=xxx&guardian_external_id=father_id'
)
assert len(resp.json['data']) == 40
assert len(ctx.captured_queries) == 5
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_list_subscribed(app, user):
category = Category.objects.create(label='Category A')
first_agenda = Agenda.objects.create(label='First agenda', kind='events', category=category)
category = Category.objects.create(label='Category B')
second_agenda = Agenda.objects.create(label='Second agenda', kind='events', category=category)
Event.objects.create(
slug='event',
start_datetime=now(),
recurrence_days=[0, 1, 3, 6], # Monday, Tuesday, Thursday, Friday
places=2,
agenda=first_agenda,
recurrence_end_date=now() + datetime.timedelta(days=364),
)
Event.objects.create(
slug='sunday-event',
start_datetime=now(),
recurrence_days=[5],
places=2,
agenda=second_agenda,
recurrence_end_date=now() + datetime.timedelta(days=364),
)
Subscription.objects.create(
agenda=first_agenda,
user_external_id='xxx',
date_start=now(),
date_end=now() + datetime.timedelta(days=30),
)
resp = app.get('/api/agendas/recurring-events/?user_external_id=xxx&subscribed=all')
assert len(resp.json['data']) == 4
assert all(event['id'].startswith('first-agenda') for event in resp.json['data'])
resp = app.get('/api/agendas/recurring-events/?user_external_id=xxx&subscribed=category-a')
assert len(resp.json['data']) == 4
assert all(event['id'].startswith('first-agenda') for event in resp.json['data'])
resp = app.get('/api/agendas/recurring-events/?user_external_id=xxx&subscribed=category-b')
assert len(resp.json['data']) == 0
Subscription.objects.create(
agenda=second_agenda,
user_external_id='xxx',
date_start=now(),
date_end=now() + datetime.timedelta(days=30),
)
resp = app.get('/api/agendas/recurring-events/?user_external_id=xxx&subscribed=all&sort=day')
assert len(resp.json['data']) == 5
# events are sorted by day
assert [x['id'] for x in resp.json['data']] == [
'first-agenda@event:0',
'first-agenda@event:1',
'first-agenda@event:3',
'second-agenda@sunday-event:5',
'first-agenda@event:6',
]
resp = app.get('/api/agendas/recurring-events/?user_external_id=xxx&subscribed=category-b')
assert len(resp.json['data']) == 1
# other user
resp = app.get('/api/agendas/recurring-events/?user_external_id=yyy&subscribed=all')
assert len(resp.json['data']) == 0
Subscription.objects.create(
agenda=second_agenda,
user_external_id='yyy',
date_start=now(),
date_end=now() + datetime.timedelta(days=30),
)
resp = app.get('/api/agendas/recurring-events/?user_external_id=yyy&subscribed=all')
assert len(resp.json['data']) == 1
# sorting depends on querystring order
Event.objects.create(
slug='event',
start_datetime=now(),
recurrence_days=[0],
places=2,
agenda=second_agenda,
recurrence_end_date=now() + datetime.timedelta(days=364),
)
resp = app.get('/api/agendas/recurring-events/?subscribed=category-a,category-b&user_external_id=xxx')
event_ids = [x['id'] for x in resp.json['data']]
assert event_ids.index('first-agenda@event:0') < event_ids.index('second-agenda@event:0')
resp = app.get('/api/agendas/recurring-events/?subscribed=category-b,category-a&user_external_id=xxx')
event_ids = [x['id'] for x in resp.json['data']]
assert event_ids.index('first-agenda@event:0') > event_ids.index('second-agenda@event:0')
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_list_overlapping_events(app):
agenda = Agenda.objects.create(label='First Agenda', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
Event.objects.create(
label='Event 12-14',
start_datetime=start,
duration=120,
places=2,
recurrence_end_date=end,
recurrence_days=[1],
agenda=agenda,
)
Event.objects.create(
label='Event 14-15',
start_datetime=start + datetime.timedelta(hours=2),
duration=60,
places=2,
recurrence_end_date=end,
recurrence_days=[1],
agenda=agenda,
)
Event.objects.create(
label='Event 15-17',
start_datetime=start + datetime.timedelta(hours=3),
duration=120,
places=2,
recurrence_end_date=end,
recurrence_days=[1, 3, 5],
agenda=agenda,
)
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
Desk.objects.create(agenda=agenda2, slug='_exceptions_holder')
Event.objects.create(
label='Event 12-18',
start_datetime=start,
duration=360,
places=2,
recurrence_end_date=end,
recurrence_days=[1, 5],
agenda=agenda2,
)
Event.objects.create(
label='No duration',
start_datetime=start,
places=2,
recurrence_end_date=end,
recurrence_days=[5],
agenda=agenda2,
)
resp = app.get(
'/api/agendas/recurring-events/?agendas=first-agenda,second-agenda&sort=day&check_overlaps=true'
)
assert [(x['id'], set(x['overlaps'])) for x in resp.json['data']] == [
('first-agenda@event-12-14:1', {'second-agenda@event-12-18:1'}),
(
'second-agenda@event-12-18:1',
{'first-agenda@event-12-14:1', 'first-agenda@event-14-15:1', 'first-agenda@event-15-17:1'},
),
('first-agenda@event-14-15:1', {'second-agenda@event-12-18:1'}),
('first-agenda@event-15-17:1', {'second-agenda@event-12-18:1'}),
('first-agenda@event-15-17:3', set()),
('second-agenda@event-12-18:5', {'first-agenda@event-15-17:5'}),
('second-agenda@no-duration:5', set()),
('first-agenda@event-15-17:5', {'second-agenda@event-12-18:5'}),
]
resp = app.get('/api/agendas/recurring-events/?agendas=first-agenda,second-agenda&sort=day')
assert ['overlaps' not in x for x in resp.json['data']]