api: add shared custody support in recurring event list and fillslots (#63048)

This commit is contained in:
Valentin Deniaud 2022-03-30 11:13:48 +02:00
parent 9a8e19d0c6
commit afe588a1fb
4 changed files with 283 additions and 16 deletions

View File

@ -218,6 +218,7 @@ class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
)
user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
guardian_external_id = serializers.CharField(required=False, max_length=250, allow_blank=False)
def validate(self, attrs):
super().validate(attrs)
@ -230,6 +231,10 @@ class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
raise ValidationError(
{'user_external_id': _('This field is required when using "subscribed" parameter.')}
)
if 'guardian_external_id' in attrs and not user_external_id:
raise serializers.ValidationError(
{'user_external_id': _('This field is required when using "guardian_external_id" parameter.')}
)
if 'subscribed' in attrs:
lookups = {'subscriptions__user_external_id': user_external_id}
@ -257,17 +262,12 @@ class AgendaOrSubscribedSlugsMixin(DateRangeMixin):
class MultipleAgendasDatetimesSerializer(AgendaOrSubscribedSlugsMixin, DatetimesSerializer):
show_past_events = serializers.BooleanField(default=False)
guardian_external_id = serializers.CharField(max_length=250, required=False)
class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, serializers.Serializer):
pass
class MultipleAgendasFillslotsSerializer(AgendaOrSubscribedSlugsSerializer):
guardian_external_id = serializers.CharField(max_length=250, required=False)
class RecurringFillslotsQueryStringSerializer(AgendaOrSubscribedSlugsSerializer):
action = serializers.ChoiceField(required=True, choices=['update', 'book', 'unbook'])

View File

