From 57cb74a2edb43b7483626a4b0e34230d1118950a Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Wed, 30 Nov 2022 16:57:32 +0100 Subject: [PATCH] api: set date end on shared custody agenda (#71633) --- chrono/api/serializers.py | 45 +++++++++++++-- chrono/api/views.py | 4 +- tests/api/test_shared_custody.py | 99 +++++++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 8 deletions(-) diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py index c451d81b..bfb07f6d 100644 --- a/chrono/api/serializers.py +++ b/chrono/api/serializers.py @@ -3,7 +3,7 @@ import datetime from django.contrib.auth.models import Group from django.db import models, transaction -from django.db.models import ExpressionWrapper, F +from django.db.models import ExpressionWrapper, F, Q from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -585,7 +585,34 @@ class SubscriptionSerializer(serializers.ModelSerializer): return attrs -class SharedCustodyAgendaCreateSerializer(serializers.Serializer): +class SharedCustodyAgendaMixin: + def validate(self, attrs): + date_start = attrs['date_start'] + date_end = attrs.get('date_end') + + if date_end and date_start >= date_end: + raise ValidationError(_('date_start must be before date_end')) + + child_id = self.get_child_id(attrs) + overlapping_agendas = SharedCustodyAgenda.objects.filter( + Q(date_end__gte=date_start) | Q(date_end__isnull=True), child__user_external_id=child_id + ) + if date_end: + overlapping_agendas = overlapping_agendas.filter(date_start__lte=date_end) + if self.instance: + overlapping_agendas = overlapping_agendas.exclude(pk=self.instance.pk) + + if overlapping_agendas: + request = self.context.get('request') + raise ValidationError( + _('Invalid date_start/date_end, agenda would overlap with %s.') + % request.build_absolute_uri(overlapping_agendas[0].get_absolute_url()) + ) + + return attrs + + +class SharedCustodyAgendaCreateSerializer(SharedCustodyAgendaMixin, serializers.Serializer): period_mirrors = { 'even': 'odd', 'odd': 'even', @@ -606,10 +633,16 @@ class SharedCustodyAgendaCreateSerializer(serializers.Serializer): child_id = serializers.CharField(max_length=250) weeks = serializers.ChoiceField(required=False, choices=['', 'even', 'odd']) date_start = serializers.DateField(required=True) + date_end = serializers.DateField(required=False) settings_url = serializers.SerializerMethodField() + def get_child_id(self, attrs): + return attrs['child_id'] + def validate(self, attrs): + super().validate(attrs) + attrs['holidays'] = collections.defaultdict(dict) for key, value in self.initial_data.items(): if key in attrs or ':' not in key: @@ -675,6 +708,7 @@ class SharedCustodyAgendaCreateSerializer(serializers.Serializer): second_guardian=other_guardian, child=child, date_start=validated_data['date_start'], + date_end=validated_data.get('date_end'), ) if validated_data.get('weeks'): @@ -723,7 +757,10 @@ class SharedCustodyAgendaCreateSerializer(serializers.Serializer): return request.build_absolute_uri(obj.get_settings_url()) -class SharedCustodyAgendaSerializer(serializers.ModelSerializer): +class SharedCustodyAgendaSerializer(SharedCustodyAgendaMixin, serializers.ModelSerializer): class Meta: model = SharedCustodyAgenda - fields = ['date_start'] + fields = ['date_start', 'date_end'] + + def get_child_id(self, attrs): + return self.instance.child.user_external_id diff --git a/chrono/api/views.py b/chrono/api/views.py index 1c9ef9d8..4c15a24f 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -3013,7 +3013,7 @@ class SharedCustodyAgendas(APIView): serializer_class = serializers.SharedCustodyAgendaCreateSerializer def post(self, request): - serializer = self.serializer_class(data=request.data) + serializer = self.serializer_class(data=request.data, context={'request': request}) if not serializer.is_valid(): raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors) agenda = serializer.save() @@ -3036,7 +3036,7 @@ class SharedCustodyAgendaAPI(APIView): def patch(self, request, agenda_pk): agenda = get_object_or_404(SharedCustodyAgenda, pk=agenda_pk) - serializer = self.serializer_class(agenda, data=request.data) + serializer = self.serializer_class(agenda, data=request.data, context={'request': request}) if not serializer.is_valid(): raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors) agenda = serializer.save() diff --git a/tests/api/test_shared_custody.py b/tests/api/test_shared_custody.py index d68ddfae..f2061d7a 100644 --- a/tests/api/test_shared_custody.py +++ b/tests/api/test_shared_custody.py @@ -45,6 +45,53 @@ def test_add_shared_custody_agenda(app, user, settings): 'backoffice_url': 'http://testserver/manage/shared-custody/%s/' % agenda.pk, } + # cannot add agenda without date_end + params['date_start'] = '2019-01-01' + resp = app.post_json('/api/shared-custody/', params=params, status=400) + assert resp.json['errors']['non_field_errors'][0] == ( + 'Invalid date_start/date_end, agenda would overlap with http://testserver/manage/shared-custody/%s/.' + % agenda.pk + ) + + # cannot add agenda with date_end after existing agenda date_start + params['date_start'] = '2019-01-01' + params['date_end'] = '2020-11-01' + resp = app.post_json('/api/shared-custody/', params=params, status=400) + assert resp.json['errors']['non_field_errors'][0] == ( + 'Invalid date_start/date_end, agenda would overlap with http://testserver/manage/shared-custody/%s/.' + % agenda.pk + ) + + # can add agenda with date_end before existing agenda date_start + params['date_start'] = '2019-01-01' + params['date_end'] = '2020-01-01' + resp = app.post_json('/api/shared-custody/', params=params) + assert resp.json['err'] == 0 + new_agenda_id = resp.json['data']['id'] + + # cannot add agenda that would overlap previous one + params['date_start'] = '2018-01-01' + params['date_end'] = '2019-01-01' + resp = app.post_json('/api/shared-custody/', params=params, status=400) + assert resp.json['errors']['non_field_errors'][0] == ( + 'Invalid date_start/date_end, agenda would overlap with http://testserver/manage/shared-custody/%s/.' + % new_agenda_id + ) + + params['date_start'] = '2020-01-01' + params['date_end'] = '2020-02-01' + resp = app.post_json('/api/shared-custody/', params=params, status=400) + assert resp.json['errors']['non_field_errors'][0] == ( + 'Invalid date_start/date_end, agenda would overlap with http://testserver/manage/shared-custody/%s/.' + % new_agenda_id + ) + + params['date_start'] = '2021-02-01' + params['date_end'] = '2021-01-01' + resp = app.post_json('/api/shared-custody/', params=params, status=400) + assert resp.json['errors']['non_field_errors'][0] == 'date_start must be before date_end' + + # different child, no overlap check params = { 'guardian_first_name': 'John', 'guardian_last_name': 'Doe', @@ -59,7 +106,7 @@ def test_add_shared_custody_agenda(app, user, settings): } resp = app.post_json('/api/shared-custody/', params=params) assert resp.json['data']['id'] != agenda.pk - assert SharedCustodyAgenda.objects.filter(first_guardian=first_guardian).count() == 2 + assert SharedCustodyAgenda.objects.filter(first_guardian=first_guardian).count() == 3 def test_add_shared_custody_agenda_with_rules(app, user, settings): @@ -184,7 +231,7 @@ def test_add_shared_custody_agenda_with_rules(app, user, settings): assert resp.json['errors']['non_field_errors'][0] == 'Short holidays cannot be cut into quarters.' -def test_shared_custody_agenda_update_date_start(app, user, settings): +def test_shared_custody_agenda_update_dates(app, user, settings): 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') @@ -198,6 +245,54 @@ def test_shared_custody_agenda_update_date_start(app, user, settings): agenda.refresh_from_db() assert agenda.date_start == datetime.date(year=2020, month=10, day=20) + assert agenda.date_end is None + + resp = app.patch_json( + '/api/shared-custody/%s/' % agenda.pk, params={'date_start': '2020-10-20', 'date_end': '2021-01-01'} + ) + assert resp.json['err'] == 0 + + agenda.refresh_from_db() + assert agenda.date_start == datetime.date(year=2020, month=10, day=20) + assert agenda.date_end == datetime.date(year=2021, month=1, day=1) + + resp = app.patch_json( + '/api/shared-custody/%s/' % agenda.pk, params={'date_start': '2021-01-01', 'date_end': '2022-01-01'} + ) + assert resp.json['err'] == 0 + + agenda.refresh_from_db() + assert agenda.date_start == datetime.date(year=2021, month=1, day=1) + assert agenda.date_end == datetime.date(year=2022, month=1, day=1) + + resp = app.patch_json( + '/api/shared-custody/%s/' % agenda.pk, params={'date_start': '2021-01-01', 'date_end': None} + ) + assert resp.json['err'] == 0 + + agenda.refresh_from_db() + assert agenda.date_start == datetime.date(year=2021, month=1, day=1) + assert agenda.date_end is None + + SharedCustodyAgenda.objects.create( + first_guardian=father, + second_guardian=mother, + child=child, + date_start=datetime.date(year=2020, month=1, day=1), + date_end=datetime.date(year=2020, month=6, day=1), + ) + + resp = app.patch_json( + '/api/shared-custody/%s/' % agenda.pk, params={'date_start': '2020-01-01'}, status=400 + ) + assert 'overlap' in resp.json['errors']['non_field_errors'][0] + + resp = app.patch_json( + '/api/shared-custody/%s/' % agenda.pk, + params={'date_start': '2020-10-20', 'date_end': '2019-10-20'}, + status=400, + ) + assert resp.json['errors']['non_field_errors'][0] == 'date_start must be before date_end' resp = app.patch_json('/api/shared-custody/%s/' % agenda.pk, params={'first_guardian': 'xxx'}, status=400) app.patch_json('/api/shared-custody/%s/' % agenda.pk, params={}, status=400)