Projet

Général

Profil

0001-api-add-endpoint-to-fill-a-list-of-slots-16238.patch

Thomas Noël, 06 avril 2018 14:55

Télécharger (33,5 ko)

Voir les différences:

Subject: [PATCH] api: add endpoint to fill a list of slots (#16238)

 chrono/api/urls.py  |   2 +
 chrono/api/views.py | 210 ++++++++++++++++++++------------
 tests/test_api.py   | 338 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 472 insertions(+), 78 deletions(-)
chrono/api/urls.py
25 25
    url(r'agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
26 26
    url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_pk>[\w:-]+)/$',
27 27
        views.fillslot, name='api-fillslot'),
28
    url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$',
29
        views.fillslots, name='api-agenda-fillslots'),
28 30
    url(r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_pk>\w+)/$', views.slot_status,
29 31
        name='api-event-status'),
30 32

  
chrono/api/views.py
111 111
                reverse('api-agenda-desks',
112 112
                        kwargs={'agenda_identifier': agenda.slug}))
113 113
        }
114
    agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
115
        reverse('api-agenda-fillslots',
116
                kwargs={'agenda_identifier': agenda.slug}))
114 117

  
115 118
    return agenda_detail
116 119

  
......
298 301

  
299 302

  
300 303
class SlotSerializer(serializers.Serializer):
301
    label = serializers.CharField(max_length=150, allow_blank=True, required=False)
302
    user_name = serializers.CharField(max_length=250, allow_blank=True, required=False)
303
    backoffice_url = serializers.URLField(allow_blank=True, required=False)
304
    count = serializers.IntegerField(min_value=1, required=False)
304
    '''
305
    payload to fill one slot. The slot (event id) is in the URL.
306
    '''
307
    label = serializers.CharField(max_length=150, allow_blank=True) #, required=False)
308
    user_name = serializers.CharField(max_length=250, allow_blank=True) #, required=False)
309
    backoffice_url = serializers.URLField(allow_blank=True) # , required=False)
310
    count = serializers.IntegerField(min_value=1) # , required=False)
311

  
305 312

  
313
class SlotsSerializer(SlotSerializer):
314
    '''
315
    payload to fill multiple slots: same as SlotSerializer, but the
316
    slots list is in the payload.
317
    '''
318
    slots = serializers.ListField(required=True,
319
        child=serializers.CharField(max_length=64, allow_blank=False))
306 320

  
307
class Fillslot(APIView):
321

  
322
class Fillslots(APIView):
308 323
    permission_classes = (permissions.IsAuthenticated,)
324
    serializer_class = SlotsSerializer
309 325

  
310 326
    def post(self, request, agenda_identifier=None, event_pk=None, format=None):
327
        return self.fillslot(request=request, agenda_identifier=agenda_identifier,
328
                             format=format)
329

  
330
    def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
311 331
        try:
312 332
            agenda = Agenda.objects.get(slug=agenda_identifier)
313 333
        except Agenda.DoesNotExist:
......
317 337
            except (ValueError, Agenda.DoesNotExist):
318 338
                raise Http404()
319 339

  
320
        serializer = SlotSerializer(data=request.data)
340
        serializer = self.serializer_class(data=request.data, partial=True)
321 341
        if not serializer.is_valid():
322 342
            return Response({
323 343
                'err': 1,
324 344
                'reason': 'invalid payload',
325 345
                'errors': serializer.errors
326 346
            }, status=status.HTTP_400_BAD_REQUEST)
327
        label = serializer.validated_data.get('label') or ''
328
        user_name = serializer.validated_data.get('user_name') or ''
329
        backoffice_url = serializer.validated_data.get('backoffice_url') or ''
330
        places_count = serializer.validated_data.get('count') or 1
331
        extra_data = {}
332
        for k, v in request.data.items():
333
            if k not in serializer.validated_data:
334
                extra_data[k] = v
347
        payload = serializer.validated_data
335 348

  
336
        if 'count' in request.GET:
349
        if 'slots' in payload:
350
            slots = payload['slots']
351
        if not slots:
352
            return Response({
353
                'err': 1,
354
                'reason': 'slots list cannot be empty',
355
            }, status=status.HTTP_400_BAD_REQUEST)
356

  
357
        if 'count' in payload:
358
            places_count = payload['count']
