api: add shared custody support in recurring event list and fillslots (#63048)
This commit is contained in:
parent
9a8e19d0c6
commit
afe588a1fb
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue