api: fillslot & tstzrange_constraint, retry (#67053)
This commit is contained in:
parent
b17a2ee543
commit
7ccc5cd1c0
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue