api: configure shared custody agenda on creation (#64423)

This commit is contained in:
Valentin Deniaud 2022-04-26 10:59:33 +02:00
parent 513980d5b1
commit 3fab3b136c
3 changed files with 321 additions and 51 deletions

View File

@ -1,11 +1,27 @@
import collections
import datetime
from django.contrib.auth.models import Group
from django.db import models, transaction
from django.db.models import ExpressionWrapper, F
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from chrono.agendas.models import Agenda, Booking, Category, CheckType, Event, EventsType, Subscription
from chrono.agendas.models import (
Agenda,
Booking,
Category,
CheckType,
Event,
EventsType,
Person,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
SharedCustodyRule,
Subscription,
TimePeriodExceptionGroup,
)
def get_objects_from_slugs(slugs, qs):
@ -488,16 +504,144 @@ class SubscriptionSerializer(serializers.ModelSerializer):
return attrs
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ['user_external_id', 'first_name', 'last_name']
extra_kwargs = {'user_external_id': {'validators': []}}
class SharedCustodyAgendaSerializer(serializers.Serializer):
first_guardian_first_name = serializers.CharField(max_length=250)
first_guardian_last_name = serializers.CharField(max_length=250)
first_guardian_id = serializers.CharField(max_length=250)
second_guardian_first_name = serializers.CharField(max_length=250)
second_guardian_last_name = serializers.CharField(max_length=250)
second_guardian_id = serializers.CharField(max_length=250)
period_mirrors = {
'even': 'odd',
'odd': 'even',
'first-half': 'second-half',
'second-half': 'first-half',
'first-and-third-quarters': 'second-and-fourth-quarters',
'second-and-fourth-quarters': 'first-and-third-quarters',
}
guardian_first_name = serializers.CharField(max_length=250)
guardian_last_name = serializers.CharField(max_length=250)
guardian_id = serializers.CharField(max_length=250)
other_guardian_first_name = serializers.CharField(max_length=250)
other_guardian_last_name = serializers.CharField(max_length=250)
other_guardian_id = serializers.CharField(max_length=250)
children = PersonSerializer(many=True)
weeks = serializers.ChoiceField(required=False, choices=['', 'even', 'odd'])
class SharedCustodyChildSerializer(serializers.Serializer):
first_name = serializers.CharField(max_length=250)
last_name = serializers.CharField(max_length=250)
user_external_id = serializers.CharField(max_length=250)
settings_url = serializers.SerializerMethodField()
def validate(self, attrs):
attrs['holidays'] = collections.defaultdict(dict)
for key, value in self.initial_data.items():
if key in attrs or ':' not in key:
continue
holiday_slug, field = key.split(':')
if field not in ('periodicity', 'years'):
raise ValidationError(
_('Unknown parameter for holiday %(holiday)s: %(param)s')
% {'holiday': holiday_slug, 'param': field}
)
attrs['holidays'][holiday_slug][field] = value
for holiday_slug in attrs['holidays'].copy():
try:
holiday = TimePeriodExceptionGroup.objects.get(slug=holiday_slug)
except TimePeriodExceptionGroup.DoesNotExist:
raise ValidationError(_('Unknown holiday: %s') % holiday_slug)
field_values = attrs['holidays'].pop(holiday_slug)
if 'periodicity' not in field_values:
raise ValidationError(_('Missing periodicity for holiday: %s') % holiday_slug)
holidays = holiday.exceptions.annotate(
delta=ExpressionWrapper(
F('end_datetime') - F('start_datetime'), output_field=models.DurationField()
)
)
is_short_holiday = holidays.filter(delta__lt=datetime.timedelta(days=28)).exists()
if 'quarter' in field_values['periodicity'] and is_short_holiday:
raise ValidationError(_('Short holidays cannot be cut into quarters.'))
attrs['holidays'][holiday] = field_values
return attrs
@transaction.atomic
def create(self, validated_data):
guardian, dummy = Person.objects.get_or_create(
user_external_id=validated_data['guardian_id'],
defaults={
'first_name': validated_data['guardian_first_name'],
'last_name': validated_data['guardian_last_name'],
},
)
other_guardian, dummy = Person.objects.get_or_create(
user_external_id=validated_data['other_guardian_id'],
defaults={
'first_name': validated_data['other_guardian_first_name'],
'last_name': validated_data['other_guardian_last_name'],
},
)
self.agenda = SharedCustodyAgenda.objects.create(
first_guardian=guardian, second_guardian=other_guardian
)
children = []
children_data = validated_data.pop('children')
for child in children_data:
children.append(
Person.objects.get_or_create(
user_external_id=child['user_external_id'],
defaults={'first_name': child['first_name'], 'last_name': child['last_name']},
)[0]
)
self.agenda.children.set(children)
if validated_data.get('weeks'):
self.create_custody_rules(guardian, validated_data['weeks'], create_mirror_for=other_guardian)
for holiday, params in validated_data.get('holidays', {}).items():
self.create_holiday_rules(
holiday,
guardian,
create_mirror_for=other_guardian,
periodicity=params['periodicity'],
years=params.get('years', ''),
)
return self.agenda
def create_custody_rules(self, guardian, weeks, create_mirror_for=None):
SharedCustodyRule.objects.create(
agenda=self.agenda, days=list(range(7)), weeks=weeks, guardian=guardian
)
if create_mirror_for:
self.create_custody_rules(create_mirror_for, self.period_mirrors[weeks])
def create_holiday_rules(self, holiday, guardian, years, periodicity, create_mirror_for=None):
rule = SharedCustodyHolidayRule.objects.create(
agenda=self.agenda, holiday=holiday, guardian=guardian, years=years, periodicity=periodicity
)
rule.update_or_create_periods()
if years:
rule = SharedCustodyHolidayRule.objects.create(
agenda=self.agenda,
holiday=holiday,
guardian=guardian,
years=self.period_mirrors[years],
periodicity=self.period_mirrors[periodicity],
)
rule.update_or_create_periods()
if create_mirror_for:
self.create_holiday_rules(holiday, create_mirror_for, years, self.period_mirrors[periodicity])
def get_settings_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(obj.get_settings_url())

View File

@ -2773,27 +2773,7 @@ class SharedCustodyAgendas(APIView):
serializer = self.serializer_class(data=request.data)
if not serializer.is_valid():
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
data = serializer.validated_data
with transaction.atomic():
first_guardian, dummy = Person.objects.get_or_create(
user_external_id=data['first_guardian_id'],
defaults={
'first_name': data['first_guardian_first_name'],
'last_name': data['first_guardian_last_name'],
},
)
second_guardian, dummy = Person.objects.get_or_create(
user_external_id=data['second_guardian_id'],
defaults={
'first_name': data['second_guardian_first_name'],
'last_name': data['second_guardian_last_name'],
},
)
agenda = SharedCustodyAgenda.objects.create(
first_guardian=first_guardian, second_guardian=second_guardian
)
agenda = serializer.save()
response = {
'id': agenda.pk,
@ -2807,7 +2787,7 @@ shared_custody_agendas = SharedCustodyAgendas.as_view()
class SharedCustodyAgendaAddChild(APIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.SharedCustodyChildSerializer
serializer_class = serializers.PersonSerializer
def post(self, request, agenda_pk):
agenda = get_object_or_404(SharedCustodyAgenda, pk=agenda_pk)

View File

@ -1,29 +1,49 @@
import pytest
from django.core.files.base import ContentFile
from chrono.agendas.models import Person, SharedCustodyAgenda
from chrono.agendas.models import Person, SharedCustodyAgenda, UnavailabilityCalendar
pytestmark = pytest.mark.django_db
with open('tests/data/holidays.ics') as f:
ICS_HOLIDAYS = f.read()
def test_add_shared_custody_agenda(app, user, settings):
app.authorization = ('Basic', ('john.doe', 'password'))
params = {
'first_guardian_first_name': 'John',
'first_guardian_last_name': 'Doe',
'first_guardian_id': 'xxx',
'second_guardian_first_name': 'Jane',
'second_guardian_last_name': 'Doe',
'second_guardian_id': 'yyy',
'guardian_first_name': 'John',
'guardian_last_name': 'Doe',
'guardian_id': 'xxx',
'other_guardian_first_name': 'Jane',
'other_guardian_last_name': 'Doe',
'other_guardian_id': 'yyy',
'children': [
{
'first_name': 'James',
'last_name': 'Doe',
'user_external_id': 'zzz',
},
{
'first_name': 'Arthur',
'last_name': 'Doe',
'user_external_id': '123',
},
],
}
resp = app.post_json('/api/shared-custody/', params=params)
john = Person.objects.get(user_external_id='xxx', first_name='John', last_name='Doe')
jane = Person.objects.get(user_external_id='yyy', first_name='Jane', last_name='Doe')
first_guardian = Person.objects.get(user_external_id='xxx', first_name='John', last_name='Doe')
second_guardian = Person.objects.get(user_external_id='yyy', first_name='Jane', last_name='Doe')
first_chidren = Person.objects.get(user_external_id='zzz', first_name='James', last_name='Doe')
second_children = Person.objects.get(user_external_id='123', first_name='Arthur', last_name='Doe')
agenda = SharedCustodyAgenda.objects.get()
assert agenda.first_guardian == john
assert agenda.second_guardian == jane
assert agenda.first_guardian == first_guardian
assert agenda.second_guardian == second_guardian
assert set(agenda.children.all()) == {first_chidren, second_children}
assert resp.json['data'] == {
'id': agenda.pk,
@ -31,16 +51,142 @@ def test_add_shared_custody_agenda(app, user, settings):
}
params = {
'first_guardian_first_name': 'John',
'first_guardian_last_name': 'Doe',
'first_guardian_id': 'xxx',
'second_guardian_first_name': 'Other',
'second_guardian_last_name': 'Other',
'second_guardian_id': 'zzz',
'guardian_first_name': 'John',
'guardian_last_name': 'Doe',
'guardian_id': 'xxx',
'other_guardian_first_name': 'Other',
'other_guardian_last_name': 'Doe',
'other_guardian_id': 'other',
'children': [
{
'first_name': 'Bruce',
'last_name': 'Doe',
'user_external_id': 'bruce',
},
],
}
resp = app.post_json('/api/shared-custody/', params=params)
assert resp.json['data']['id'] != agenda.pk
assert SharedCustodyAgenda.objects.filter(first_guardian=john).count() == 2
assert SharedCustodyAgenda.objects.filter(first_guardian=first_guardian).count() == 2
def test_add_shared_custody_agenda_with_rules(app, user, settings):
app.authorization = ('Basic', ('john.doe', 'password'))
# configure holidays
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar', slug='chrono-holidays')
source = unavailability_calendar.timeperiodexceptionsource_set.create(
ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics')
)
source.refresh_timeperiod_exceptions_from_ics()
params = {
'guardian_first_name': 'John',
'guardian_last_name': 'Doe',
'guardian_id': 'xxx',
'other_guardian_first_name': 'Jane',
'other_guardian_last_name': 'Doe',
'other_guardian_id': 'yyy',
'children': [
{
'first_name': 'James',
'last_name': 'Doe',
'user_external_id': 'zzz',
},
],
}
resp = app.post_json('/api/shared-custody/', params={'weeks': '', **params})
agenda = SharedCustodyAgenda.objects.get(pk=resp.json['data']['id'])
assert not agenda.is_complete()
assert not agenda.rules.exists()
resp = app.post_json('/api/shared-custody/', params={'weeks': 'even', **params})
agenda = SharedCustodyAgenda.objects.get(pk=resp.json['data']['id'])
assert agenda.is_complete()
assert agenda.rules.filter(guardian__first_name='John', weeks='even').exists()
assert agenda.rules.filter(guardian__first_name='Jane', weeks='odd').exists()
resp = app.post_json('/api/shared-custody/', params={'weeks': 'odd', **params})
agenda = SharedCustodyAgenda.objects.get(pk=resp.json['data']['id'])
assert agenda.is_complete()
assert agenda.rules.filter(guardian__first_name='John', weeks='odd').exists()
assert agenda.rules.filter(guardian__first_name='Jane', weeks='even').exists()
resp = app.post_json(
'/api/shared-custody/', params={'christmas_holidays:periodicity': 'first-half', **params}
)
agenda = SharedCustodyAgenda.objects.get(pk=resp.json['data']['id'])
assert agenda.holiday_rules.filter(
guardian__first_name='John', holiday__slug='christmas_holidays', periodicity='first-half', years=''
).exists()
assert agenda.holiday_rules.filter(
guardian__first_name='Jane', holiday__slug='christmas_holidays', periodicity='second-half', years=''
).exists()
assert agenda.periods.count() == 12
resp = app.post_json(
'/api/shared-custody/',
params={
'summer_holidays:periodicity': 'first-and-third-quarters',
'summer_holidays:years': 'odd',
**params,
},
)
agenda = SharedCustodyAgenda.objects.get(pk=resp.json['data']['id'])
assert agenda.holiday_rules.filter(
guardian__first_name='John',
holiday__slug='summer_holidays',
periodicity='first-and-third-quarters',
years='odd',
).exists()
assert agenda.holiday_rules.filter(
guardian__first_name='John',
holiday__slug='summer_holidays',
periodicity='second-and-fourth-quarters',
years='even',
).exists()
assert agenda.holiday_rules.filter(
guardian__first_name='Jane',
holiday__slug='summer_holidays',
periodicity='first-and-third-quarters',
years='even',
).exists()
assert agenda.holiday_rules.filter(
guardian__first_name='Jane',
holiday__slug='summer_holidays',
periodicity='second-and-fourth-quarters',
years='odd',
).exists()
assert agenda.periods.count() == 20
# unknown holiday
resp = app.post_json(
'/api/shared-custody/', params={'unknown:periodicity': 'first-half', **params}, status=400
)
assert resp.json['errors']['non_field_errors'][0] == 'Unknown holiday: unknown'
# unknown holiday param
resp = app.post_json(
'/api/shared-custody/', params={'summer_holidays:unknown': 'first-half', **params}, status=400
)
assert (
resp.json['errors']['non_field_errors'][0] == 'Unknown parameter for holiday summer_holidays: unknown'
)
# years without periodicity
resp = app.post_json(
'/api/shared-custody/', params={'summer_holidays:years': 'even', **params}, status=400
)
assert resp.json['errors']['non_field_errors'][0] == 'Missing periodicity for holiday: summer_holidays'
# quarters with short holiday
resp = app.post_json(
'/api/shared-custody/',
params={'christmas_holidays:periodicity': 'first-and-third-quarters', **params},
status=400,
)
assert resp.json['errors']['non_field_errors'][0] == 'Short holidays cannot be cut into quarters.'
def test_shared_custody_agenda_add_child(app, user, settings):