api: fillslot & tstzrange_constraint, retry (#67053)

This commit is contained in:
Lauréline Guérin 2022-07-21 11:30:06 +02:00
parent b17a2ee543
commit 7ccc5cd1c0
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
2 changed files with 74 additions and 20 deletions

View File

@ -1320,7 +1320,7 @@ class Fillslots(APIView):
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
def fillslot(self, request, agenda_identifier=None, slots=None, format=None):
def fillslot(self, request, agenda_identifier=None, slots=None, format=None, retry=False):
slots = slots or []
multiple_booking = bool(not slots)
try:
@ -1550,25 +1550,41 @@ class Fillslots(APIView):
if (event.booked_places + places_count) > event.places:
raise APIError(N_('sold out'))
with transaction.atomic():
if to_cancel_booking:
cancelled_booking_id = to_cancel_booking.pk
to_cancel_booking.cancel()
try:
with transaction.atomic():
if to_cancel_booking:
cancelled_booking_id = to_cancel_booking.pk
to_cancel_booking.cancel()
# now we have a list of events, book them.
primary_booking = None
for event in events:
if agenda.accept_meetings():
event.save()
if resources:
event.resources.add(*resources)
for dummy in range(places_count):
new_booking = make_booking(
event, payload, extra_data, primary_booking, in_waiting_list, color
)
new_booking.save()
if primary_booking is None:
primary_booking = new_booking
# now we have a list of events, book them.
primary_booking = None
for event in events:
if agenda.accept_meetings():
event.save()
if resources:
event.resources.add(*resources)
for dummy in range(places_count):
new_booking = make_booking(
event, payload, extra_data, primary_booking, in_waiting_list, color
)
new_booking.save()
if primary_booking is None:
primary_booking = new_booking
except IntegrityError as e:
if 'tstzrange_constraint' in str(e):
# "optimistic concurrency control", between our availability
# check with get_all_slots() and now, new event can have been
# created and conflict with the events we want to create, and
# so we get an IntegrityError exception. In this case we
# restart the fillslot() from the begginning to redo the
# availability check and return a proper error to the client.
#
# To prevent looping, we raise an APIError during the second run
# of fillslot().
if retry:
raise APIError(N_('no more desk available'))
return self.fillslot(request, agenda_identifier=agenda_identifier, slots=slots, retry=True)
raise
response = {
'err': 0,

View File

@ -3,7 +3,7 @@ import urllib.parse as urlparse
from unittest import mock
import pytest
from django.db import connection
from django.db import IntegrityError, connection
from django.test.utils import CaptureQueriesContext
from django.utils.timezone import localtime, now
@ -172,6 +172,44 @@ def test_booking_api(app, user):
resp = app.post('/api/agenda/0/fillslot/%s/' % event.id, status=404)
def test_booking_api_event_exclusion_constraint(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
desk = Desk.objects.create(agenda=agenda, slug='desk')
tomorrow = now() + datetime.timedelta(days=1)
TimePeriod.objects.create(
weekday=tomorrow.date().weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
event_id = resp.json['data'][2]['id']
app.authorization = ('Basic', ('john.doe', 'password'))
original_save = Event.save
global first_call # pylint: disable=global-variable-undefined
first_call = True
def wrapped(*args, **kwargs):
global first_call # pylint: disable=global-variable-undefined
if first_call is True:
first_call = False
raise IntegrityError('tstzrange_constraint')
return original_save(*args, **kwargs)
with mock.patch('chrono.agendas.models.Event.save', autospec=True) as mock_create:
mock_create.side_effect = wrapped
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_id),
)
assert len(mock_create.call_args_list) == 2
assert resp.json['err'] == 0
assert Event.objects.count() == 1
assert Booking.objects.count() == 1
def test_booking_api_extra_emails(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(