From 41df2c58a5bb2274b4ac2d14adc178f543708149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Thu, 6 Apr 2023 14:38:08 +0200 Subject: [PATCH] api: events fillslots, return details of cancelled events (#76326) --- chrono/agendas/models.py | 2 +- chrono/api/views.py | 92 +++++++++++++++------ tests/api/fillslot/test_events.py | 11 ++- tests/api/fillslot/test_recurring_events.py | 35 ++++++++ 4 files changed, 114 insertions(+), 26 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index f8d5548d..b2ecf555 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -1797,7 +1797,7 @@ class Event(models.Model): if end_datetime: booked_events = booked_events.filter(start_datetime__lte=end_datetime) if exclude_events: - booked_events = booked_events.exclude(pk__in=exclude_events) + booked_events = booked_events.exclude(pk__in=[e.pk for e in exclude_events]) return Event.annotate_queryset_with_overlaps(qs, booked_events) diff --git a/chrono/api/views.py b/chrono/api/views.py index d00ef7a6..585a273b 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -487,26 +487,13 @@ def get_event_detail( bypass_delays=False, with_status=False, ): + details = get_short_event_detail( + request=request, + event=event, + agenda=agenda, + multiple_agendas=multiple_agendas, + ) agenda = agenda or event.agenda - details = { - 'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug, - 'slug': event.slug, # kept for compatibility - 'text': get_event_text(event, agenda), - 'label': event.label or '', - 'agenda_label': agenda.label, - 'date': format_response_date(event.start_datetime), - 'datetime': format_response_datetime(event.start_datetime), - 'end_datetime': format_response_datetime(event.end_datetime) if event.end_datetime else '', - 'description': event.description, - 'pricing': event.pricing, - 'url': event.url, - 'duration': event.duration, - 'checked': event.checked, - 'check_locked': event.check_locked, - 'invoiced': event.invoiced, - } - for key, value in event.get_custom_fields().items(): - details['custom_field_%s' % key] = value if booking: details['booking'] = { 'id': booking.pk, @@ -598,6 +585,35 @@ def get_event_detail( return details +def get_short_event_detail( + request, + event, + agenda=None, + multiple_agendas=False, +): + agenda = agenda or event.agenda + details = { + 'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug, + 'slug': event.slug, # kept for compatibility + 'text': get_event_text(event, agenda), + 'label': event.label or '', + 'agenda_label': agenda.label, + 'date': format_response_date(event.start_datetime), + 'datetime': format_response_datetime(event.start_datetime), + 'end_datetime': format_response_datetime(event.end_datetime) if event.end_datetime else '', + 'description': event.description, + 'pricing': event.pricing, + 'url': event.url, + 'duration': event.duration, + 'checked': event.checked, + 'check_locked': event.check_locked, + 'invoiced': event.invoiced, + } + for key, value in event.get_custom_fields().items(): + details['custom_field_%s' % key] = value + return details + + def get_events_meta_detail( request, events, @@ -1777,7 +1793,7 @@ class RecurringFillslots(APIView): end_datetime, user_external_id, guardian_external_id, - ).values_list('pk', flat=True) + ) if payload.get('check_overlaps'): self.check_for_overlaps(events_to_book, serializer.initial_slots) @@ -1830,12 +1846,21 @@ class RecurringFillslots(APIView): user_external_id=user_external_id, event__in=events_to_unbook, cancellation_datetime__isnull=True ) + events_by_id = {x.id: x for x in list(events_to_book) + list(events_to_unbook)} with transaction.atomic(): # cancel existing bookings cancellation_datetime = now() Booking.objects.filter(primary_booking__in=bookings_to_cancel).update( cancellation_datetime=cancellation_datetime ) + cancelled_events = [ + get_short_event_detail( + request, + events_by_id[x.event_id], + multiple_agendas=True, + ) + for x in bookings_to_cancel + ] cancelled_count = bookings_to_cancel.update(cancellation_datetime=cancellation_datetime) # and delete outdated cancelled bookings Booking.objects.filter( @@ -1851,11 +1876,11 @@ class RecurringFillslots(APIView): 'full_events': [get_event_detail(request, x, multiple_agendas=True) for x in full_events], } if payload.get('include_booked_events_detail'): - events_to_book_by_id = {x.id: x for x in events_to_book} response['booked_events'] = [ - get_event_detail(request, events_to_book_by_id[x.event_id], booking=x, multiple_agendas=True) + get_event_detail(request, events_by_id[x.event_id], booking=x, multiple_agendas=True) for x in created_bookings ] + response['cancelled_events'] = cancelled_events return Response(response) def get_event_recurrences( @@ -1903,7 +1928,7 @@ class RecurringFillslots(APIView): def get_events_to_unbook(self, agendas, events_to_book): events_to_book_ids = set(events_to_book.values_list('pk', flat=True)) events_to_unbook = [ - e.pk + e for agenda in agendas for e in agenda.prefetched_events if (e.user_places_count or e.user_waiting_places_count) @@ -2042,6 +2067,9 @@ class EventsFillslots(APIView): user_external_id=user_external_id, event__in=events_to_unbook, cancellation_datetime__isnull=True ) + events_by_id = { + x.id: x for x in (list(events) + events_to_unbook + events_to_unbook_out_of_min_delay) + } with transaction.atomic(): # cancel existing bookings cancellation_datetime = now() @@ -2049,12 +2077,28 @@ class EventsFillslots(APIView): cancellation_datetime=cancellation_datetime, out_of_min_delay=True, ) + cancelled_events = [ + get_short_event_detail( + request, + events_by_id[x.event_id], + multiple_agendas=self.multiple_agendas, + ) + for x in bookings_to_cancel_out_of_min_delay + ] cancelled_count = bookings_to_cancel_out_of_min_delay.update( cancellation_datetime=cancellation_datetime, out_of_min_delay=True ) Booking.objects.filter(primary_booking__in=bookings_to_cancel).update( cancellation_datetime=cancellation_datetime, out_of_min_delay=False ) + cancelled_events += [ + get_short_event_detail( + request, + events_by_id[x.event_id], + multiple_agendas=self.multiple_agendas, + ) + for x in bookings_to_cancel + ] cancelled_count += bookings_to_cancel.update( cancellation_datetime=cancellation_datetime, out_of_min_delay=False ) @@ -2068,7 +2112,6 @@ class EventsFillslots(APIView): # don't reload agendas and events types for event in events: event.agenda = agendas_by_ids[event.agenda_id] - events_by_id = {x.id: x for x in events} response = { 'err': 0, 'booking_count': len(bookings), @@ -2087,6 +2130,7 @@ class EventsFillslots(APIView): if x.event_id in waiting_list_event_ids ], 'cancelled_booking_count': cancelled_count, + 'cancelled_events': cancelled_events, } return Response(response) diff --git a/tests/api/fillslot/test_events.py b/tests/api/fillslot/test_events.py index 5b4a9624..b2a2a74d 100644 --- a/tests/api/fillslot/test_events.py +++ b/tests/api/fillslot/test_events.py @@ -46,6 +46,8 @@ def test_api_events_fillslots(app, user): == Booking.objects.filter(event=second_event).latest('pk').pk ) assert len(resp.json['waiting_list_events']) == 0 + assert resp.json['cancelled_booking_count'] == 0 + assert len(resp.json['cancelled_events']) == 0 events = Event.objects.all() assert events.filter(booked_places=1).count() == 2 @@ -64,6 +66,8 @@ def test_api_events_fillslots(app, user): resp.json['booked_events'][1]['booking']['id'] == Booking.objects.filter(event=second_event).latest('pk').pk ) + assert resp.json['cancelled_booking_count'] == 0 + assert len(resp.json['cancelled_events']) == 0 params['user_external_id'] = 'user_id_3' resp = app.post_json(fillslots_url, params=params) @@ -81,12 +85,16 @@ def test_api_events_fillslots(app, user): == Booking.objects.filter(event=event).latest('pk').pk ) assert Booking.objects.filter(in_waiting_list=True, event=event).count() == 1 + assert resp.json['cancelled_booking_count'] == 0 + assert len(resp.json['cancelled_events']) == 0 # change bookings params = {'user_external_id': 'user_id', 'slots': 'event-2,event-3'} resp = app.post_json(fillslots_url, params=params) assert resp.json['booking_count'] == 1 assert resp.json['cancelled_booking_count'] == 1 + assert len(resp.json['cancelled_events']) == 1 + assert [x['date'] for x in resp.json['cancelled_events']] == ['2021-09-07'] user_bookings = Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True) assert {b.event.slug for b in user_bookings} == {'event-2', 'event-3'} @@ -181,7 +189,6 @@ def test_api_events_fillslots_with_cancelled(app, user): resp = app.post_json(fillslots_url, params=params) assert resp.json['booking_count'] == 2 assert len(resp.json['booked_events']) == 2 - assert resp.json['cancelled_booking_count'] == 0 assert resp.json['booked_events'][0]['id'] == 'event-1' assert ( resp.json['booked_events'][0]['booking']['id'] @@ -194,6 +201,8 @@ def test_api_events_fillslots_with_cancelled(app, user): ) assert Booking.objects.filter(user_external_id='user_id').count() == 3 assert Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True).count() == 3 + assert resp.json['cancelled_booking_count'] == 0 + assert len(resp.json['cancelled_events']) == 0 assert Booking.objects.filter(pk=booking_1.pk).exists() is False # cancelled booking deleted booking_2.refresh_from_db() diff --git a/tests/api/fillslot/test_recurring_events.py b/tests/api/fillslot/test_recurring_events.py index dd67f3f7..ab52b4ed 100644 --- a/tests/api/fillslot/test_recurring_events.py +++ b/tests/api/fillslot/test_recurring_events.py @@ -1311,8 +1311,23 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user): }, ) assert resp.json['booking_count'] == 180 + assert resp.json['cancelled_booking_count'] == 0 assert len(ctx.captured_queries) == 15 + with CaptureQueriesContext(connection) as ctx: + resp = app.post_json( + '/api/agendas/recurring-events/fillslots/?action=update&agendas=%s' % agenda_slugs, + params={ + 'slots': events_to_book[1:], + 'user_external_id': 'user', + 'include_booked_events_detail': True, + 'check_overlaps': agenda_slugs, + }, + ) + assert resp.json['booking_count'] == 0 + assert resp.json['cancelled_booking_count'] == 5 + assert len(ctx.captured_queries) == 17 + 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='xxx', first_name='James', last_name='Doe') @@ -1521,16 +1536,36 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user): resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params) assert resp.json['booking_count'] == 5 assert resp.json['cancelled_booking_count'] == 14 + assert 'booked_events' not in resp.json + assert 'cancelled_events' not in resp.json # booking overlapping events is allowed if one has no duration params = { 'user_external_id': 'user_id', 'check_overlaps': 'first-agenda,second-agenda', 'slots': 'second-agenda@event-12-18:5,second-agenda@no-duration:5', + 'include_booked_events_detail': True, } resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params) assert resp.json['booking_count'] == 8 + assert [x['date'] for x in resp.json['booked_events']] == [ + '2021-09-11', + '2021-09-11', + '2021-09-18', + '2021-09-18', + '2021-09-25', + '2021-09-25', + '2021-10-02', + '2021-10-02', + ] assert resp.json['cancelled_booking_count'] == 5 + assert [x['date'] for x in resp.json['cancelled_events']] == [ + '2021-09-07', + '2021-09-14', + '2021-09-21', + '2021-09-28', + '2021-10-05', + ] # booking overlapping events with durations is forbidden params = {