359
        elif 'count' in request.query_params:
360
            # legacy: count in the query string
337 361
            try:
338
                places_count = int(request.GET['count'])
362
                places_count = int(request.query_params['count'])
339 363
            except ValueError:
340 364
                return Response({
341 365
                    'err': 1,
342
                    'reason': 'invalid value for count (%r)' % request.GET['count'],
366
                    'reason': 'invalid value for count (%r)' % request.query_params['count'],
343 367
                }, status=status.HTTP_400_BAD_REQUEST)
368
        else:
369
            places_count = 1
370

  
371
        extra_data = {}
372
        for k, v in request.data.items():
373
            if k not in serializer.validated_data:
374
                extra_data[k] = v
344 375

  
345 376
        available_desk = None
377

  
346 378
        if agenda.kind == 'meetings':
347
            # event_pk is actually a timeslot id (meeting_type:start_datetime);
348
            # split it back to get both parts.
349
            meeting_type_id, start_datetime_str = event_pk.split(':')
350
            start_datetime = make_aware(datetime.datetime.strptime(
351
                start_datetime_str, '%Y-%m-%d-%H%M'))
352

  
353
            slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
354
            # sort available matching slots by desk id
355
            slots = [slot for slot in slots if not slot.full and slot.start_datetime == start_datetime]
356
            slots.sort(key=lambda x: x.desk.id)
357
            if slots:
358
                # book first available desk
359
                available_desk = slots[0].desk
360

  
361
            if not available_desk:
379
            # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
380
            # split them back to get both parts
381
            meeting_type_id = slots[0].split(':')[0]
382
            datetimes = set()
383
            for slot in slots:
384
                meeting_type_id_, datetime_str = slot.split(':')
385
                if meeting_type_id_ != meeting_type_id:
386
                    return Response({
387
                        'err': 1,
388
                        'reason': 'all slots must have the same meeting type id (%s)' % meeting_type_id
389
                    }, status=status.HTTP_400_BAD_REQUEST)
390
                datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
391

  
392
            # get all free slots and separate them by desk
393
            all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
394
            all_slots = [slot for slot in all_slots if not slot.full]
395
            datetimes_by_desk = defaultdict(set)
396
            for slot in all_slots:
397
                datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
398

  
399
            # search first desk where all requested slots are free
400
            for available_desk_id in sorted(datetimes_by_desk.keys()):
401
                if datetimes.issubset(datetimes_by_desk[available_desk_id]):
402
                    available_desk = Desk.objects.get(id=available_desk_id)
403
                    break
404
            else:
362 405
                return Response({'err': 1, 'reason': 'no more desk available'})
363 406

  
364
            # booking requires a real Event object (not a lazy Timeslot);
365
            # create it now, with data from the timeslot and the desk we
366
            # found.
367
            event = Event.objects.create(agenda=agenda,
368
                    meeting_type_id=meeting_type_id,
369
                    start_datetime=start_datetime,
370
                    full=False, places=1,
371
                    desk=available_desk)
372

  
373
            event_pk = event.id
374

  
375
        event = Event.objects.filter(id=event_pk)[0]
376
        new_booking = Booking(event_id=event_pk, extra_data=extra_data,
377
                              label=label, user_name=user_name, backoffice_url=backoffice_url)
378

  
379
        if event.waiting_list_places:
380
            if (event.booked_places + places_count) > event.places or event.waiting_list:
381
                # if this is full or there are people waiting, put new bookings
382
                # in the waiting list.
383
                new_booking.in_waiting_list = True
384

  
385
                if (event.waiting_list + places_count) > event.waiting_list_places:
407
            # all datetimes are free, book them in order
408
            datetimes = list(datetimes)
409
            datetimes.sort()
410

  
411
            # booking requires real Event objects (not lazy Timeslots);
412
            # create them now, with data from the slots and the desk we found.
413
            events = []
414
            for start_datetime in datetimes:
415
                events.append(Event.objects.create(agenda=agenda,
416
                        meeting_type_id=meeting_type_id,
417
                        start_datetime=start_datetime,
418
                        full=False, places=1,
419
                        desk=available_desk))
420
        else:
421
            events = Event.objects.filter(id__in=slots).order_by('start_datetime')
422

  
423
        # search free places. Switch to waiting list if necessary.
424
        in_waiting_list = False
425
        for event in events:
426
            if event.waiting_list_places:
427
                if (event.booked_places + places_count) > event.places or event.waiting_list:
428
                    # if this is full or there are people waiting, put new bookings
429
                    # in the waiting list.
430
                    in_waiting_list = True
431
                    if (event.waiting_list + places_count) > event.waiting_list_places:
432
                        return Response({'err': 1, 'reason': 'sold out'})
433
            else:
434
                if (event.booked_places + places_count) > event.places:
386 435
                    return Response({'err': 1, 'reason': 'sold out'})
387 436

  
388
        else:
389
            if (event.booked_places + places_count) > event.places:
390
                return Response({'err': 1, 'reason': 'sold out'})
391

  
392
        new_booking.save()
393
        for i in range(places_count-1):
394
            additional_booking = Booking(event_id=event_pk, extra_data=extra_data,
395
                                         label=label, user_name=user_name,
396
                                         backoffice_url=backoffice_url)
397
            additional_booking.in_waiting_list = new_booking.in_waiting_list
398
            additional_booking.primary_booking = new_booking
399
            additional_booking.save()
437
        # now we have a list of events, book them.
438
        primary_booking = None
439
        for event in events:
440
            for i in range(places_count):
441
                new_booking = Booking(event_id=event.id,
442
                                      in_waiting_list=in_waiting_list,
443
                                      label=payload.get('label', ''),
444
                                      user_name=payload.get('user_name', ''),
445
                                      backoffice_url=payload.get('backoffice_url', ''),
446
                                      extra_data=extra_data)
447
                if primary_booking is not None:
448
                    new_booking.primary_booking = primary_booking
449
                new_booking.save()
450
                if primary_booking is None:
451
                    primary_booking = new_booking
400 452

  
401 453
        response = {
402 454
            'err': 0,
403
            'in_waiting_list': new_booking.in_waiting_list,
404
            'booking_id': new_booking.id,
405
            'datetime': localtime(event.start_datetime),
455
            'in_waiting_list': in_waiting_list,
456
            'booking_id': primary_booking.id,
457
            'datetime': localtime(events[0].start_datetime),
406 458
            'api': {
407 459
                'cancel_url': request.build_absolute_uri(
408
                    reverse('api-cancel-booking', kwargs={'booking_pk': new_booking.id}))
460
                    reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id}))
409 461
            }
410 462
        }
411
        if new_booking.in_waiting_list:
463
        if in_waiting_list:
412 464
            response['api']['accept_url'] = request.build_absolute_uri(
413
                    reverse('api-accept-booking', kwargs={'booking_pk': new_booking.id}))
465
                    reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id}))
414 466
        if agenda.kind == 'meetings':
415
            response['end_datetime'] = localtime(event.end_datetime)
467
            response['end_datetime'] = localtime(events[-1].end_datetime)
416 468
        if available_desk:
417 469
            response['desk'] = {
418 470
                'label': available_desk.label,
......
420 472

  
421 473
        return Response(response)
422 474

  
475
fillslots = Fillslots.as_view()
476

  
477

  
478
class Fillslot(Fillslots):
479
    serializer_class = SlotSerializer
480

  
481
    def post(self, request, agenda_identifier=None, event_pk=None, format=None):
482
        return self.fillslot(request=request,
483
                             agenda_identifier=agenda_identifier,
484
                             slots=[event_pk],  # fill a "list on one slot"
485
                             format=format)
486

  
423 487
fillslot = Fillslot.as_view()
424 488

  
425 489

  
tests/test_api.py
100 100
    resp = app.get('/api/agenda/')
101 101
    assert resp.json == {'data': [
102 102
        {'text': 'Foo bar', 'id': u'foo-bar', 'slug': 'foo-bar', 'kind': 'events',
103
         'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda1.slug}},
103
         'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda1.slug,
104
                 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda1.slug}},
104 105
        {'text': 'Foo bar Meeting', 'id': u'foo-bar-meeting', 'slug': 'foo-bar-meeting',
105 106
         'kind': 'meetings',
106 107
         'api': {'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % meetings_agenda.slug,
107 108
                 'desks_url': 'http://testserver/api/agenda/%s/desks/' % meetings_agenda.slug,
109
                 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug,
108 110
                },
109 111
        },
110 112
        {'text': 'Foo bar2', 'id': u'foo-bar2', 'kind': 'events', 'slug': 'foo-bar2',
111
         'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda2.slug}}
113
         'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda2.slug,
114
                 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug}}
112 115
        ]}
113 116

  
114 117
def test_agendas_meetingtypes_api(app, some_data, meetings_agenda):
......
290 293
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
291 294
    assert len(resp.json['data']) == 2
292 295
    fillslot_url = resp.json['data'][0]['api']['fillslot_url']
296
    two_slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
293 297

  
294 298
    time_period.end_time = datetime.time(10, 15)
295 299
    time_period.save()
......
301 305
    resp = app.post(fillslot_url)
302 306
    assert resp.json['err'] == 1
303 307
    assert resp.json['reason'] == 'no more desk available'
308
    # booking the two slots fails too
309
    fillslots_url = '/api/agenda/%s/fillslots/' % meeting_type.agenda.slug
310
    resp = app.post(fillslots_url, params={'slots': two_slots})
311
    assert resp.json['err'] == 1
312
    assert resp.json['reason'] == 'no more desk available'
304 313

  
305 314
def test_booking_api(app, some_data, user):
306 315
    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
......
309 318
    # unauthenticated
310 319
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id), status=403)
311 320

  
312
    resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
313
    event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api']['fillslot_url']
314
    assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id)
321
    for agenda_key in (agenda.slug, agenda.id):  # acces datetimes via agenda slug or id (legacy)
322
        resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
323
        event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api']['fillslot_url']
324
        assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id)
315 325

  
316 326
    app.authorization = ('Basic', ('john.doe', 'password'))
317 327
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id))
......
360 370

  
361 371
    resp = app.post('/api/agenda/233/fillslot/%s/' % event.id, status=404)
362 372

  
373
def test_booking_api_fillslots(app, some_data, user):
374
    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
375
    events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
376
    assert len(events_ids) == 3
377
    event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]  # first event
378

  
379
    # unauthenticated
380
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, status=403)
381

  
382
    for agenda_key in (agenda.slug, agenda.id):  # acces datetimes via agenda slug or id (legacy)
383
        resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
384
        api_event_ids = [x['id'] for x in resp_datetimes.json['data']]
385
        assert api_event_ids == events_ids
386

  
387
    assert Booking.objects.count() == 0
388

  
389
    app.authorization = ('Basic', ('john.doe', 'password'))
390
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_ids})
391
    primary_booking_id = resp.json['booking_id']
392
    Booking.objects.get(id=primary_booking_id)
393
    assert resp.json['datetime'] == localtime(event.start_datetime).isoformat()
394
    assert 'accept_url' not in resp.json['api']
395
    assert 'cancel_url' in resp.json['api']
396
    assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
397
    assert Booking.objects.count() == 3
398
    # these 3 bookings are related, the first is the primary one
399
    bookings = Booking.objects.all().order_by('primary_booking')
400
    assert bookings[0].primary_booking is None
401
    assert bookings[1].primary_booking.id == bookings[0].id == primary_booking_id
402
    assert bookings[2].primary_booking.id == bookings[0].id == primary_booking_id
403

  
404
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_ids})
405
    primary_booking_id_2 = resp.json['booking_id']
406
    assert Booking.objects.count() == 6
407
    assert Booking.objects.filter(event__agenda=agenda).count() == 6
408
    # 6 = 2 primary + 2*2 secondary
409
    assert Booking.objects.filter(event__agenda=agenda, primary_booking__isnull=True).count() == 2
410
    assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id).count() == 2
411
    assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id_2).count() == 2
412

  
413
    # test with additional data
414
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id,
415
            params={'slots': events_ids,
416
                    'label': 'foo', 'user_name': 'bar', 'backoffice_url': 'http://example.net/'})
417
    booking_id = resp.json['booking_id']
418
    assert Booking.objects.get(id=booking_id).label == 'foo'
419
    assert Booking.objects.get(id=booking_id).user_name == 'bar'
420
    assert Booking.objects.get(id=booking_id).backoffice_url == 'http://example.net/'
421
    assert Booking.objects.filter(primary_booking=booking_id, label='foo').count() == 2
422
    # cancel
423
    cancel_url = resp.json['api']['cancel_url']
424
    assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
425
    assert Booking.objects.get(id=booking_id).cancellation_datetime is None
426
    resp_cancel = app.post(cancel_url)
427
    assert resp_cancel.json['err'] == 0
428
    assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 3
429
    assert Booking.objects.get(id=booking_id).cancellation_datetime is not None
430

  
431
    # extra data stored in extra_data field
432
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id,
433
            params={'slots': events_ids,
434
                    'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'})
435
    assert Booking.objects.get(id=resp.json['booking_id']).label == 'l'
436
    assert Booking.objects.get(id=resp.json['booking_id']).user_name == 'u'
437
    assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == ''
438
    assert Booking.objects.get(id=resp.json['booking_id']).extra_data == {'foo': 'bar'}
439
    for booking in Booking.objects.filter(primary_booking=resp.json['booking_id']):
440
        assert booking.extra_data == {'foo': 'bar'}
441

  
442
    # test invalid data are refused
443
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id,
444
            params={'slots': events_ids,
445
                    'user_name': {'foo': 'bar'}}, status=400)
446
    assert resp.json['err'] == 1
447
    assert resp.json['reason'] == 'invalid payload'
448
    assert len(resp.json['errors']) == 1
449
    assert 'user_name' in resp.json['errors']
450

  
451
    # empty or missing slots
452
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': []}, status=400)
453
    assert resp.json['err'] == 1
454
    assert resp.json['reason'] == 'slots list cannot be empty'
455
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, status=400)
456
    assert resp.json['err'] == 1
457
    assert resp.json['reason'] == 'slots list cannot be empty'
458
    # invalid slots format
459
    resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': 'foobar'}, status=400)
460
    assert resp.json['err'] == 1
461
    assert resp.json['reason'] == 'invalid payload'
462
    assert len(resp.json['errors']) == 1
463
    assert 'slots' in resp.json['errors']
464

  
465
    # unknown agendas
466
    resp = app.post('/api/agenda/foobar/fillslots/', status=404)
467
    resp = app.post('/api/agenda/233/fillslots/', status=404)
468

  
363 469
def test_booking_api_meeting(app, meetings_agenda, user):
364 470
    agenda_id = meetings_agenda.slug
365 471
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
......
390 496
    assert resp.json['err'] == 0
391 497
    assert Booking.objects.count() == 2
392 498

  
499
def test_booking_api_meeting_fillslots(app, meetings_agenda, user):
500
    agenda_id = meetings_agenda.slug
501
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
502
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
503
    slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
504

  
505
    app.authorization = ('Basic', ('john.doe', 'password'))
506
    resp_booking = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
507
    assert Booking.objects.count() == 2
508
    primary_booking = Booking.objects.filter(primary_booking__isnull=True).first()
509
    secondary_booking = Booking.objects.filter(primary_booking=primary_booking.id).first()
510
    assert resp_booking.json['datetime'][:16] == localtime(primary_booking.event.start_datetime
511
            ).isoformat()[:16]
512
    assert resp_booking.json['end_datetime'][:16] == localtime(secondary_booking.event.end_datetime
513
            ).isoformat()[:16]
514

  
515
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
516
    assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 2
517

  
518
    # try booking the same timeslots
519
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
520
    assert resp2.json['err'] == 1
521
    assert resp2.json['reason'] == 'no more desk available'
522

  
523
    # try booking partially free timeslots (one free, one busy)
524
    nonfree_slots = [resp.json['data'][0]['id'], resp.json['data'][2]['id']]
525
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': nonfree_slots})
526
    assert resp2.json['err'] == 1
527
    assert resp2.json['reason'] == 'no more desk available'
528

  
529
    # booking other free timeslots
530
    free_slots = [resp.json['data'][3]['id'], resp.json['data'][2]['id']]
531
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': free_slots})
532
    assert resp2.json['err'] == 0
533
    cancel_url = resp2.json['api']['cancel_url']
534
    assert Booking.objects.count() == 4
535
    # 4 = 2 primary + 2 secondary
536
    assert Booking.objects.filter(primary_booking__isnull=True).count() == 2
537
    assert Booking.objects.filter(primary_booking__isnull=False).count() == 2
538
    # cancel
539
    assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
540
    resp_cancel = app.post(cancel_url)
541
    assert resp_cancel.json['err'] == 0