@ -1092,14 +1092,36 @@ class RecurringEventsList(APIView):
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
data = serializer.validated_data
agendas = Agenda.prefetch_recurring_events(data['agendas']).select_related('category')
events = []
for agenda in agendas:
for event in agenda.get_open_recurring_events():
for day in event.recurrence_days:
guardian_external_id = data.get('guardian_external_id')
if guardian_external_id:
agendas = Agenda.prefetch_events_and_exceptions(
data['agendas'],
user_external_id=data.get('user_external_id'),
guardian_external_id=guardian_external_id,
)
days_by_event = collections.defaultdict(set)
for agenda in agendas:
for event in agenda.prefetched_events:
if event.primary_event_id:
days_by_event[event.primary_event_id].add(event.start_datetime.weekday())
recurring_events = Event.objects.filter(pk__in=days_by_event).select_related(
'agenda', 'agenda__category'
)
events = []
for event in recurring_events:
for day in days_by_event[event.pk]:
event = copy.copy(event)
event.day = day
events.append(event)
else:
agendas = Agenda.prefetch_recurring_events(data['agendas']).select_related('category')
events = []
for agenda in agendas:
for event in agenda.get_open_recurring_events():
for day in event.recurrence_days:
event = copy.copy(event)
event.day = day
events.append(event)
if 'agendas' in request.query_params:
agenda_querystring_indexes = {
@ -1607,6 +1629,7 @@ class RecurringFillslots(APIView):
if not serializer.is_valid():
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
data = serializer.validated_data
guardian_external_id = data.get('guardian_external_id')
start_datetime, end_datetime = data.get('date_start'), data.get('date_end')
if not start_datetime or start_datetime < now():
@ -1625,18 +1648,33 @@ class RecurringFillslots(APIView):
if data['action'] == 'update':
events_to_book = self.get_event_recurrences(
agendas, payload['slots'], start_datetime, end_datetime, user_external_id
agendas,
payload['slots'],
start_datetime,
end_datetime,
user_external_id,
guardian_external_id,
)
events_to_unbook = self.get_events_to_unbook(agendas, events_to_book)
elif data['action'] == 'book':
events_to_book = self.get_event_recurrences(
agendas, payload['slots'], start_datetime, end_datetime, user_external_id
agendas,
payload['slots'],
start_datetime,
end_datetime,
user_external_id,
guardian_external_id,
)
events_to_unbook = []
elif data['action'] == 'unbook':
events_to_book = Event.objects.none()
events_to_unbook = self.get_event_recurrences(
agendas, payload['slots'], start_datetime, end_datetime, user_external_id
agendas,
payload['slots'],
start_datetime,
end_datetime,
user_external_id,
guardian_external_id,
).values_list('pk', flat=True)
# outdated bookings to remove (cancelled bookings to replace by an active booking)
@ -1698,7 +1736,9 @@ class RecurringFillslots(APIView):
]
return Response(response)
def get_event_recurrences(self, agendas, slots, start_datetime, end_datetime, user_external_id):
def get_event_recurrences(
self, agendas, slots, start_datetime, end_datetime, user_external_id, guardian_external_id
):
event_filter = Q()
agendas_by_slug = {a.slug: a for a in agendas}
for agenda_slug, days_by_event in slots.items():
@ -1727,6 +1767,8 @@ class RecurringFillslots(APIView):
events = events.filter(start_datetime__gte=start_datetime, cancelled=False)
if end_datetime:
events = events.filter(start_datetime__lte=end_datetime)
if guardian_external_id:
events = Agenda.filter_for_guardian(events, guardian_external_id, user_external_id)
return events
@ -1879,7 +1921,7 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
multiple_agendas = True
def post(self, request):
serializer = serializers.MultipleAgendasFillslotsSerializer(
serializer = serializers.AgendaOrSubscribedSlugsSerializer(
data=request.query_params, context={'user_external_id': request.data.get('user_external_id')}
)
if not serializer.is_valid():

View File

@ -1428,6 +1428,135 @@ def test_recurring_events_api_list(app, freezer):
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', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
child = Person.objects.create(user_external_id='child_id', name='James Doe')
custody_agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
custody_agenda.children.add(child)
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-09-06 12:00')
def test_recurring_events_api_list_multiple_agendas(app):
agenda = Agenda.objects.create(label='First Agenda', kind='events')
@ -1483,9 +1612,10 @@ def test_recurring_events_api_list_multiple_agendas_queries(app):
agenda = Agenda.objects.create(slug=f'{i}', kind='events', category=category)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
Event.objects.create(
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',
@ -1502,6 +1632,22 @@ def test_recurring_events_api_list_multiple_agendas_queries(app):
assert len(resp.json['data']) == 40
assert len(ctx.captured_queries) == 3
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
child = Person.objects.create(user_external_id='xxx', name='James Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
agenda.children.add(child)
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) == 8
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_list_subscribed(app, user):

View File

@ -3574,6 +3574,24 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
assert resp.json['booking_count'] == 180
assert len(ctx.captured_queries) == 17
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
child = Person.objects.create(user_external_id='xxx', name='James Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
agenda.children.add(child)
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.post_json(
'/api/agendas/recurring-events/fillslots/?action=update&agendas=%s&guardian_external_id=father_id'
% agenda_slugs,
params={'slots': events_to_book, 'user_external_id': 'xxx'},
)
assert resp.json['booking_count'] == 100
assert len(ctx.captured_queries) == 17
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots(app, user):
@ -4551,3 +4569,64 @@ def test_api_events_fillslots_multiple_agendas_shared_custody(app, user):
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'Some events are outside guardian custody: first-agenda@event-thursday'
@pytest.mark.freeze_time('2022-03-07 14:00') # Monday of 10th week
def test_recurring_events_api_fillslots_shared_custody(app, user, freezer):
agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
label='Event',
start_datetime=now(),
recurrence_days=list(range(7)),
places=2,
waiting_list_places=1,
agenda=agenda,
recurrence_end_date=now() + datetime.timedelta(days=14), # 2 weeks
)
event.create_all_recurrences()
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
child = Person.objects.create(user_external_id='child_id', name='James Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
agenda.children.add(child)
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, weeks='odd', days=[0, 1, 2])
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, weeks='even', days=[3, 4, 5])
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, weeks='even', days=[0, 1, 2])
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, weeks='odd', days=[3, 4, 5])
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[6])
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = (
'/api/agendas/recurring-events/fillslots/?agendas=foo-bar&action=update&guardian_external_id=%s'
)
params = {
'user_external_id': 'child_id',
'slots': ','.join('foo-bar@event:%s' % i for i in range(7)), # book every days
'include_booked_events_detail': True,
}
resp = app.post_json(fillslots_url % 'father_id', params=params)
assert resp.json['booking_count'] == 6
assert [x['date'] for x in resp.json['booked_events']] == [
'2022-03-10',
'2022-03-11',
'2022-03-12',
'2022-03-14',
'2022-03-15',
'2022-03-16',
]
resp = app.post_json(fillslots_url % 'mother_id', params=params)
assert resp.json['booking_count'] == 8
assert [x['date'] for x in resp.json['booked_events']] == [
'2022-03-07',
'2022-03-08',
'2022-03-09',
'2022-03-13',
'2022-03-17',
'2022-03-18',
'2022-03-19',
'2022-03-20',
]