Projet

Général

Profil

0004-api-split-fillslot-endpoints-43077.patch

Lauréline Guérin, 22 mai 2020 10:58

Télécharger (54,9 ko)

Voir les différences:

Subject: [PATCH 4/5] api: split fillslot endpoints (#43077)

 chrono/api/serializers.py |  18 +-
 chrono/api/urls.py        |  10 +
 chrono/api/views.py       | 538 ++++++++++++++++++++++----------------
 tests/test_api.py         | 110 +++++---
 4 files changed, 406 insertions(+), 270 deletions(-)
chrono/api/serializers.py
17 17
from rest_framework import serializers
18 18

  
19 19

  
20
class SlotSerializer(serializers.Serializer):
20
class BaseSlotSerializer(serializers.Serializer):
21 21
    '''
22 22
    payload to fill one slot. The slot (event id) is in the URL.
23 23
    '''
......
27 27
    user_name = serializers.CharField(max_length=250, allow_blank=True)
28 28
    user_display_label = serializers.CharField(max_length=250, allow_blank=True)
29 29
    backoffice_url = serializers.URLField(allow_blank=True)
30

  
31

  
32
class SlotSerializer(BaseSlotSerializer):
30 33
    count = serializers.IntegerField(min_value=1)
31 34
    cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
32 35
    force_waiting_list = serializers.BooleanField(default=False)
33 36

  
34 37

  
38
class MeetingsSlotSerializer(BaseSlotSerializer):
39
    pass
40

  
41

  
35 42
class StringOrListField(serializers.ListField):
36 43
    def to_internal_value(self, data):
37 44
        if isinstance(data, str):
......
48 55
    slots = StringOrListField(required=True, child=serializers.CharField(max_length=64, allow_blank=False))
49 56

  
50 57

  
58
class MeetingsSlotsSerializer(MeetingsSlotSerializer):
59
    '''
60
    payload to fill multiple slots: same as MeetingsSlotSerializer, but the
61
    slots list is in the payload.
62
    '''
63

  
64
    slots = StringOrListField(required=True, child=serializers.CharField(max_length=64, allow_blank=False))
65

  
66

  
51 67
class ResizeSerializer(serializers.Serializer):
52 68
    count = serializers.IntegerField(min_value=1)
chrono/api/urls.py
50 50
        views.meeting_datetimes,
51 51
        name='api-agenda-meeting-datetimes',
52 52
    ),
53
    url(
54
        r'^agenda/(?P<agenda_identifier>[\w-]+)/meetings/fillslot/(?P<event_identifier>[\w:-]+)/$',
55
        views.meetings_fillslot,
56
        name='api-agenda-meetings-fillslot',
57
    ),
58
    url(
59
        r'^agenda/(?P<agenda_identifier>[\w-]+)/meetings/fillslots/$',
60
        views.meetings_fillslots,
61
        name='api-agenda-meetings-fillslots',
62
    ),
53 63
    url(r'^booking/(?P<booking_pk>\w+)/$', views.booking),
54 64
    url(r'^booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking, name='api-cancel-booking'),
55 65
    url(r'^booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking, name='api-accept-booking'),
chrono/api/views.py
27 27
from django.utils.encoding import force_text
28 28
from django.utils.formats import date_format
29 29
from django.utils.timezone import now, make_aware, localtime
30
from django.utils.translation import gettext_noop
31 30
from django.utils.translation import ugettext_lazy as _
32 31

  
33 32
from rest_framework import permissions, status
......
256 255
        agenda_detail['api'] = {
257 256
            'datetimes_url': request.build_absolute_uri(
258 257
                reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug})
259
            )
258
            ),
259
            'fillslots_url': request.build_absolute_uri(
260
                reverse('api-agenda-fillslots', kwargs={'agenda_identifier': agenda.slug})
261
            ),
260 262
        }
261 263
    elif agenda.accept_meetings():
262 264
        agenda_detail['api'] = {
......
266 268
            'desks_url': request.build_absolute_uri(
267 269
                reverse('api-agenda-desks', kwargs={'agenda_identifier': agenda.slug})
268 270
            ),
271
            'fillslots_url': request.build_absolute_uri(
272
                reverse('api-agenda-meetings-fillslots', kwargs={'agenda_identifier': agenda.slug})
273
            ),
269 274
        }
270
    agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
271
        reverse('api-agenda-fillslots', kwargs={'agenda_identifier': agenda.slug})
272
    )
273 275

  
274 276
    return agenda_detail
275 277

  
......
388 390
                        'fillslot_url': request.build_absolute_uri(
389 391
                            reverse(
390 392
                                'api-fillslot',
391
                                kwargs={'agenda_identifier': agenda.slug, 'event_identifier': x.id,},
393
                                kwargs={'agenda_identifier': agenda.slug, 'event_identifier': x.id},
392 394
                            )
393 395
                        ),
394 396
                        'status_url': request.build_absolute_uri(
395 397
                            reverse(
396 398
                                'api-event-status',
397
                                kwargs={'agenda_identifier': agenda.slug, 'event_identifier': x.id,},
399
                                kwargs={'agenda_identifier': agenda.slug, 'event_identifier': x.id},
398 400
                            )
399 401
                        ),
400 402
                    },
......
455 457
        fake_event_identifier = '__event_identifier__'
456 458
        fillslot_url = request.build_absolute_uri(
457 459
            reverse(
458
                'api-fillslot',
459
                kwargs={'agenda_identifier': agenda.slug, 'event_identifier': fake_event_identifier,},
460
                'api-agenda-meetings-fillslot',
461
                kwargs={'agenda_identifier': agenda.slug, 'event_identifier': fake_event_identifier},
460 462
            )
461 463
        )
462 464

  
......
477 479
                    'datetime': format_response_datetime(slot.start_datetime),
478 480
                    'text': date_format(slot.start_datetime, format='DATETIME_FORMAT'),
479 481
                    'disabled': bool(slot.full),
480
                    'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, slot_id),},
482
                    'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, slot_id)},
481 483
                }
482 484
                for slot in generator_of_unique_slots
483 485
                # we do not have the := operator, so we do that
......
546 548
agenda_desk_list = AgendaDeskList.as_view()
547 549

  
548 550

  
549
class Fillslots(APIView):
551
class FillSlotsMixin(object):
550 552
    permission_classes = (permissions.IsAuthenticated,)
551
    serializer_class = serializers.SlotsSerializer
552 553

  
553
    def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
554
    def post(self, *args, **kwargs):
554 555
        try:
555
            return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
556
            serializer = self.serializer_class(data=self.request.data, partial=True)
557
            if not serializer.is_valid():
558
                raise APIError(
559
                    _('invalid payload'),
560
                    err_class='invalid payload',
561
                    errors=serializer.errors,
562
                    http_status=status.HTTP_400_BAD_REQUEST,
563
                )
564
            self.agenda = self.get_agenda()
565
            return self._post(payload=serializer.validated_data)
556 566
        except APIError as e:
557 567
            return e.to_response()
558 568

  
559
    def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
560
        multiple_booking = bool(not slots)
561
        try:
562
            agenda = Agenda.objects.get(slug=agenda_identifier)
563
        except Agenda.DoesNotExist:
564
            try:
565
                # legacy access by agenda id
566
                agenda = Agenda.objects.get(id=int(agenda_identifier))
567
            except (ValueError, Agenda.DoesNotExist):
568
                raise Http404()
569

  
570
        serializer = self.serializer_class(data=request.data, partial=True)
571
        if not serializer.is_valid():
572
            raise APIError(
573
                _('invalid payload'),
574
                err_class='invalid payload',
575
                errors=serializer.errors,
576
                http_status=status.HTTP_400_BAD_REQUEST,
577
            )
578
        payload = serializer.validated_data
579

  
580
        if 'slots' in payload:
581
            slots = payload['slots']
569
    def validate_slots(self, slots):
582 570
        if not slots:
583 571
            raise APIError(
584 572
                _('slots list cannot be empty'),
585 573
                err_class='slots list cannot be empty',
586 574
                http_status=status.HTTP_400_BAD_REQUEST,
587 575
            )
576
        return slots
588 577

  
578
    def get_agenda(self):
579
        agenda_identifier = self.kwargs['agenda_identifier']
580
        try:
581
            return Agenda.objects.get(slug=agenda_identifier)
582
        except Agenda.DoesNotExist:
583
            try:
584
                # legacy access by agenda id
585
                return Agenda.objects.get(pk=int(agenda_identifier))
586
            except (ValueError, Agenda.DoesNotExist):
587
                raise Http404()
588

  
589
    def get_places_count(self, payload):
589 590
        if 'count' in payload:
590 591
            places_count = payload['count']
591
        elif 'count' in request.query_params:
592
        elif 'count' in self.request.query_params:
592 593
            # legacy: count in the query string
593 594
            try:
594
                places_count = int(request.query_params['count'])
595
                places_count = int(self.request.query_params['count'])
595 596
            except ValueError:
596 597
                raise APIError(
597
                    _('invalid value for count (%s)') % request.query_params['count'],
598
                    err_class='invalid value for count (%s)' % request.query_params['count'],
598
                    _('invalid value for count (%s)') % self.request.query_params['count'],
599
                    err_class='invalid value for count (%s)' % self.request.query_params['count'],
599 600
                    http_status=status.HTTP_400_BAD_REQUEST,
600 601
                )
601 602
        else:
......
607 608
                err_class='count cannot be less than or equal to zero',
608 609
                http_status=status.HTTP_400_BAD_REQUEST,
609 610
            )
611
        return places_count
610 612

  
611
        to_cancel_booking = None
612
        cancel_booking_id = None
613
        if payload.get('cancel_booking_id'):
614
            try:
615
                cancel_booking_id = int(payload.get('cancel_booking_id'))
616
            except (ValueError, TypeError):
617
                raise APIError(
618
                    _('cancel_booking_id is not an integer'),
619
                    err_class='cancel_booking_id is not an integer',
620
                    http_status=status.HTTP_400_BAD_REQUEST,
621
                )
622

  
623
        if cancel_booking_id is not None:
624
            cancel_error = None
625
            try:
626
                to_cancel_booking = Booking.objects.get(pk=cancel_booking_id)
627
                if to_cancel_booking.cancellation_datetime:
628
                    cancel_error = gettext_noop('cancel booking: booking already cancelled')
629
                else:
630
                    to_cancel_places_count = (
631
                        to_cancel_booking.secondary_booking_set.filter(event=to_cancel_booking.event).count()
632
                        + 1
633
                    )
634
                    if places_count != to_cancel_places_count:
635
                        cancel_error = gettext_noop('cancel booking: count is different')
636
            except Booking.DoesNotExist:
637
                cancel_error = gettext_noop('cancel booking: booking does no exist')
613
    def get_booking_to_cancel(self, payload, places_count=None):
614
        if not payload.get('cancel_booking_id'):
615
            return
638 616

  
639
            if cancel_error:
640
                raise APIError(
641
                    _(cancel_error), err_class=cancel_error,
642
                )
643

  
644
        extra_data = {}
645
        for k, v in request.data.items():
646
            if k not in serializer.validated_data:
647
                extra_data[k] = v
648

  
649
        available_desk = None
617
        try:
618
            to_cancel_booking = Booking.objects.get(pk=int(payload['cancel_booking_id']))
619
        except (ValueError, TypeError):
620
            raise APIError(
621
                _('cancel_booking_id is not an integer'),
622
                err_class='cancel_booking_id is not an integer',
623
                http_status=status.HTTP_400_BAD_REQUEST,
624
            )
625
        except Booking.DoesNotExist:
626
            raise APIError(
627
                _('cancel booking: booking does no exist'), err_class='cancel booking: booking does no exist'
628
            )
650 629

  
651
        if agenda.accept_meetings():
652
            # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
653
            # split them back to get both parts
654
            meeting_type_id = slots[0].split(':')[0]
655
            datetimes = set()
656
            for slot in slots:
657
                try:
658
                    meeting_type_id_, datetime_str = slot.split(':')
659
                except ValueError:
660
                    raise APIError(
661
                        _('invalid slot: %s') % slot,
662
                        err_class='invalid slot: %s' % slot,
663
                        http_status=status.HTTP_400_BAD_REQUEST,
664
                    )
665
                if meeting_type_id_ != meeting_type_id:
666
                    raise APIError(
667
                        _('all slots must have the same meeting type id (%s)') % meeting_type_id,
668
                        err_class='all slots must have the same meeting type id (%s)' % meeting_type_id,
669
                        http_status=status.HTTP_400_BAD_REQUEST,
670
                    )
671
                datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
630
        if to_cancel_booking.cancellation_datetime:
631
            raise APIError(
632
                _('cancel booking: booking already cancelled'),
633
                err_class='cancel booking: booking already cancelled',
634
            )
672 635

  
673
            # get all free slots and separate them by desk
674
            try:
675
                all_slots = sorted(
676
                    get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id)),
677
                    key=lambda slot: slot.start_datetime,
678
                )
679
            except (MeetingType.DoesNotExist, ValueError):
636
        if places_count is not None:
637
            # multiple booking
638
            to_cancel_places_count = (
639
                to_cancel_booking.secondary_booking_set.filter(event=to_cancel_booking.event).count() + 1
640
            )
641
            if places_count != to_cancel_places_count:
680 642
                raise APIError(
681
                    _('invalid meeting type id: %s') % meeting_type_id,
682
                    err_class='invalid meeting type id: %s' % meeting_type_id,
683
                    http_status=status.HTTP_400_BAD_REQUEST,
643
                    _('cancel booking: count is different'), err_class='cancel booking: count is different'
684 644
                )
685 645

  
686
            all_free_slots = [slot for slot in all_slots if not slot.full]
687
            datetimes_by_desk = collections.defaultdict(set)
688
            for slot in all_free_slots:
689
                datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
690

  
691
            available_desk = None
692

  
693
            if agenda.kind == 'virtual':
694
                # Compute fill_rate by agenda/date
695
                fill_rates = collections.defaultdict(dict)
696
                for slot in all_slots:
697
                    ref_date = slot.start_datetime.date()
698
                    if ref_date not in fill_rates[slot.desk.agenda]:
699
                        date_dict = fill_rates[slot.desk.agenda][ref_date] = {'free': 0, 'full': 0}
700
                    else:
701
                        date_dict = fill_rates[slot.desk.agenda][ref_date]
702
                    if slot.full:
703
                        date_dict['full'] += 1
704
                    else:
705
                        date_dict['free'] += 1
706
                for dd in fill_rates.values():
707
                    for date_dict in dd.values():
708
                        date_dict['fill_rate'] = date_dict['full'] / (date_dict['full'] + date_dict['free'])
709

  
710
                # select a desk on the agenda with min fill_rate on the given date
711
                for available_desk_id in sorted(datetimes_by_desk.keys()):
712
                    if datetimes.issubset(datetimes_by_desk[available_desk_id]):
713
                        desk = Desk.objects.get(id=available_desk_id)
714
                        if available_desk is None:
715
                            available_desk = desk
716
                            available_desk_rate = 0
717
                            for dt in datetimes:
718
                                available_desk_rate += fill_rates[available_desk.agenda][dt.date()][
719
                                    'fill_rate'
720
                                ]
721
                        else:
722
                            for dt in datetimes:
723
                                desk_rate = 0
724
                                for dt in datetimes:
725
                                    desk_rate += fill_rates[desk.agenda][dt.date()]['fill_rate']
726
                            if desk_rate < available_desk_rate:
727
                                available_desk = desk
728
                                available_desk_rate = desk_rate
729

  
730
            else:
731
                # meeting agenda
732
                # search first desk where all requested slots are free
733
                for available_desk_id in sorted(datetimes_by_desk.keys()):
734
                    if datetimes.issubset(datetimes_by_desk[available_desk_id]):
735
                        available_desk = Desk.objects.get(id=available_desk_id)
736
                        break
646
        return to_cancel_booking
737 647

  
738
            if available_desk is None:
739
                raise APIError(
740
                    _('no more desk available'), err_class='no more desk available',
741
                )
742 648

  
743
            # all datetimes are free, book them in order
744
            datetimes = list(datetimes)
745
            datetimes.sort()
649
class EventFillSlotsMixin(FillSlotsMixin):
650
    def get_events(self, slots):
651
        try:
652
            events = Event.objects.filter(agenda=self.agenda, id__in=[int(s) for s in slots]).order_by(
653
                'start_datetime'
654
            )
655
        except ValueError:
656
            events = Event.objects.filter(agenda=self.agenda, slug__in=slots).order_by('start_datetime')
746 657

  
747
            # get a real meeting_type for virtual agenda
748
            if agenda.kind == 'virtual':
749
                meeting_type_id = MeetingType.objects.get(
750
                    agenda=available_desk.agenda, slug=meeting_type_id
751
                ).pk
658
        if not events.count():
659
            raise APIError(
660
                _('unknown event identifiers or slugs'),
661
                err_class='unknown event identifiers or slugs',
662
                http_status=status.HTTP_400_BAD_REQUEST,
663
            )
752 664

  
753
            # booking requires real Event objects (not lazy Timeslots);
754
            # create them now, with data from the slots and the desk we found.
755
            events = []
756
            for start_datetime in datetimes:
757
                events.append(
758
                    Event.objects.create(
759
                        agenda=available_desk.agenda,
760
                        meeting_type_id=meeting_type_id,
761
                        start_datetime=start_datetime,
762
                        full=False,
763
                        places=1,
764
                        desk=available_desk,
765
                    )
766
                )
767
        else:
768
            try:
769
                events = Event.objects.filter(id__in=[int(s) for s in slots]).order_by('start_datetime')
770
            except ValueError:
771
                events = Event.objects.filter(slug__in=slots).order_by('start_datetime')
665
        return events
772 666

  
773
            if not events.count():
774
                raise APIError(
775
                    _('unknown event identifiers or slugs'),
776
                    err_class='unknown event identifiers or slugs',
777
                    http_status=status.HTTP_400_BAD_REQUEST,
778
                )
667
    def fillslots(self, payload, slots, multiple_booking=False):
668
        places_count = self.get_places_count(payload)
669
        to_cancel_booking = self.get_booking_to_cancel(payload, places_count)
670
        events = self.get_events(slots)
779 671

  
780 672
        # search free places. Switch to waiting list if necessary.
781 673
        in_waiting_list = False
......
804 696
                        _('sold out'), err_class='sold out',
805 697
                    )
806 698

  
699
        extra_data = {}
700
        for k, v in self.request.data.items():
701
            if k not in payload:
702
                extra_data[k] = v
703

  
807 704
        with transaction.atomic():
808 705
            if to_cancel_booking:
809 706
                cancelled_booking_id = to_cancel_booking.pk
......
813 710
            primary_booking = None
814 711
            for event in events:
815 712
                for i in range(places_count):
816
                    new_booking = Booking(
713
                    new_booking = Booking.objects.create(
817 714
                        event_id=event.id,
715
                        primary_booking=primary_booking,
818 716
                        in_waiting_list=in_waiting_list,
819 717
                        label=payload.get('label', ''),
820 718
                        user_external_id=payload.get('user_external_id', ''),
......
823 721
                        user_display_label=payload.get('user_display_label', ''),
824 722
                        extra_data=extra_data,
825 723
                    )
826
                    if primary_booking is not None:
827
                        new_booking.primary_booking = primary_booking
828
                    new_booking.save()
829
                    if primary_booking is None:
830
                        primary_booking = new_booking
724
                    primary_booking = primary_booking or new_booking
831 725

  
832 726
        response = {
833 727
            'err': 0,
834 728
            'in_waiting_list': in_waiting_list,
835 729
            'booking_id': primary_booking.id,
836
            'datetime': format_response_datetime(events[0].start_datetime),
730
            'datetime': format_response_datetime(primary_booking.event.start_datetime),
837 731
            'agenda': {
838 732
                'label': primary_booking.event.agenda.label,
839 733
                'slug': primary_booking.event.agenda.slug,
840 734
            },
841 735
            'api': {
842
                'cancel_url': request.build_absolute_uri(
736
                'cancel_url': self.request.build_absolute_uri(
843 737
                    reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id})
844 738
                ),
845
                'ics_url': request.build_absolute_uri(
739
                'ics_url': self.request.build_absolute_uri(
846 740
                    reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id})
847 741
                ),
848 742
            },
849 743
        }
850 744
        if in_waiting_list:
851
            response['api']['accept_url'] = request.build_absolute_uri(
745
            response['api']['accept_url'] = self.request.build_absolute_uri(
852 746
                reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id})
853 747
            )
854
        elif agenda.kind == 'events':
855
            response['api']['suspend_url'] = request.build_absolute_uri(
748
        else:
749
            response['api']['suspend_url'] = self.request.build_absolute_uri(
856 750
                reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
857 751
            )
858
        if agenda.accept_meetings():
859
            response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
860
            response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
861
        if available_desk:
862
            response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
863 752
        if to_cancel_booking:
864 753
            response['cancelled_booking_id'] = cancelled_booking_id
865
        if agenda.kind == 'events' and not multiple_booking:
754
        if not multiple_booking:
866 755
            event = events[0]
867 756
            # event.full is not up to date, it might have been changed by previous new_booking.save().
868 757
            event.refresh_from_db()
869 758
            response['places'] = get_event_places(event)
870
        if agenda.kind == 'events' and multiple_booking:
759
        else:
871 760
            response['events'] = [
872 761
                {
873 762
                    'slug': x.slug,
......
881 770
        return Response(response)
882 771

  
883 772

  
884
fillslots = Fillslots.as_view()
885

  
773
class MeetingsFillSlotsMixin(FillSlotsMixin):
774
    def fillslots(self, payload, slots, multiple_booking=False):
775
        places_count = self.get_places_count(payload)
776
        to_cancel_booking = self.get_booking_to_cancel(payload, places_count)
886 777

  
887
class Fillslot(Fillslots):
888
    serializer_class = serializers.SlotSerializer
778
        # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
779
        # split them back to get both parts
780
        meeting_type_id = slots[0].split(':')[0]
781
        datetimes = set()
782
        for slot in slots:
783
            try:
784
                meeting_type_id_, datetime_str = slot.split(':')
785
            except ValueError:
786
                raise APIError(
787
                    _('invalid slot: %s') % slot,
788
                    err_class='invalid slot: %s' % slot,
789
                    http_status=status.HTTP_400_BAD_REQUEST,
790
                )
791
            if meeting_type_id_ != meeting_type_id:
792
                raise APIError(
793
                    _('all slots must have the same meeting type id (%s)') % meeting_type_id,
794
                    err_class='all slots must have the same meeting type id (%s)' % meeting_type_id,
795
                    http_status=status.HTTP_400_BAD_REQUEST,
796
                )
797
            datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
889 798

  
890
    def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
799
        # get all free slots and separate them by desk
891 800
        try:
892
            return self.fillslot(
893
                request=request,
894
                agenda_identifier=agenda_identifier,
895
                slots=[event_identifier],  # fill a "list on one slot"
896
                format=format,
801
            all_slots = sorted(
802
                get_all_slots(self.agenda, self.agenda.get_meetingtype(id_=meeting_type_id)),
803
                key=lambda slot: slot.start_datetime,
897 804
            )
898
        except APIError as e:
899
            return e.to_response()
805
        except (MeetingType.DoesNotExist, ValueError):
806
            raise APIError(
807
                _('invalid meeting type id: %s') % meeting_type_id,
808
                err_class='invalid meeting type id: %s' % meeting_type_id,
809
                http_status=status.HTTP_400_BAD_REQUEST,
810
            )
811

  
812
        all_free_slots = [slot for slot in all_slots if not slot.full]
813
        datetimes_by_desk = collections.defaultdict(set)
814
        for slot in all_free_slots:
815
            datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
816

  
817
        available_desk = None
818

  
819
        if self.agenda.kind == 'virtual':
820
            # Compute fill_rate by agenda/date
821
            fill_rates = collections.defaultdict(dict)
822
            for slot in all_slots:
823
                ref_date = slot.start_datetime.date()
824
                if ref_date not in fill_rates[slot.desk.agenda]:
825
                    date_dict = fill_rates[slot.desk.agenda][ref_date] = {'free': 0, 'full': 0}
826
                else:
827
                    date_dict = fill_rates[slot.desk.agenda][ref_date]
828
                if slot.full:
829
                    date_dict['full'] += 1
830
                else:
831
                    date_dict['free'] += 1
832
            for dd in fill_rates.values():
833
                for date_dict in dd.values():
834
                    date_dict['fill_rate'] = date_dict['full'] / (date_dict['full'] + date_dict['free'])
835

  
836
            # select a desk on the agenda with min fill_rate on the given date
837
            for available_desk_id in sorted(datetimes_by_desk.keys()):
838
                if datetimes.issubset(datetimes_by_desk[available_desk_id]):
839
                    desk = Desk.objects.get(id=available_desk_id)
840
                    if available_desk is None:
841
                        available_desk = desk
842
                        available_desk_rate = 0
843
                        for dt in datetimes:
844
                            available_desk_rate += fill_rates[available_desk.agenda][dt.date()]['fill_rate']
845
                    else:
846
                        for dt in datetimes:
847
                            desk_rate = 0
848
                            for dt in datetimes:
849
                                desk_rate += fill_rates[desk.agenda][dt.date()]['fill_rate']
850
                        if desk_rate < available_desk_rate:
851
                            available_desk = desk
852
                            available_desk_rate = desk_rate
853

  
854
        else:
855
            # meeting agenda
856
            # search first desk where all requested slots are free
857
            for available_desk_id in sorted(datetimes_by_desk.keys()):
858
                if datetimes.issubset(datetimes_by_desk[available_desk_id]):
859
                    available_desk = Desk.objects.get(id=available_desk_id)
860
                    break
861

  
862
        if available_desk is None:
863
            raise APIError(
864
                _('no more desk available'), err_class='no more desk available',
865
            )
866

  
867
        # all datetimes are free, book them in order
868
        datetimes = list(datetimes)
869
        datetimes.sort()
870

  
871
        # get a real meeting_type for virtual agenda
872
        if self.agenda.kind == 'virtual':
873
            meeting_type_id = MeetingType.objects.get(agenda=available_desk.agenda, slug=meeting_type_id).pk
874

  
875
        extra_data = {}
876
        for k, v in self.request.data.items():
877
            if k not in payload:
878
                extra_data[k] = v
879

  
880
        with transaction.atomic():
881
            if to_cancel_booking:
882
                cancelled_booking_id = to_cancel_booking.pk
883
                to_cancel_booking.cancel()
884

  
885
            # booking requires real Event objects (not lazy Timeslots);
886
            # create them now, with data from the slots and the desk we found.
887
            first_event = None
888
            primary_booking = None
889
            for start_datetime in datetimes:
890
                event = Event.objects.create(
891
                    agenda=available_desk.agenda,
892
                    meeting_type_id=meeting_type_id,
893
                    start_datetime=start_datetime,
894
                    full=False,
895
                    places=1,
896
                    desk=available_desk,
897
                )
898
                first_event = first_event or event
899
                # now book the event
900
                booking = Booking.objects.create(
901
                    event=event,
902
                    primary_booking=primary_booking,
903
                    label=payload.get('label', ''),
904
                    user_external_id=payload.get('user_external_id', ''),
905
                    user_name=payload.get('user_name', ''),
906
                    backoffice_url=payload.get('backoffice_url', ''),
907
                    user_display_label=payload.get('user_display_label', ''),
908
                    extra_data=extra_data,
909
                )
910
                primary_booking = primary_booking or booking
911
            last_event = event
912

  
913
        response = {
914
            'err': 0,
915
            'booking_id': primary_booking.pk,
916
            'datetime': format_response_datetime(first_event.start_datetime),
917
            'end_datetime': format_response_datetime(last_event.end_datetime),
918
            'duration': (last_event.end_datetime - last_event.start_datetime).seconds // 60,
919
            'agenda': {'label': first_event.agenda.label, 'slug': first_event.agenda.slug},
920
            'api': {
921
                'cancel_url': self.request.build_absolute_uri(
922
                    reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.pk})
923
                ),
924
                'ics_url': self.request.build_absolute_uri(
925
                    reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.pk})
926
                ),
927
            },
928
        }
929
        if to_cancel_booking:
930
            response['cancelled_booking_id'] = cancelled_booking_id
931
        if available_desk:
932
            response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
933

  
934
        return Response(response)
935

  
936

  
937
class SingleFillSlotsMixin(object):
938
    def get_slots(self):
939
        return self.validate_slots([self.kwargs['event_identifier']])
940

  
941
    def _post(self, payload):
942
        slots = self.get_slots()
943
        return self.fillslots(payload=payload, slots=slots)
944

  
945

  
946
class MultipleFillSlotsMixin(object):
947
    def get_slots(self, payload):
948
        return self.validate_slots(payload.get('slots') or [])
949

  
950
    def _post(self, payload):
951
        slots = self.get_slots(payload=payload)
952
        return self.fillslots(payload=payload, slots=slots, multiple_booking=True)
953

  
954

  
955
class FillSlots(MultipleFillSlotsMixin, EventFillSlotsMixin, APIView):
956
    serializer_class = serializers.SlotsSerializer
957

  
958

  
959
fillslots = FillSlots.as_view()
960

  
961

  
962
class MeetingsFillSlots(MultipleFillSlotsMixin, MeetingsFillSlotsMixin, APIView):
963
    serializer_class = serializers.MeetingsSlotsSerializer
964

  
965

  
966
meetings_fillslots = MeetingsFillSlots.as_view()
967

  
968

  
969
class FillSlot(SingleFillSlotsMixin, EventFillSlotsMixin, APIView):
970
    serializer_class = serializers.SlotSerializer
971

  
972

  
973
fillslot = FillSlot.as_view()
974

  
975

  
976
class MeetingsFillSlot(SingleFillSlotsMixin, MeetingsFillSlotsMixin, APIView):
977
    serializer_class = serializers.MeetingsSlotSerializer
900 978

  
901 979

  
902
fillslot = Fillslot.as_view()
980
meetings_fillslot = MeetingsFillSlot.as_view()
903 981

  
904 982

  
905 983
class BookingAPI(APIView):
tests/test_api.py
169 169
                'api': {
170 170
                    'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % meetings_agenda.slug,
171 171
                    'desks_url': 'http://testserver/api/agenda/%s/desks/' % meetings_agenda.slug,
172
                    'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug,
172
                    'fillslots_url': 'http://testserver/api/agenda/%s/meetings/fillslots/'
173
                    % meetings_agenda.slug,
173 174
                },
174 175
            },
175 176
            {
......
182 183
                'api': {
183 184
                    'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_agenda.slug,
184 185
                    'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_agenda.slug,
185
                    'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_agenda.slug,
186
                    'fillslots_url': 'http://testserver/api/agenda/%s/meetings/fillslots/'
187
                    % virtual_agenda.slug,
186 188
                },
187 189
            },
188 190
        ]
......
390 392

  
391 393
    default_desk, _ = Desk.objects.get_or_create(agenda=meetings_agenda, slug='desk-1')
392 394
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
393
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug)
394 395

  
395 396
    # test with short time periods
396 397
    TimePeriod.objects.filter(desk=default_desk).delete()
......
420 421
    assert resp.json['err_class'] == 'no more desk available'
421 422
    assert resp.json['err_desc'] == 'no more desk available'
422 423
    # booking the two slots fails too
423
    fillslots_url = '/api/agenda/%s/fillslots/' % meeting_type.agenda.slug
424
    fillslots_url = '/api/agenda/%s/meetings/fillslots/' % meeting_type.agenda.slug
424 425
    resp = app.post(fillslots_url, params={'slots': two_slots})
425 426
    assert resp.json['err'] == 1
426 427
    assert resp.json['reason'] == 'no more desk available'  # legacy
......
609 610
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
610 611
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
611 612
    event = resp.json['data'][2]
612
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda_id, event['id']))
613
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda_id, event['id']))
613 614
    assert Booking.objects.count() == 5
614 615
    assert 'ics_url' in resp.json['api']
615 616
    booking = Booking.objects.get(id=resp.json['booking_id'])
......
772 773
    event_id = resp.json['data'][2]['id']
773 774
    assert urlparse.urlparse(
774 775
        resp.json['data'][2]['api']['fillslot_url']
775
    ).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
776
    ).path == '/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda.slug, event_id)
776 777

  
777 778
    app.authorization = ('Basic', ('john.doe', 'password'))
778 779

  
779 780
    # verify malformed event_pk returns a 400
780
    resp_booking = app.post('/api/agenda/%s/fillslot/None/' % agenda_id, status=400)
781
    resp_booking = app.post('/api/agenda/%s/meetings/fillslot/None/' % agenda_id, status=400)
781 782
    assert resp_booking.json['err'] == 1
782 783

  
783 784
    # make a booking
784
    resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
785
    resp_booking = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
785 786
    assert Booking.objects.count() == 1
786 787
    assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime).strftime(
787 788
        '%Y-%m-%d %H:%M:%S'
......
795 796
    assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 1
796 797

  
797 798
    # try booking the same timeslot
798
    resp2 = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
799
    resp2 = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
799 800
    assert resp2.json['err'] == 1
800 801
    assert resp2.json['reason'] == 'no more desk available'  # legacy
801 802
    assert resp2.json['err_class'] == 'no more desk available'
......
803 804

  
804 805
    # try booking another timeslot
805 806
    event_id = resp.json['data'][3]['id']
806
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
807
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
807 808
    assert resp.json['err'] == 0
808 809
    assert Booking.objects.count() == 2
809 810

  
......
815 816
    slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
816 817

  
817 818
    app.authorization = ('Basic', ('john.doe', 'password'))
818
    resp_booking = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
819
    resp_booking = app.post('/api/agenda/%s/meetings/fillslots/' % agenda_id, params={'slots': slots})
819 820
    assert Booking.objects.count() == 2
820 821
    primary_booking = Booking.objects.filter(primary_booking__isnull=True).first()
821 822
    secondary_booking = Booking.objects.filter(primary_booking=primary_booking.id).first()
......
830 831
    assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 2
831 832

  
832 833
    # try booking the same timeslots
833
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
834
    resp2 = app.post('/api/agenda/%s/meetings/fillslots/' % agenda_id, params={'slots': slots})
834 835
    assert resp2.json['err'] == 1
835 836
    assert resp2.json['reason'] == 'no more desk available'  # legacy
836 837
    assert resp2.json['err_class'] == 'no more desk available'
......
838 839

  
839 840
    # try booking partially free timeslots (one free, one busy)
840 841
    nonfree_slots = [resp.json['data'][0]['id'], resp.json['data'][2]['id']]
841
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': nonfree_slots})
842
    resp2 = app.post('/api/agenda/%s/meetings/fillslots/' % agenda_id, params={'slots': nonfree_slots})
842 843
    assert resp2.json['err'] == 1
843 844
    assert resp2.json['reason'] == 'no more desk available'  # legacy
844 845
    assert resp2.json['err_class'] == 'no more desk available'
......
846 847

  
847 848
    # booking other free timeslots
848 849
    free_slots = [resp.json['data'][3]['id'], resp.json['data'][2]['id']]
849
    resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': free_slots})
850
    resp2 = app.post('/api/agenda/%s/meetings/fillslots/' % agenda_id, params={'slots': free_slots})
850 851
    assert resp2.json['err'] == 0
851 852
    cancel_url = resp2.json['api']['cancel_url']
852 853
    assert Booking.objects.count() == 4
......
860 861
    assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 2
861 862

  
862 863
    impossible_slots = ['1:2017-05-22-1130', '2:2017-05-22-1100']
863
    resp = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': impossible_slots}, status=400)
864
    resp = app.post(
865
        '/api/agenda/%s/meetings/fillslots/' % agenda_id, params={'slots': impossible_slots}, status=400
866
    )
864 867
    assert resp.json['err'] == 1
865 868
    assert resp.json['reason'] == 'all slots must have the same meeting type id (1)'  # legacy
866 869
    assert resp.json['err_class'] == 'all slots must have the same meeting type id (1)'
867 870
    assert resp.json['err_desc'] == 'all slots must have the same meeting type id (1)'
868 871

  
869 872
    unknown_slots = ['0:2017-05-22-1130']
870
    resp = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': unknown_slots}, status=400)
873
    resp = app.post(
874
        '/api/agenda/%s/meetings/fillslots/' % agenda_id, params={'slots': unknown_slots}, status=400
875
    )
871 876
    assert resp.json['err'] == 1
872 877
    assert resp.json['reason'] == 'invalid meeting type id: 0'  # legacy
873 878
    assert resp.json['err_class'] == 'invalid meeting type id: 0'
874 879
    assert resp.json['err_desc'] == 'invalid meeting type id: 0'
875 880
    unknown_slots = ['foobar:2017-05-22-1130']
876
    resp = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': unknown_slots}, status=400)
881
    resp = app.post(
882
        '/api/agenda/%s/meetings/fillslots/' % agenda_id, params={'slots': unknown_slots}, status=400
883
    )
877 884
    assert resp.json['err'] == 1
878 885
    assert resp.json['reason'] == 'invalid meeting type id: foobar'  # legacy
879 886
    assert resp.json['err_class'] == 'invalid meeting type id: foobar'
880 887
    assert resp.json['err_desc'] == 'invalid meeting type id: foobar'
881 888

  
882 889

  
890
def test_booking_api_meeting_with_extra_params_in_payload(app, user):
891
    agenda = Agenda.objects.create(kind='meetings', slug='slug')
892
    meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
893
    desk = Desk.objects.create(label='Desk', agenda=agenda)
894
    TimePeriod.objects.create(
895
        weekday=3, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=desk,
896
    )
897

  
898
    app.authorization = ('Basic', ('john.doe', 'password'))
899
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.pk)
900
    event_id = resp.json['data'][2]['id']
901
    # count is a param of events fillslot endpoints
902
    app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda.slug, event_id), params={'count': 42})
903
    assert Booking.objects.count() == 1
904
    booking = Booking.objects.latest('pk')
905
    booking.cancel()
906

  
907
    # count is a param of events fillslot endpoints
908
    resp = app.post(
909
        '/api/agenda/%s/meetings/fillslots/' % agenda.slug, params={'slots': [event_id], 'count': 42}
910
    )
911
    assert Booking.objects.count() == 2
912

  
913

  
883 914
def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user):
884 915
    meetings_agenda.maximal_booking_delay = 365
885 916
    meetings_agenda.save()
......
892 923
    assert event_id[-4:] == resp.json['data'][2 * 18]['id'][-4:]
893 924
    assert urlparse.urlparse(
894 925
        resp.json['data'][event_index]['api']['fillslot_url']
895
    ).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
926
    ).path == '/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda.slug, event_id)
896 927

  
897 928
    app.authorization = ('Basic', ('john.doe', 'password'))
898
    resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
929
    resp_booking = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
899 930
    assert Booking.objects.count() == 1
900 931
    assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime).strftime(
901 932
        '%Y-%m-%d %H:%M:%S'
......
920 951
    event_id = resp.json['data'][0]['id']
921 952

  
922 953
    app.authorization = ('Basic', ('john.doe', 'password'))
923
    app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
954
    app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
924 955
    assert Booking.objects.count() == 1
925 956

  
926 957
    # the longer event at the same time shouldn't be available anymore
......
946 977
    event_id = resp.json['data'][0]['id']
947 978

  
948 979
    app.authorization = ('Basic', ('john.doe', 'password'))
949
    app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
980
    app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
950 981
    assert Booking.objects.count() == 1
951 982

  
952 983
    # this should have removed two short events
......
959 990
    # book another long event
960 991
    event_id = resp.json['data'][10]['id']
961 992
    app.authorization = ('Basic', ('john.doe', 'password'))
962
    app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
993
    app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
963 994
    assert Booking.objects.count() == 2
964 995

  
965 996
    resp_short2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_2.id)
......
1011 1042
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
1012 1043
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.pk)
1013 1044
    event_id = resp.json['data'][2]['id']
1014
    resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.pk, event_id))