542
    assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 2
543

  
544
    impossible_slots = ['1:2017-05-22-1130', '2:2017-05-22-1100']
545
    resp = app.post('/api/agenda/%s/fillslots/' % agenda_id,
546
                    params={'slots': impossible_slots},
547
                    status=400)
548
    assert resp.json['err'] == 1
549
    assert resp.json['reason'] == 'all slots must have the same meeting type id (1)'
550

  
393 551
def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user):
394 552
    meetings_agenda.maximal_booking_delay = 365
395 553
    meetings_agenda.save()
......
746 904
    assert Event.objects.get(id=event.id).booked_places == 3
747 905
    assert Event.objects.get(id=event.id).waiting_list == 2
748 906

  
907
def test_multiple_booking_api_fillslots(app, some_data, user):
908
    agenda = Agenda.objects.filter(label=u'Foo bar')[0]
909
    # get slots of first 2 events
910
    events = [x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period()][:2]
911
    events_ids = [x.id for x in events]
912
    resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
913
    slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_ids]
914

  
915
    app.authorization = ('Basic', ('john.doe', 'password'))
916
    resp = app.post('/api/agenda/%s/fillslots/?count=NaN' % agenda.slug, params={'slots': slots}, status=400)
917
    assert resp.json['err'] == 1
918
    assert resp.json['reason'] == "invalid value for count (u'NaN')"
919

  
920
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
921
                    params={'slots': slots, 'count': 'NaN'}, status=400)
922
    assert resp.json['err'] == 1
923
    assert resp.json['reason'] == "invalid payload"
924
    assert 'count' in resp.json['errors']
925

  
926
    # get 3 places on 2 slots
927
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
928
                    params={'slots': slots, 'count': '3'})
929
    # one booking with 5 children
930
    booking = Booking.objects.get(id=resp.json['booking_id'])
931
    cancel_url = resp.json['api']['cancel_url']
932
    assert Booking.objects.filter(primary_booking=booking).count() == 5
933
    assert resp.json['datetime'] == localtime(events[0].start_datetime).isoformat()
934
    assert 'accept_url' not in resp.json['api']
935
    assert 'cancel_url' in resp.json['api']
936
    for event in events:
937
        assert Event.objects.get(id=event.id).booked_places == 3
938

  
939
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
940
                    params={'slots': slots, 'count': 2})
941
    for event in events:
942
        assert Event.objects.get(id=event.id).booked_places == 5
943

  
944
    resp = app.post(cancel_url)
945
    for event in events:
946
        assert Event.objects.get(id=event.id).booked_places == 2
947

  
948
    # check available places overflow
949
    # NB: limit only the first event !
950
    events[0].places = 3
951
    events[0].waiting_list_places = 8
952
    events[0].save()
953

  
954
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
955
                    params={'slots': slots, 'count': 5})
956
    for event in events:
957
        assert Event.objects.get(id=event.id).booked_places == 2
958
        assert Event.objects.get(id=event.id).waiting_list == 5
959
    accept_url = resp.json['api']['accept_url']
960

  
961
    return
962

  
963
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
964
                    params={'slots': slots, 'count': 5})
965
    assert resp.json['err'] == 1
966
    assert resp.json['reason'] == 'sold out'
967
    for event in events:
968
        assert Event.objects.get(id=event.id).booked_places == 2
969
        assert Event.objects.get(id=event.id).waiting_list == 5
970

  
971
    # accept the waiting list
972
    resp = app.post(accept_url)
973
    for event in events:
974
        assert Event.objects.get(id=event.id).booked_places == 7
975
        assert Event.objects.get(id=event.id).waiting_list == 0
976

  
977
    # check with a short waiting list
978
    Booking.objects.all().delete()
979
    # NB: limit only the first event !
980
    events[0].places = 4
981
    events[0].waiting_list_places = 2
982
    events[0].save()
983

  
984
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
985
                    params={'slots': slots, 'count': 5})
986
    assert resp.json['err'] == 1
987
    assert resp.json['reason'] == 'sold out'
988

  
989
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
990
                    params={'slots': slots, 'count': 3})
991
    assert resp.json['err'] == 0
992
    for event in events:
993
        assert Event.objects.get(id=event.id).booked_places == 3
994
        assert Event.objects.get(id=event.id).waiting_list == 0
