From 8f413b28eb0a8b2746dec6d5e0f39c6498a65bd3 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 5 Jul 2021 17:29:16 +0200 Subject: [PATCH] api: allow changing recurrence bookings (#54746) --- chrono/api/views.py | 23 ++++++++++--- tests/api/test_fillslot.py | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/chrono/api/views.py b/chrono/api/views.py index 99dbcb09..f6dcb95d 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -1571,6 +1571,7 @@ class RecurringFillslots(APIView): http_status=status.HTTP_400_BAD_REQUEST, ) payload = serializer.validated_data + user_external_id = payload['user_external_id'] open_event_slugs = set(agenda.get_open_recurring_events().values_list('slug', flat=True)) slots = collections.defaultdict(list) @@ -1600,12 +1601,19 @@ class RecurringFillslots(APIView): events_to_book = Event.objects.filter(event_filter) events_to_book = events_to_book.filter(start_datetime__gte=start_datetime, cancelled=False) + if end_datetime: + events_to_book = events_to_book.filter(start_datetime__lte=end_datetime) + + events_to_unbook = list( + agenda.event_set.filter(booking__user_external_id=user_external_id, primary_event__isnull=False) + .exclude(pk__in=events_to_book) + .values_list('pk', flat=True) + ) + events_to_book = events_to_book.exclude(booking__user_external_id=user_external_id) full_events = list(events_to_book.filter(full=True)) events_to_book = events_to_book.filter(full=False) - if end_datetime: - events_to_book = events_to_book.filter(start_datetime__lte=end_datetime) - if not events_to_book.exists(): + if not events_to_book.exists() and not events_to_unbook: if full_events: raise APIError(_('all events are all full'), err_class='all events are all full') raise APIError(_('no event recurrences to book'), err_class='no event recurrences to book') @@ -1621,9 +1629,15 @@ class RecurringFillslots(APIView): extra_data = {k: v for k, v in request.data.items() if k not in payload} bookings = [make_booking(event, payload, extra_data) for event in events_to_book] + events_to_update = Event.annotate_queryset( + agenda.event_set.filter(pk__in=events_to_unbook + [event.pk for event in events_to_book]) + ) with transaction.atomic(): + deleted_count = Booking.objects.filter( + user_external_id=user_external_id, event__in=events_to_unbook + ).delete()[0] Booking.objects.bulk_create(bookings) - events_to_book.update( + events_to_update.update( full=Q(booked_places_count__gte=F('places'), waiting_list_places=0) | Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')), almost_full=Q(booked_places_count__gte=0.9 * F('places')), @@ -1632,6 +1646,7 @@ class RecurringFillslots(APIView): response = { 'err': 0, 'booking_count': len(bookings), + 'cancelled_booking_count': deleted_count, 'full_events': [get_event_detail(request, x, agenda=agenda) for x in full_events], } return Response(response) diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py index d1d68c3e..bf2ca717 100644 --- a/tests/api/test_fillslot.py +++ b/tests/api/test_fillslot.py @@ -2168,6 +2168,7 @@ def test_recurring_events_api_fillslots(app, user, freezer): assert resp.json['booking_count'] == 155 assert len(resp.json['full_events']) == 1 assert resp.json['full_events'][0]['slug'] == event.slug + resp = app.post_json(fillslots_url, params=params) params['user_external_id'] = 'user_id_4' resp = app.post_json(fillslots_url, params=params) @@ -2179,6 +2180,7 @@ def test_recurring_events_api_fillslots(app, user, freezer): assert resp.json['booking_count'] == 4 assert Booking.objects.filter(user_external_id='user_id_4').count() == 4 + params['user_external_id'] = 'user_id_5' resp = app.post_json(fillslots_url + '?date_start=2020-10-06&date_end=2020-11-06', params=params) assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'no event recurrences to book' @@ -2226,6 +2228,74 @@ def test_recurring_events_api_fillslots_waiting_list(app, user, freezer): assert events.filter(waiting_list_count=2).count() == 5 +def test_recurring_events_api_fillslots_change_bookings(app, user, freezer): + freezer.move_to('2021-09-06 12:00') + agenda = Agenda.objects.create(label='Foo bar', kind='events') + event = Event.objects.create( + label='Event', + start_datetime=now(), + recurrence_days=[0, 1, 3, 4], # Monday, Tuesday, Thursday, Friday + places=1, + waiting_list_places=1, + agenda=agenda, + recurrence_end_date=now() + datetime.timedelta(days=364), + ) + event.create_all_recurrences() + + app.authorization = ('Basic', ('john.doe', 'password')) + fillslots_url = '/api/agenda/%s/recurring-events/fillslots/' % agenda.slug + params = {'user_external_id': 'user_id'} + # Book Monday and Thursday + params['slots'] = 'event:0,event:3' + resp = app.post_json(fillslots_url, params=params) + assert resp.json['booking_count'] == 104 + assert resp.json['cancelled_booking_count'] == 0 + assert Booking.objects.count() == 104 + assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 52 + assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52 + + # Change booking to Monday and Tuesday + params['slots'] = 'event:0,event:1' + resp = app.post_json(fillslots_url, params=params) + assert resp.json['booking_count'] == 52 + assert resp.json['cancelled_booking_count'] == 52 + assert Booking.objects.count() == 104 + assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 52 + assert Booking.objects.filter(event__start_datetime__week_day=3).count() == 52 + + params = {'user_external_id': 'user_id_2'} + params['slots'] = 'event:0,event:3' + resp = app.post_json(fillslots_url, params=params) + assert resp.json['booking_count'] == 104 + assert resp.json['cancelled_booking_count'] == 0 + assert Booking.objects.count() == 208 + assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 104 + assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52 + events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False)) + assert events.filter(booked_places_count=1).count() == 156 + assert events.filter(waiting_list_count=1).count() == 52 + + params['slots'] = 'event:1,event:4' + resp = app.post_json(fillslots_url, params=params) + assert resp.json['booking_count'] == 104 + assert resp.json['cancelled_booking_count'] == 104 + assert Booking.objects.count() == 208 + assert Booking.objects.filter(event__start_datetime__week_day=3).count() == 104 + assert Booking.objects.filter(event__start_datetime__week_day=6).count() == 52 + events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False)) + assert events.filter(booked_places_count=1).count() == 156 + assert events.filter(waiting_list_count=1).count() == 52 + + # only recurring events are impacted + normal_event = Event.objects.create( + start_datetime=now() + datetime.timedelta(days=1), places=2, agenda=agenda + ) + Booking.objects.create(event=normal_event, user_external_id='user_id') + resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event:0'}) + assert resp.json['cancelled_booking_count'] == 52 + assert Booking.objects.filter(user_external_id='user_id', event=normal_event).count() == 1 + + @pytest.mark.freeze_time('2021-09-06 12:00') def test_api_events_fillslots(app, user): agenda = Agenda.objects.create(label='Foo bar', kind='events') -- 2.20.1