api: add endpoint to book multiple events accross several agendas (#56104)

This commit is contained in:
Valentin Deniaud 2021-08-10 15:38:23 +02:00
parent 0cf2c62199
commit 67f1f68fd6
3 changed files with 140 additions and 5 deletions

View File

@ -21,6 +21,11 @@ from . import views
urlpatterns = [
url(r'^agenda/$', views.agendas),
url(r'^agendas/datetimes/$', views.agendas_datetimes, name='api-agendas-datetimes'),
url(
r'^agendas/events/fillslots/$',
views.agendas_events_fillslots,
name='api-agendas-events-fillslots',
),
url(r'^agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda_detail),
url(r'^agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
url(

View File

@ -1605,7 +1605,10 @@ class EventsFillslots(APIView):
serializer_class = serializers.EventsSlotsSerializer
def post(self, request, agenda_identifier):
agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
self.agenda = get_object_or_404(Agenda, slug=agenda_identifier, kind='events')
return self.fillslots(request)
def fillslots(self, request):
start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
serializer = self.serializer_class(data=request.data, partial=True)
@ -1619,10 +1622,11 @@ class EventsFillslots(APIView):
payload = serializer.validated_data
user_external_id = payload['user_external_id']
events = get_events_from_slots(payload['slots'], request, agenda, payload)
events = self.get_events(request, payload)
events = events.select_related('agenda')
events = Event.annotate_queryset(events)
already_booked_events = agenda.event_set.filter(booking__user_external_id=user_external_id)
already_booked_events = self.get_already_booked_events(user_external_id)
if start_datetime:
already_booked_events = already_booked_events.filter(start_datetime__gte=start_datetime)
if end_datetime:
@ -1649,7 +1653,7 @@ class EventsFillslots(APIView):
bookings = [make_booking(event, payload, extra_data) for event in events]
events_to_update = Event.annotate_queryset(
agenda.event_set.filter(pk__in=events_to_unbook + [event.pk for event in events])
Event.objects.filter(pk__in=events_to_unbook + [event.pk for event in events])
)
with transaction.atomic():
deleted_count = Booking.objects.filter(
@ -1665,15 +1669,66 @@ class EventsFillslots(APIView):
response = {
'err': 0,
'booking_count': len(bookings),
'waiting_list_events': [get_event_detail(request, x, agenda=agenda) for x in waiting_list_events],
'waiting_list_events': [get_event_detail(request, x) for x in waiting_list_events],
'cancelled_booking_count': deleted_count,
}
return Response(response)
def get_events(self, request, payload):
return get_events_from_slots(payload['slots'], request, self.agenda, payload)
def get_already_booked_events(self, user_external_id):
return self.agenda.event_set.filter(booking__user_external_id=user_external_id)
events_fillslots = EventsFillslots.as_view()
class MultipleAgendasEventsFillslots(EventsFillslots):
def post(self, request):
self.agendas = None
if 'agendas' not in request.GET:
raise APIError(
_('Missing agendas list in querystring'),
err_class='Missing agendas list in querystring',
http_status=status.HTTP_400_BAD_REQUEST,
)
self.agenda_slugs = [s for s in request.GET.get('agendas', '').split(',') if s]
self.agendas = get_objects_from_slugs(self.agenda_slugs, qs=Agenda.objects.filter(kind='events'))
return self.fillslots(request)
def get_events(self, request, payload):
events_by_agenda = collections.defaultdict(list)
for slot in payload['slots']:
agenda, event = slot.split('@')
events_by_agenda[agenda].append(event)
agendas = get_objects_from_slugs(events_by_agenda.keys(), qs=Agenda.objects.filter(kind='events'))
agendas_by_slug = {agenda.slug: agenda for agenda in agendas}
if not set(agendas_by_slug).issubset(self.agenda_slugs):
extra_agendas = set(agendas_by_slug) - set(self.agenda_slugs)
extra_agendas = ','.join(extra_agendas)
raise APIError(
_('Some events belong to agendas that are not present in querystring: %s' % extra_agendas),
err_class='Some events belong to agendas that are not present in querystring: %s'
% extra_agendas,
http_status=status.HTTP_400_BAD_REQUEST,
)
events = Event.objects.none()
for agenda_slug, event_slugs in events_by_agenda.items():
events |= get_events_from_slots(event_slugs, request, agendas_by_slug[agenda_slug], payload)
return events
def get_already_booked_events(self, user_external_id):
return Event.objects.filter(agenda__in=self.agendas, booking__user_external_id=user_external_id)
agendas_events_fillslots = MultipleAgendasEventsFillslots.as_view()
class BookingFilter(filters.FilterSet):
agenda = filters.CharFilter(field_name='event__agenda__slug', lookup_expr='exact')
category = filters.CharFilter(field_name='event__agenda__category__slug', lookup_expr='exact')

View File

@ -2432,3 +2432,78 @@ def test_api_events_fillslots_past_event(app, user):
params['events'] = 'all'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
assert resp.json['err'] == 0
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_multiple_agendas(app, user):
first_agenda = Agenda.objects.create(label='First agenda', kind='events')
Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
first_event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=5),
places=2,
agenda=first_agenda,
)
second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
second_event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=6),
places=2,
agenda=second_agenda,
)
agenda_slugs = '%s,%s' % (first_agenda.slug, second_agenda.slug)
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
event_slugs = ','.join((resp.json['data'][0]['id'], resp.json['data'][1]['id']))
assert event_slugs == 'first-agenda@event,second-agenda@event'
app.authorization = ('Basic', ('john.doe', 'password'))
params = {'user_external_id': 'user_id', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['booking_count'] == 2
assert first_event.booking_set.count() == 1
assert second_event.booking_set.count() == 1
# booking modification
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@event'}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 1
assert first_event.booking_set.count() == 1
assert second_event.booking_set.count() == 0
params = {'user_external_id': 'user_id_2', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['booking_count'] == 2
assert first_event.booking_set.count() == 2
assert second_event.booking_set.count() == 1
params = {'user_external_id': 'user_id_3', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'some events are full: Event'
# invalid agenda slugs
resp = app.post_json(
'/api/agendas/events/fillslots/?agendas=first-agenda,xxx,yyy', params=params, status=400
)
assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
params = {'user_external_id': 'user_id_3', 'slots': 'first-agenda@event,xxx@event,yyy@event'}
resp = app.post_json(
'/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params, status=400
)
assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
# missing agendas parameter
resp = app.post_json('/api/agendas/events/fillslots/', params=params, status=400)
assert resp.json['err_desc'] == 'Missing agendas list in querystring'
# 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['err_desc']
== 'Some events belong to agendas that are not present in querystring: second-agenda'
)