diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py index feba6530..a02b7605 100644 --- a/chrono/api/serializers.py +++ b/chrono/api/serializers.py @@ -188,6 +188,18 @@ class RecurringFillslotsSerializer(MultipleAgendasEventsFillSlotsSerializer): check_overlaps = CommaSeparatedStringField( required=False, child=serializers.SlugField(max_length=160, allow_blank=False) ) + start_time = serializers.TimeField(required=False) + end_time = serializers.TimeField(required=False) + + def validate(self, attrs): + super().validate(attrs) + use_partial_bookings = any(agenda.partial_bookings for agenda in self.context['agendas']) + if use_partial_bookings: + if not attrs.get('start_time') or not attrs.get('end_time'): + raise ValidationError(_('must include start_time and end_time for partial bookings agenda')) + if attrs['start_time'] > attrs['end_time']: + raise ValidationError(_('start_time must be before end_time')) + return attrs def validate_slots(self, value): super().validate_slots(value) @@ -401,6 +413,14 @@ class AgendaOrSubscribedSlugsMixin(AgendaSlugsMixin): attrs['agenda_slugs'] = [agenda.slug for agenda in agendas] else: attrs['agenda_slugs'] = self.agenda_slugs + + if any( + agenda.partial_bookings != attrs['agendas'][0].partial_bookings for agenda in attrs['agendas'] + ): + raise serializers.ValidationError( + {'agendas': _('Cannot mix partial bookings agendas with other kinds.')} + ) + return attrs def validate_agendas(self, value): diff --git a/chrono/api/views.py b/chrono/api/views.py index b13bec83..e34b6b4b 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -461,6 +461,8 @@ def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_li user_display_label=payload.get('user_display_label', ''), extra_emails=payload.get('extra_emails', []), extra_phone_numbers=payload.get('extra_phone_numbers', []), + start_time=payload.get('start_time'), + end_time=payload.get('end_time'), extra_data=extra_data, color=color, ) diff --git a/tests/api/fillslot/test_recurring_events.py b/tests/api/fillslot/test_recurring_events.py index e804db7a..975abab1 100644 --- a/tests/api/fillslot/test_recurring_events.py +++ b/tests/api/fillslot/test_recurring_events.py @@ -16,7 +16,7 @@ from chrono.agendas.models import ( SharedCustodyRule, Subscription, ) -from chrono.utils.timezone import now +from chrono.utils.timezone import make_aware, now pytestmark = pytest.mark.django_db @@ -1609,7 +1609,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user): @pytest.mark.freeze_time('2021-09-06 12:00') -def test_recurring_events_api_fillslots_overlapping_events_partial_booking(app, user): +def test_recurring_events_api_fillslots_partly_overlapping_events(app, user): 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) @@ -1677,3 +1677,63 @@ def test_recurring_events_api_fillslots_overlapping_events_partial_booking(app, '2021-09-28', '2021-10-05', ] + + +@pytest.mark.freeze_time('2023-05-01 10:00') +def test_recurring_events_api_fillslots_partial_bookings(app, user): + agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True) + Desk.objects.create(agenda=agenda, slug='_exceptions_holder') + start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0)) + event = Event.objects.create( + label='Event 08-18', + start_datetime=start_datetime, + end_time=datetime.time(18, 00), + places=2, + recurrence_end_date=start_datetime + datetime.timedelta(days=30), + recurrence_days=[1], + agenda=agenda, + ) + event.create_all_recurrences() + + app.authorization = ('Basic', ('john.doe', 'password')) + + params = { + 'user_external_id': 'user_id', + 'slots': 'foo-bar@event-08-18:1', + 'start_time': '10:00', + 'end_time': '15:00', + } + fillslots_url = '/api/agendas/recurring-events/fillslots/?action=update&agendas=%s' % agenda.slug + resp = app.post_json(fillslots_url, params=params) + + assert Booking.objects.count() == 5 + for booking in Booking.objects.all(): + assert booking.start_time == datetime.time(10, 00) + assert booking.end_time == datetime.time(15, 00) + + # mix with other kind + other_agenda = Agenda.objects.create(label='Not partial', kind='events') + resp = app.post_json(fillslots_url + ',%s' % other_agenda.slug, params=params, status=400) + assert resp.json['errors']['agendas'][0] == 'Cannot mix partial bookings agendas with other kinds.' + + # missing start_time + del params['start_time'] + resp = app.post_json(fillslots_url, params=params, status=400) + assert ( + resp.json['errors']['non_field_errors'][0] + == 'must include start_time and end_time for partial bookings agenda' + ) + + # missing end_time + params['start_time'] = '10:00' + del params['end_time'] + resp = app.post_json(fillslots_url, params=params, status=400) + assert ( + resp.json['errors']['non_field_errors'][0] + == 'must include start_time and end_time for partial bookings agenda' + ) + + # end before start + params['end_time'] = '09:00' + resp = app.post_json(fillslots_url, params=params, status=400) + assert resp.json['errors']['non_field_errors'][0] == 'start_time must be before end_time'