Projet

Général

Profil

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

Thomas Noël, 04 avril 2018 19:57

Télécharger (14,6 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 | 203 ++++++++++++++++++++++++++++++++++------------------
 tests/test_api.py   |   7 +-
 3 files changed, 141 insertions(+), 71 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):
304
    '''
305
    payload to fill one slot. The slot (event id) is in the URL.
306
    '''
301 307
    label = serializers.CharField(max_length=150, allow_blank=True, required=False)
302 308
    user_name = serializers.CharField(max_length=250, allow_blank=True, required=False)
303 309
    backoffice_url = serializers.URLField(allow_blank=True, required=False)
304 310
    count = serializers.IntegerField(min_value=1, required=False)
305 311

  
306 312

  
307
class Fillslot(APIView):
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(
319
        child=serializers.CharField(max_length=64, allow_blank=False))
320

  
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)
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
                    })
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 datetimes_by_desk:
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, try to book them.
438
        primary_booking = None
439

  
440
        for event in events:
441
            for i in range(places_count):
442
                new_booking = Booking(event_id=event.id,
443
                                      in_waiting_list=in_waiting_list,
444
                                      label=payload.get('label', ''),
445
                                      user_name=payload.get('user_name', ''),
446
                                      backoffice_url=payload.get('backoffice_url', ''),
447
                                      extra_data=extra_data)
448
                if primary_booking is not None:
449
                    new_booking.primary_booking = primary_booking
450
                new_booking.save()
451
                if primary_booking is None:
452
                    primary_booking = new_booking
400 453

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

  
421 474
        return Response(response)
422 475

  
476
fillslots = Fillslots.as_view()
477

  
478

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

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

  
423 488
fillslot = Fillslot.as_view()
424 489

  
425 490

  
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):
115
-