api: add endpoint to fill a list of slots (#16238)

This commit is contained in:
Thomas NOËL 2018-04-04 19:54:34 +02:00
parent 642cda9b49
commit ae54f6960f
3 changed files with 470 additions and 76 deletions

View File

@ -25,6 +25,8 @@ urlpatterns = [
url(r'agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_pk>[\w:-]+)/$',
views.fillslot, name='api-fillslot'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$',
views.fillslots, name='api-agenda-fillslots'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_pk>\w+)/$', views.slot_status,
name='api-event-status'),

View File

@ -113,6 +113,9 @@ def get_agenda_detail(request, agenda):
reverse('api-agenda-desks',
kwargs={'agenda_identifier': agenda.slug}))
}
agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
reverse('api-agenda-fillslots',
kwargs={'agenda_identifier': agenda.slug}))
return agenda_detail
@ -300,16 +303,33 @@ agenda_desk_list = AgendaDeskList.as_view()
class SlotSerializer(serializers.Serializer):
label = serializers.CharField(max_length=150, allow_blank=True, required=False)
user_name = serializers.CharField(max_length=250, allow_blank=True, required=False)
backoffice_url = serializers.URLField(allow_blank=True, required=False)
count = serializers.IntegerField(min_value=1, required=False)
'''
payload to fill one slot. The slot (event id) is in the URL.
'''
label = serializers.CharField(max_length=150, allow_blank=True) #, required=False)
user_name = serializers.CharField(max_length=250, allow_blank=True) #, required=False)
backoffice_url = serializers.URLField(allow_blank=True) # , required=False)
count = serializers.IntegerField(min_value=1) # , required=False)
class Fillslot(APIView):
class SlotsSerializer(SlotSerializer):
'''
payload to fill multiple slots: same as SlotSerializer, but the
slots list is in the payload.
'''
slots = serializers.ListField(required=True,
child=serializers.CharField(max_length=64, allow_blank=False))
class Fillslots(APIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = SlotsSerializer
def post(self, request, agenda_identifier=None, event_pk=None, format=None):
return self.fillslot(request=request, agenda_identifier=agenda_identifier,
format=format)
def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
try:
agenda = Agenda.objects.get(slug=agenda_identifier)
except Agenda.DoesNotExist:
@ -319,102 +339,134 @@ class Fillslot(APIView):
except (ValueError, Agenda.DoesNotExist):
raise Http404()
serializer = SlotSerializer(data=request.data)
serializer = self.serializer_class(data=request.data, partial=True)
if not serializer.is_valid():
return Response({
'err': 1,
'reason': 'invalid payload',
'errors': serializer.errors
}, status=status.HTTP_400_BAD_REQUEST)
label = serializer.validated_data.get('label') or ''
user_name = serializer.validated_data.get('user_name') or ''
backoffice_url = serializer.validated_data.get('backoffice_url') or ''
places_count = serializer.validated_data.get('count') or 1
payload = serializer.validated_data
if 'slots' in payload:
slots = payload['slots']
if not slots:
return Response({
'err': 1,
'reason': 'slots list cannot be empty',
}, status=status.HTTP_400_BAD_REQUEST)
if 'count' in payload:
places_count = payload['count']
elif 'count' in request.query_params:
# legacy: count in the query string
try:
places_count = int(request.query_params['count'])
except ValueError:
return Response({
'err': 1,
'reason': 'invalid value for count (%s)' % request.query_params['count'],
}, status=status.HTTP_400_BAD_REQUEST)
else:
places_count = 1
extra_data = {}
for k, v in request.data.items():
if k not in serializer.validated_data:
extra_data[k] = v
if 'count' in request.GET:
try:
places_count = int(request.GET['count'])
except ValueError:
return Response({
'err': 1,
'reason': 'invalid value for count (%s)' % request.GET['count'],
}, status=status.HTTP_400_BAD_REQUEST)
available_desk = None
if agenda.kind == 'meetings':
# event_pk is actually a timeslot id (meeting_type:start_datetime);
# split it back to get both parts.
meeting_type_id, start_datetime_str = event_pk.split(':')
start_datetime = make_aware(datetime.datetime.strptime(
start_datetime_str, '%Y-%m-%d-%H%M'))
# slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
# split them back to get both parts
meeting_type_id = slots[0].split(':')[0]
datetimes = set()
for slot in slots:
meeting_type_id_, datetime_str = slot.split(':')
if meeting_type_id_ != meeting_type_id:
return Response({
'err': 1,
'reason': 'all slots must have the same meeting type id (%s)' % meeting_type_id
}, status=status.HTTP_400_BAD_REQUEST)
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
# sort available matching slots by desk id
slots = [slot for slot in slots if not slot.full and slot.start_datetime == start_datetime]
slots.sort(key=lambda x: x.desk.id)
if slots:
# book first available desk
available_desk = slots[0].desk
# get all free slots and separate them by desk
all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
all_slots = [slot for slot in all_slots if not slot.full]
datetimes_by_desk = defaultdict(set)
for slot in all_slots:
datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
if not available_desk:
# search first desk where all requested slots are free
for available_desk_id in sorted(datetimes_by_desk.keys()):
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
available_desk = Desk.objects.get(id=available_desk_id)
break
else:
return Response({'err': 1, 'reason': 'no more desk available'})
# booking requires a real Event object (not a lazy Timeslot);
# create it now, with data from the timeslot and the desk we
# found.
event = Event.objects.create(agenda=agenda,
meeting_type_id=meeting_type_id,
start_datetime=start_datetime,
full=False, places=1,
desk=available_desk)
# all datetimes are free, book them in order
datetimes = list(datetimes)
datetimes.sort()
event_pk = event.id
# booking requires real Event objects (not lazy Timeslots);
# create them now, with data from the slots and the desk we found.
events = []
for start_datetime in datetimes:
events.append(Event.objects.create(agenda=agenda,
meeting_type_id=meeting_type_id,
start_datetime=start_datetime,
full=False, places=1,
desk=available_desk))
else:
events = Event.objects.filter(id__in=slots).order_by('start_datetime')
event = Event.objects.filter(id=event_pk)[0]
new_booking = Booking(event_id=event_pk, extra_data=extra_data,
label=label, user_name=user_name, backoffice_url=backoffice_url)
if event.waiting_list_places:
if (event.booked_places + places_count) > event.places or event.waiting_list:
# if this is full or there are people waiting, put new bookings
# in the waiting list.
new_booking.in_waiting_list = True
if (event.waiting_list + places_count) > event.waiting_list_places:
# search free places. Switch to waiting list if necessary.
in_waiting_list = False
for event in events:
if event.waiting_list_places:
if (event.booked_places + places_count) > event.places or event.waiting_list:
# if this is full or there are people waiting, put new bookings
# in the waiting list.
in_waiting_list = True
if (event.waiting_list + places_count) > event.waiting_list_places:
return Response({'err': 1, 'reason': 'sold out'})
else:
if (event.booked_places + places_count) > event.places:
return Response({'err': 1, 'reason': 'sold out'})
else:
if (event.booked_places + places_count) > event.places:
return Response({'err': 1, 'reason': 'sold out'})
new_booking.save()
for i in range(places_count-1):
additional_booking = Booking(event_id=event_pk, extra_data=extra_data,
label=label, user_name=user_name,
backoffice_url=backoffice_url)
additional_booking.in_waiting_list = new_booking.in_waiting_list
additional_booking.primary_booking = new_booking
additional_booking.save()
# now we have a list of events, book them.
primary_booking = None
for event in events:
for i in range(places_count):
new_booking = Booking(event_id=event.id,
in_waiting_list=in_waiting_list,
label=payload.get('label', ''),
user_name=payload.get('user_name', ''),
backoffice_url=payload.get('backoffice_url', ''),
extra_data=extra_data)
if primary_booking is not None:
new_booking.primary_booking = primary_booking
new_booking.save()
if primary_booking is None:
primary_booking = new_booking
response = {
'err': 0,
'in_waiting_list': new_booking.in_waiting_list,
'booking_id': new_booking.id,
'datetime': localtime(event.start_datetime),
'in_waiting_list': in_waiting_list,
'booking_id': primary_booking.id,
'datetime': localtime(events[0].start_datetime),
'api': {
'cancel_url': request.build_absolute_uri(
reverse('api-cancel-booking', kwargs={'booking_pk': new_booking.id}))
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id}))
}
}
if new_booking.in_waiting_list:
if in_waiting_list:
response['api']['accept_url'] = request.build_absolute_uri(
reverse('api-accept-booking', kwargs={'booking_pk': new_booking.id}))
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id}))
if agenda.kind == 'meetings':
response['end_datetime'] = localtime(event.end_datetime)
response['end_datetime'] = localtime(events[-1].end_datetime)
if available_desk:
response['desk'] = {
'label': available_desk.label,
@ -422,6 +474,18 @@ class Fillslot(APIView):
return Response(response)
fillslots = Fillslots.as_view()
class Fillslot(Fillslots):
serializer_class = SlotSerializer
def post(self, request, agenda_identifier=None, event_pk=None, format=None):
return self.fillslot(request=request,
agenda_identifier=agenda_identifier,
slots=[event_pk], # fill a "list on one slot"
format=format)
fillslot = Fillslot.as_view()

View File

@ -100,15 +100,18 @@ def test_agendas_api(app, some_data, meetings_agenda):
resp = app.get('/api/agenda/')
assert resp.json == {'data': [
{'text': 'Foo bar', 'id': u'foo-bar', 'slug': 'foo-bar', 'kind': 'events',
'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda1.slug}},
'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda1.slug,
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda1.slug}},
{'text': 'Foo bar Meeting', 'id': u'foo-bar-meeting', 'slug': 'foo-bar-meeting',
'kind': 'meetings',
'api': {'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % meetings_agenda.slug,
'desks_url': 'http://testserver/api/agenda/%s/desks/' % meetings_agenda.slug,
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug,
},
},
{'text': 'Foo bar2', 'id': u'foo-bar2', 'kind': 'events', 'slug': 'foo-bar2',
'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda2.slug}}
'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda2.slug,
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug}}
]}
def test_agendas_meetingtypes_api(app, some_data, meetings_agenda):
@ -290,6 +293,7 @@ def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda,
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == 2
fillslot_url = resp.json['data'][0]['api']['fillslot_url']
two_slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
time_period.end_time = datetime.time(10, 15)
time_period.save()
@ -301,6 +305,11 @@ def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda,
resp = app.post(fillslot_url)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available'
# booking the two slots fails too
fillslots_url = '/api/agenda/%s/fillslots/' % meeting_type.agenda.slug
resp = app.post(fillslots_url, params={'slots': two_slots})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available'
def test_booking_api(app, some_data, user):
agenda = Agenda.objects.filter(label=u'Foo bar')[0]
@ -309,9 +318,10 @@ def test_booking_api(app, some_data, user):
# unauthenticated
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id), status=403)
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api']['fillslot_url']
assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id)
for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy)
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api']['fillslot_url']
assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id))
@ -360,6 +370,102 @@ def test_booking_api(app, some_data, user):
resp = app.post('/api/agenda/233/fillslot/%s/' % event.id, status=404)
def test_booking_api_fillslots(app, some_data, user):
agenda = Agenda.objects.filter(label=u'Foo bar')[0]
events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
assert len(events_ids) == 3
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] # first event
# unauthenticated
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, status=403)
for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy)
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
api_event_ids = [x['id'] for x in resp_datetimes.json['data']]
assert api_event_ids == events_ids
assert Booking.objects.count() == 0
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_ids})
primary_booking_id = resp.json['booking_id']
Booking.objects.get(id=primary_booking_id)
assert resp.json['datetime'] == localtime(event.start_datetime).isoformat()
assert 'accept_url' not in resp.json['api']
assert 'cancel_url' in resp.json['api']
assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
assert Booking.objects.count() == 3
# these 3 bookings are related, the first is the primary one
bookings = Booking.objects.all().order_by('primary_booking')
assert bookings[0].primary_booking is None
assert bookings[1].primary_booking.id == bookings[0].id == primary_booking_id
assert bookings[2].primary_booking.id == bookings[0].id == primary_booking_id
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_ids})
primary_booking_id_2 = resp.json['booking_id']
assert Booking.objects.count() == 6
assert Booking.objects.filter(event__agenda=agenda).count() == 6
# 6 = 2 primary + 2*2 secondary
assert Booking.objects.filter(event__agenda=agenda, primary_booking__isnull=True).count() == 2
assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id).count() == 2
assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id_2).count() == 2
# test with additional data
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids,
'label': 'foo', 'user_name': 'bar', 'backoffice_url': 'http://example.net/'})
booking_id = resp.json['booking_id']
assert Booking.objects.get(id=booking_id).label == 'foo'
assert Booking.objects.get(id=booking_id).user_name == 'bar'
assert Booking.objects.get(id=booking_id).backoffice_url == 'http://example.net/'
assert Booking.objects.filter(primary_booking=booking_id, label='foo').count() == 2
# cancel
cancel_url = resp.json['api']['cancel_url']
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
assert Booking.objects.get(id=booking_id).cancellation_datetime is None
resp_cancel = app.post(cancel_url)
assert resp_cancel.json['err'] == 0
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 3
assert Booking.objects.get(id=booking_id).cancellation_datetime is not None
# extra data stored in extra_data field
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids,
'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'})
assert Booking.objects.get(id=resp.json['booking_id']).label == 'l'
assert Booking.objects.get(id=resp.json['booking_id']).user_name == 'u'
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == ''
assert Booking.objects.get(id=resp.json['booking_id']).extra_data == {'foo': 'bar'}
for booking in Booking.objects.filter(primary_booking=resp.json['booking_id']):
assert booking.extra_data == {'foo': 'bar'}
# test invalid data are refused
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids,
'user_name': {'foo': 'bar'}}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid payload'
assert len(resp.json['errors']) == 1
assert 'user_name' in resp.json['errors']
# empty or missing slots
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': []}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'slots list cannot be empty'
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'slots list cannot be empty'
# invalid slots format
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': 'foobar'}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid payload'
assert len(resp.json['errors']) == 1
assert 'slots' in resp.json['errors']
# unknown agendas
resp = app.post('/api/agenda/foobar/fillslots/', status=404)
resp = app.post('/api/agenda/233/fillslots/', status=404)
def test_booking_api_meeting(app, meetings_agenda, user):
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
@ -390,6 +496,58 @@ def test_booking_api_meeting(app, meetings_agenda, user):
assert resp.json['err'] == 0
assert Booking.objects.count() == 2
def test_booking_api_meeting_fillslots(app, meetings_agenda, user):
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
app.authorization = ('Basic', ('john.doe', 'password'))
resp_booking = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
assert Booking.objects.count() == 2
primary_booking = Booking.objects.filter(primary_booking__isnull=True).first()
secondary_booking = Booking.objects.filter(primary_booking=primary_booking.id).first()
assert resp_booking.json['datetime'][:16] == localtime(primary_booking.event.start_datetime
).isoformat()[:16]
assert resp_booking.json['end_datetime'][:16] == localtime(secondary_booking.event.end_datetime
).isoformat()[:16]
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 2
# try booking the same timeslots
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
assert resp2.json['err'] == 1
assert resp2.json['reason'] == 'no more desk available'
# try booking partially free timeslots (one free, one busy)
nonfree_slots = [resp.json['data'][0]['id'], resp.json['data'][2]['id']]
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': nonfree_slots})
assert resp2.json['err'] == 1
assert resp2.json['reason'] == 'no more desk available'
# booking other free timeslots
free_slots = [resp.json['data'][3]['id'], resp.json['data'][2]['id']]
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': free_slots})
assert resp2.json['err'] == 0
cancel_url = resp2.json['api']['cancel_url']
assert Booking.objects.count() == 4
# 4 = 2 primary + 2 secondary
assert Booking.objects.filter(primary_booking__isnull=True).count() == 2
assert Booking.objects.filter(primary_booking__isnull=False).count() == 2
# cancel
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
resp_cancel = app.post(cancel_url)
assert resp_cancel.json['err'] == 0
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 2
impossible_slots = ['1:2017-05-22-1130', '2:2017-05-22-1100']
resp = app.post('/api/agenda/%s/fillslots/' % agenda_id,
params={'slots': impossible_slots},
status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'all slots must have the same meeting type id (1)'
def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user):
meetings_agenda.maximal_booking_delay = 365
meetings_agenda.save()
@ -746,6 +904,107 @@ def test_multiple_booking_api(app, some_data, user):
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).waiting_list == 2
def test_multiple_booking_api_fillslots(app, some_data, user):
agenda = Agenda.objects.filter(label=u'Foo bar')[0]
# get slots of first 2 events
events = [x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period()][:2]
events_ids = [x.id for x in events]
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_ids]
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslots/?count=NaN' % agenda.slug, params={'slots': slots}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == "invalid value for count (NaN)"
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': 'NaN'}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == "invalid payload"
assert 'count' in resp.json['errors']
# get 3 places on 2 slots
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': '3'})
# one booking with 5 children
booking = Booking.objects.get(id=resp.json['booking_id'])
cancel_url = resp.json['api']['cancel_url']
assert Booking.objects.filter(primary_booking=booking).count() == 5
assert resp.json['datetime'] == localtime(events[0].start_datetime).isoformat()
assert 'accept_url' not in resp.json['api']
assert 'cancel_url' in resp.json['api']
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': 2})
for event in events:
assert Event.objects.get(id=event.id).booked_places == 5
resp = app.post(cancel_url)
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
# check available places overflow
# NB: limit only the first event !
events[0].places = 3
events[0].waiting_list_places = 8
events[0].save()
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': 5})
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).waiting_list == 5
accept_url = resp.json['api']['accept_url']
return
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': 5})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out'
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).waiting_list == 5
# accept the waiting list
resp = app.post(accept_url)
for event in events:
assert Event.objects.get(id=event.id).booked_places == 7
assert Event.objects.get(id=event.id).waiting_list == 0
# check with a short waiting list
Booking.objects.all().delete()
# NB: limit only the first event !
events[0].places = 4
events[0].waiting_list_places = 2
events[0].save()
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': 5})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out'
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': 3})
assert resp.json['err'] == 0
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).waiting_list == 0
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': 3})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out'
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
params={'slots': slots, 'count': '2'})
assert resp.json['err'] == 0
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).waiting_list == 2
def test_agenda_detail_api(app, some_data):
agenda = Agenda.objects.get(slug='foo-bar')
resp = app.get('/api/agenda/%s/' % agenda.slug)
@ -898,6 +1157,75 @@ def test_agenda_meeting_api_multiple_desk(app, meetings_agenda, user):
app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert queries_count_datetime1 == len(ctx.captured_queries)
def test_agenda_meeting_api_fillslots_multiple_desks(app, meetings_agenda, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
# add a second desk, same timeperiods
time_period = meetings_agenda.desk_set.first().timeperiod_set.first()
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda)
TimePeriod.objects.create(
start_time=time_period.start_time, end_time=time_period.end_time,
weekday=time_period.weekday, desk=desk2)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
slots = [x['id'] for x in resp.json['data'][:3]]
def get_free_places():
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
return len([x for x in resp.json['data'] if not x['disabled']])
start_free_places = get_free_places()
# booking 3 slots on desk 1
fillslots_url = '/api/agenda/%s/fillslots/' % agenda_id
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 0
desk1 = resp.json['desk']['slug']
cancel_url = resp.json['api']['cancel_url']
assert get_free_places() == start_free_places
# booking same slots again, will be on desk 2
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 0
assert resp.json['desk']['slug'] != desk2
# 3 places are disabled in datetimes list
assert get_free_places() == start_free_places - len(slots)
# try booking again: no desk available
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available'
assert get_free_places() == start_free_places - len(slots)
# cancel desk 1 booking
resp = app.post(cancel_url)
assert resp.json['err'] == 0
# all places are free again
assert get_free_places() == start_free_places
# booking a single slot (must be on desk 1)
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, slots[1]))
assert resp.json['err'] == 0
assert resp.json['desk']['slug'] == desk1
cancel_url = resp.json['api']['cancel_url']
assert get_free_places() == start_free_places - 1
# try booking the 3 slots again: no desk available, one slot is not fully available
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available'
# cancel last signel slot booking, desk1 will be free
resp = app.post(cancel_url)
assert resp.json['err'] == 0
assert get_free_places() == start_free_places
# booking again is ok, on desk 1
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 0
assert resp.json['desk']['slug'] == desk1
assert get_free_places() == start_free_places - len(slots)
def test_agenda_meeting_same_day(app, meetings_agenda, mock_now, user):
app.authorization = ('Basic', ('john.doe', 'password'))