From 1744b1d7df99ec4409eefdd80a7156bade5a766f Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Wed, 1 Dec 2021 15:08:18 +0100 Subject: [PATCH 2/4] api: add support for subscriptions in multiple agendas fillslots (#58446) --- chrono/api/serializers.py | 10 +-- chrono/api/views.py | 26 ++++++- tests/api/test_fillslot.py | 137 +++++++++++++++++++++++++++++++++++-- 3 files changed, 161 insertions(+), 12 deletions(-) diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py index aae605e3..64380e6a 100644 --- a/chrono/api/serializers.py +++ b/chrono/api/serializers.py @@ -95,9 +95,7 @@ class MultipleAgendasEventsSlotsSerializer(EventsSlotsSerializer): extra_agendas = slots_agenda_slugs - set(allowed_agenda_slugs) if extra_agendas: extra_agendas = ', '.join(sorted(extra_agendas)) - raise ValidationError( - _('Some events belong to agendas that are not present in querystring: %s' % extra_agendas) - ) + raise ValidationError(_('Events from the following agendas cannot be booked: %s') % extra_agendas) return value @@ -214,7 +212,7 @@ class AgendaOrSubscribedSlugsMixin(metaclass=serializers.SerializerMetaclass): raise ValidationError(_('Either "agendas" or "subscribed" parameter is required.')) if 'agendas' in attrs and 'subscribed' in attrs: raise ValidationError(_('"agendas" and "subscribed" parameters are mutually exclusive.')) - user_external_id = attrs.get('user_external_id') + user_external_id = attrs.get('user_external_id', self.context.get('user_external_id')) if 'subscribed' in attrs and not user_external_id: raise ValidationError( {'user_external_id': _('This field is required when using "subscribed" parameter.')} @@ -237,6 +235,10 @@ class MultipleAgendasDatetimesSerializer(AgendaOrSubscribedSlugsMixin, Datetimes show_past_events = serializers.BooleanField(default=False) +class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, serializers.Serializer): + pass + + class AgendaSlugsSerializer(serializers.Serializer): agendas = CommaSeparatedStringField( required=True, child=serializers.SlugField(max_length=160, allow_blank=False) diff --git a/chrono/api/views.py b/chrono/api/views.py index e1d77cad..8bfcbec5 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -1696,8 +1696,16 @@ class MultipleAgendasEventsFillslots(EventsFillslots): serializer_class = serializers.MultipleAgendasEventsSlotsSerializer def post(self, request): - self.agenda_slugs = get_agendas_from_request(request) - self.agendas = get_objects_from_slugs(self.agenda_slugs, qs=Agenda.objects.filter(kind='events')) + serializer = serializers.AgendaOrSubscribedSlugsSerializer( + data=request.query_params, context={'user_external_id': request.data.get('user_external_id')} + ) + if not serializer.is_valid(): + raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors) + data = serializer.validated_data + + self.agendas = data['agendas'] + self.agenda_slugs = data['agenda_slugs'] + return self.fillslots(request) def get_events(self, request, payload): @@ -1714,6 +1722,20 @@ class MultipleAgendasEventsFillslots(EventsFillslots): for agenda_slug, event_slugs in events_by_agenda.items(): events |= get_events_from_slots(event_slugs, request, agendas_by_slug[agenda_slug], payload) + if 'subscribed' in request.query_params: + events_outside_subscriptions = events.difference( + events.filter( + agenda__subscriptions__user_external_id=payload['user_external_id'], + agenda__subscriptions__date_start__lt=F('start_datetime'), + agenda__subscriptions__date_end__gt=F('start_datetime'), + ) + ) # workaround exclude method bug https://code.djangoproject.com/ticket/29697 + if events_outside_subscriptions.exists(): + event_slugs = ', '.join( + '%s@%s' % (event.agenda.slug, event.slug) for event in events_outside_subscriptions + ) + raise APIErrorBadRequest(N_('Some events are outside user subscriptions: %s'), event_slugs) + return events def get_already_booked_events(self, user_external_id): diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py index 66b26627..ed248452 100644 --- a/tests/api/test_fillslot.py +++ b/tests/api/test_fillslot.py @@ -11,10 +11,12 @@ from chrono.agendas.models import ( Agenda, Booking, BookingColor, + Category, Desk, Event, MeetingType, Resource, + Subscription, TimePeriod, VirtualMember, ) @@ -2416,7 +2418,7 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user): resp = app.post_json(fillslots_url % 'second-agenda', params=params, status=400) assert resp.json['err'] == 1 assert resp.json['errors']['slots'] == [ - 'Some events belong to agendas that are not present in querystring: first-agenda' + 'Events from the following agendas cannot be booked: first-agenda' ] @@ -2671,19 +2673,19 @@ def test_api_events_fillslots_multiple_agendas(app, user): resp = app.post_json( '/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params, status=400 ) - assert resp.json['errors']['slots'] == [ - 'Some events belong to agendas that are not present in querystring: xxx, yyy' - ] + assert resp.json['errors']['slots'] == ['Events from the following agendas cannot be booked: xxx, yyy'] # missing agendas parameter resp = app.post_json('/api/agendas/events/fillslots/', params=params, status=400) - assert resp.json['errors']['agendas'] == ['This field is required.'] + assert resp.json['errors']['non_field_errors'] == [ + 'Either "agendas" or "subscribed" parameter is required.' + ] # valid agendas parameter and event slugs, but mismatch between the two params = {'user_external_id': 'user_id_3', 'slots': event_slugs} resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400) assert resp.json['errors']['slots'] == [ - 'Some events belong to agendas that are not present in querystring: second-agenda' + 'Events from the following agendas cannot be booked: second-agenda' ] # missing @ in slot @@ -2754,6 +2756,129 @@ def test_api_events_fillslots_multiple_agendas_check_delays(app, user): assert resp.json['err'] == 0 +@pytest.mark.freeze_time('2021-09-06 12:00') +def test_api_events_fillslots_multiple_agendas_subscribed(app, user): + category = Category.objects.create(label='Category A') + first_agenda = Agenda.objects.create(label='First agenda', kind='events', category=category) + second_agenda = Agenda.objects.create(label='Second agenda', kind='events', category=category) + category = Category.objects.create(label='Category B') + third_agenda = Agenda.objects.create(label='Third agenda', kind='events', category=category) + for agenda in Agenda.objects.all(): + Event.objects.create( + slug='event', + start_datetime=now() + datetime.timedelta(days=5), + places=5, + agenda=agenda, + ) + Event.objects.create( + slug='event-2', + start_datetime=now() + datetime.timedelta(days=20), + places=5, + agenda=agenda, + ) + + # add subscriptions to first and second agenda + for agenda in (first_agenda, second_agenda): + Subscription.objects.create( + agenda=agenda, + user_external_id='xxx', + date_start=now(), + date_end=now() + datetime.timedelta(days=10), + ) + + # book events + app.authorization = ('Basic', ('john.doe', 'password')) + params = {'user_external_id': 'xxx', 'slots': 'first-agenda@event,second-agenda@event'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params) + assert resp.json['booking_count'] == 2 + assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1 + assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1 + assert Booking.objects.count() == 2 + + # update bookings for category-a + params = {'user_external_id': 'xxx', 'slots': 'second-agenda@event'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params) + assert resp.json['booking_count'] == 0 + assert resp.json['cancelled_booking_count'] == 1 + assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0 + assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1 + assert Booking.objects.count() == 1 + + # try to book event from agenda with no subscription TODO messages + params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'} + for slug in ('all', 'category-a', 'category-b'): + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=%s' % slug, params=params, status=400) + assert ( + resp.json['errors']['slots'][0] + == 'Events from the following agendas cannot be booked: third-agenda' + ) + + # add subscription to third agenda + Subscription.objects.create( + agenda=third_agenda, + user_external_id='xxx', + date_start=now(), + date_end=now() + datetime.timedelta(days=10), + ) + params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-b', params=params) + assert resp.json['booking_count'] == 1 + assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0 + assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1 + assert Event.objects.get(agenda=third_agenda, slug='event').booking_set.count() == 1 + assert Booking.objects.count() == 2 + + # add subscription to first agenda spanning event-2 + for agenda in (first_agenda, second_agenda): + Subscription.objects.create( + agenda=agenda, + user_external_id='xxx', + date_start=now() + datetime.timedelta(days=15), + date_end=now() + datetime.timedelta(days=25), + ) + # book event-2 while updating all bookings + params = {'user_external_id': 'xxx', 'slots': 'first-agenda@event,second-agenda@event-2'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params) + assert resp.json['booking_count'] == 2 + assert resp.json['cancelled_booking_count'] == 2 + assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1 + assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 1 + assert Booking.objects.count() == 2 + + # other user + for agenda in (first_agenda, second_agenda): + Subscription.objects.create( + agenda=agenda, + user_external_id='yyy', + date_start=now(), + date_end=now() + datetime.timedelta(days=25), + ) + params = {'user_external_id': 'yyy', 'slots': 'first-agenda@event,second-agenda@event-2'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params) + assert resp.json['booking_count'] == 2 + assert resp.json['cancelled_booking_count'] == 0 + assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 2 + assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 2 + assert Booking.objects.count() == 4 + + # try to book event outside subscription date range + params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event-2'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params, status=400) + assert resp.json['err_class'] == 'Some events are outside user subscriptions: third-agenda@event-2' + + # mismatch between subscribed parameter and event + params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params, status=400) + assert ( + resp.json['errors']['slots'][0] == 'Events from the following agendas cannot be booked: third-agenda' + ) + + # missing user_external_id + params = {'slots': 'third-agenda@event'} + resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params, status=400) + assert 'required' in resp.json['errors']['user_external_id'][0] + + def test_url_translation(app, some_data, user): app.authorization = ('Basic', ('john.doe', 'password')) agenda_id = Agenda.objects.filter(label='Foo bar')[0].id -- 2.30.2