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']]