995

  
996
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
997
                    params={'slots': slots, 'count': 3})
998
    assert resp.json['err'] == 1
999
    assert resp.json['reason'] == 'sold out'
1000

  
1001
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug,
1002
                    params={'slots': slots, 'count': '2'})
1003
    assert resp.json['err'] == 0
1004
    for event in events:
1005
        assert Event.objects.get(id=event.id).booked_places == 3
1006
        assert Event.objects.get(id=event.id).waiting_list == 2
1007

  
749 1008
def test_agenda_detail_api(app, some_data):
750 1009
    agenda = Agenda.objects.get(slug='foo-bar')
751 1010
    resp = app.get('/api/agenda/%s/' % agenda.slug)
......
898 1157
        app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
899 1158
        assert queries_count_datetime1 == len(ctx.captured_queries)
900 1159

  
1160
def test_agenda_meeting_api_fillslots_multiple_desks(app, meetings_agenda, user):
1161
    app.authorization = ('Basic', ('john.doe', 'password'))
1162
    agenda_id = meetings_agenda.slug
1163
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
1164

  
1165
    # add a second desk, same timeperiods
1166
    time_period = meetings_agenda.desk_set.first().timeperiod_set.first()
1167
    desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda)
1168
    TimePeriod.objects.create(
1169
        start_time=time_period.start_time, end_time=time_period.end_time,
1170
        weekday=time_period.weekday, desk=desk2)
1171

  
1172
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
1173
    slots = [x['id'] for x in resp.json['data'][:3]]
1174

  
1175
    def get_free_places():
1176
        resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
1177
        return len([x for x in resp.json['data'] if not x['disabled']])
1178
    start_free_places = get_free_places()
1179

  
1180
    # booking 3 slots on desk 1
1181
    fillslots_url = '/api/agenda/%s/fillslots/' % agenda_id
1182
    resp = app.post(fillslots_url, params={'slots': slots})
1183
    assert resp.json['err'] == 0
1184
    desk1 = resp.json['desk']['slug']
1185
    cancel_url = resp.json['api']['cancel_url']
1186
    assert get_free_places() == start_free_places
1187

  
1188
    # booking same slots again, will be on desk 2
1189
    resp = app.post(fillslots_url, params={'slots': slots})
1190
    assert resp.json['err'] == 0
1191
    assert resp.json['desk']['slug'] != desk2
1192
    # 3 places are disabled in datetimes list
1193
    assert get_free_places() == start_free_places - len(slots)
1194

  
1195
    # try booking again: no desk available
1196
    resp = app.post(fillslots_url, params={'slots': slots})
1197
    assert resp.json['err'] == 1
1198
    assert resp.json['reason'] == 'no more desk available'
1199
    assert get_free_places() == start_free_places - len(slots)
1200

  
1201
    # cancel desk 1 booking
1202
    resp = app.post(cancel_url)
1203
    assert resp.json['err'] == 0
1204
    # all places are free again
1205
    assert get_free_places() == start_free_places
1206

  
1207
    # booking a single slot (must be on desk 1)
1208
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, slots[1]))
1209
    assert resp.json['err'] == 0
1210
    assert resp.json['desk']['slug'] == desk1
1211
    cancel_url = resp.json['api']['cancel_url']
1212
    assert get_free_places() == start_free_places - 1
1213

  
1214
    # try booking the 3 slots again: no desk available, one slot is not fully available
1215
    resp = app.post(fillslots_url, params={'slots': slots})
1216
    assert resp.json['err'] == 1
1217
    assert resp.json['reason'] == 'no more desk available'
1218

  
1219
    # cancel last signel slot booking, desk1 will be free
1220
    resp = app.post(cancel_url)
1221
    assert resp.json['err'] == 0
1222
    assert get_free_places() == start_free_places
1223

  
1224
    # booking again is ok, on desk 1
1225
    resp = app.post(fillslots_url, params={'slots': slots})
1226
    assert resp.json['err'] == 0
1227
    assert resp.json['desk']['slug'] == desk1
1228
    assert get_free_places() == start_free_places - len(slots)
901 1229

  
902 1230
def test_agenda_meeting_same_day(app, meetings_agenda, mock_now, user):
903 1231
    app.authorization = ('Basic', ('john.doe', 'password'))
904
-