1045
    resp = app.post_json('/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda.pk, event_id))
1015 1046
    assert resp.json['err'] == 0
1016 1047
    assert 'places' not in resp.json
1017 1048

  
......
1228 1259
    event_id = resp.json['data'][2]['id']
1229 1260

  
1230 1261
    app.authorization = ('Basic', ('john.doe', 'password'))
1231
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
1262
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
1232 1263
    assert Booking.objects.count() == 1
1233 1264

  
1234 1265
    booking_id = resp.json['booking_id']
......
1239 1270
    assert len(resp.json['data']) == nb_events
1240 1271

  
1241 1272
    # book the same time slot
1242
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
1273
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
1243 1274
    assert resp.json['err'] == 0
1244 1275
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
1245 1276
    assert len([x for x in resp.json['data'] if not x.get('disabled')]) == nb_events - 1
......
2032 2063
    meeting_type2 = MeetingType.objects.create(agenda=meetings_agenda, label='Tux kart', duration=60)
2033 2064
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type2.id)
2034 2065
    event_id = resp.json['data'][0]['id']
2035
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
2066
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
2036 2067
    cancel_url = resp.json['api']['cancel_url']
2037 2068

  
2038 2069
    # add a second desk
......
2047 2078

  
2048 2079
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
2049 2080
    event_id = resp.json['data'][1]['id']
2050
    resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
2081
    resp_booking = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
2051 2082
    assert Booking.objects.count() == 2
2052 2083
    assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime(
2053 2084
        '%Y-%m-%d %H:%M:%S'
......
2057 2088
    assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x['disabled']]) + 1
2058 2089

  
2059 2090
    # try booking the same timeslot and fail
2060
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
2091
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
2061 2092
    assert Booking.objects.count() == 2
2062 2093
    assert resp.json['err'] == 1
2063 2094
    assert resp.json['reason'] == 'no more desk available'  # legacy
......
2074 2105

  
2075 2106
    # capture number of queries made for fillslot endpoint with few bookings
2076 2107
    with CaptureQueriesContext(connection) as ctx:
2077
        resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
2108
        resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
2078 2109
        queries_count_fillslot1 = len(ctx.captured_queries)
2079 2110

  
2080 2111
    assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime(
......
2091 2122
    assert len(resp.json['data']) == len(resp2.json['data'])
2092 2123

  
2093 2124
    # try booking the same slot to make sure that cancelled booking has freed the slot
2094
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
2125
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
2095 2126
    assert Booking.objects.count() == 4
2096 2127
    assert Booking.objects.exclude(cancellation_datetime__isnull=True).count() == 2
2097 2128
    assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime(
......
2099 2130
    )
2100 2131

  
2101 2132
    # try booking the same timeslot again and fail
2102
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
2133
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, event_id))
2103 2134
    assert resp.json['err'] == 1
2104 2135
    assert resp.json['reason'] == 'no more desk available'  # legacy
2105 2136
    assert resp.json['err_class'] == 'no more desk available'
......
2142 2173
    start_free_places = get_free_places()
2143 2174

  
2144 2175
    # booking 3 slots on desk 1
2145
    fillslots_url = '/api/agenda/%s/fillslots/' % agenda_id
2176
    fillslots_url = '/api/agenda/%s/meetings/fillslots/' % agenda_id
2146 2177
    resp = app.post(fillslots_url, params={'slots': slots})
2147 2178
    assert resp.json['err'] == 0
2148 2179
    desk1 = resp.json['desk']['slug']
......
2171 2202
    assert get_free_places() == start_free_places
2172 2203

  
2173 2204
    # booking a single slot (must be on desk 1)
2174
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, slots[1]))
2205
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (agenda_id, slots[1]))
2175 2206
    assert resp.json['err'] == 0
2176 2207
    assert resp.json['desk']['slug'] == desk1
2177 2208
    cancel_url = resp.json['api']['cancel_url']
......
2251 2282
    datetime_url = '/api/agenda/meetings/%s/datetimes/' % meeting_type.id
2252 2283
    desk = Desk.objects.create(label='foo', agenda=agenda)
2253 2284
    for weekday in range(7):
2254
        time_period = TimePeriod.objects.create(
2285
        TimePeriod.objects.create(
2255 2286
            weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk
2256 2287
        )
2257 2288
    resp = app.get(datetime_url)
......
2476 2507
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id)
2477 2508
    event_id = resp.json['data'][0]['id']
2478 2509
    app.authorization = ('Basic', ('john.doe', 'password'))
2479
    app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id))
2510
    app.post('/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda.id, event_id))
2480 2511
    assert Booking.objects.count() == 1
2481 2512

  
2482 2513
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id)
2483 2514
    assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 55
2484 2515
    event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id']
2485
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id))
2516
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda.id, event_id))
2486 2517
    assert resp.json['datetime'] == '2017-05-22 10:30:00'
2487 2518
    assert Booking.objects.count() == 2
2488 2519

  
2489 2520
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id)
2490 2521
    event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id']
2491
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id))
2522
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda.id, event_id))
2492 2523
    assert resp.json['datetime'] == '2017-05-22 10:50:00'
2493 2524
    assert Booking.objects.count() == 3
2494 2525

  
2495 2526
    # create a gap
2496 2527
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id)
2497 2528
    event_id = [x for x in resp.json['data'] if not x.get('disabled')][1]['id']
2498
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id))
2529
    resp = app.post('/api/agenda/%s/meetings/fillslot/%s/' % (meetings_agenda.id, event_id))
2499 2530
    assert resp.json['datetime'] == '2017-05-22 11:30:00'
2500 2531
    assert Booking.objects.count() == 4
2501 2532

  
......
2603 2634
            'api': {
2604 2635
                'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_meetings_agenda.slug,
2605 2636
                'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_meetings_agenda.slug,
2606
                'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_meetings_agenda.slug,
2637
                'fillslots_url': 'http://testserver/api/agenda/%s/meetings/fillslots/'
2638
                % virtual_meetings_agenda.slug,
2607 2639
            },
2608 2640
        },
2609 2641
    }
2610
-