Projet

Général

Profil

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

Thomas Noël, 28 mai 2018 10:59

Télécharger (33,4 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
113 113
                reverse('api-agenda-desks',
114 114
                        kwargs={'agenda_identifier': agenda.slug}))
115 115
        }
116
    agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
117
        reverse('api-agenda-fillslots',
118
                kwargs={'agenda_identifier': agenda.slug}))
116 119

  
117 120
    return agenda_detail
118 121

  
......
300 303

  
301 304

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

  
307 314

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

  
309
class Fillslot(APIView):
323

  
324
class Fillslots(APIView):
310 325
    permission_classes = (permissions.IsAuthenticated,)
326
    serializer_class = SlotsSerializer
311 327

  
312 328
    def post(self, request, agenda_identifier=None, event_pk=None, format=None):
329
        return self.fillslot(request=request, agenda_identifier=agenda_identifier,
330
                             format=format)
331

  
332
    def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
313 333
        try:
314 334
            agenda = Agenda.objects.get(slug=agenda_identifier)
315 335
        except Agenda.DoesNotExist:
......
319 339
            except (ValueError, Agenda.DoesNotExist):
320 340
                raise Http404()
321 341

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

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

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

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

  
347 378
        available_desk = None
379

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

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

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

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

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

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

  
375
            event_pk = event.id
376

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

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

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

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

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

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

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

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

  
423 475
        return Response(response)
424 476

  
477
fillslots = Fillslots.as_view()
478

  
479

  
480
class Fillslot(Fillslots):
481
    serializer_class = SlotSerializer
482

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

  
425 489
fillslot = Fillslot.as_view()
426 490

  
427 491

  
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
-