api: add endpoint to fill a list of slots (#16238)
This commit is contained in:
parent
642cda9b49
commit
ae54f6960f
|
@ -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'),
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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'))
|
||||
|
|
Loading…
Reference in New Issue