From cf4d26c58fc542a35ad76f0bd6da965322e6891e Mon Sep 17 00:00:00 2001 From: Thomas NOEL Date: Wed, 4 Apr 2018 10:57:38 +0200 Subject: [PATCH] api: handle multiple slots booking (#16238) --- chrono/api/urls.py | 2 ++ chrono/api/views.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++- tests/test_api.py | 1 + 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/chrono/api/urls.py b/chrono/api/urls.py index da3371d..b1a7790 100644 --- a/chrono/api/urls.py +++ b/chrono/api/urls.py @@ -27,6 +27,8 @@ urlpatterns = [ views.fillslot, name='api-fillslot'), url(r'agenda/(?P[\w-]+)/status/(?P\w+)/$', views.slot_status, name='api-event-status'), + url(r'agenda/(?P[\w-]+)/fillslots/$', + views.fillslots, name='api-agenda-fillslots'), url(r'agenda/meetings/(?P[\w-]+)/datetimes/$', views.meeting_datetimes, name='api-agenda-meeting-datetimes-legacy'), diff --git a/chrono/api/views.py b/chrono/api/views.py index 3408a69..bd2bb8f 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -110,7 +110,10 @@ def get_agenda_detail(request, agenda): kwargs={'agenda_identifier': agenda.slug})), 'desks_url': request.build_absolute_uri( reverse('api-agenda-desks', - kwargs={'agenda_identifier': agenda.slug})) + kwargs={'agenda_identifier': agenda.slug})), + 'fillslots_url': request.build_absolute_uri( + reverse('api-agenda-fillslots', + kwargs={'agenda_identifier': agenda.slug})), } return agenda_detail @@ -408,6 +411,97 @@ class Fillslot(GenericAPIView): fillslot = Fillslot.as_view() +class Fillslots(GenericAPIView): + serializer_class = SlotSerializer + permission_classes = (permissions.IsAuthenticated,) + + def post(self, request, agenda_identifier=None, format=None): + try: + agenda = Agenda.objects.get(slug=agenda_identifier) + except Agenda.DoesNotExist: + try: + # legacy access by agenda id + agenda = Agenda.objects.get(id=int(agenda_identifier)) + except (ValueError, Agenda.DoesNotExist): + raise Http404() + if agenda.kind != 'meetings': + raise Http404('agenda found, but it was not a meetings agenda') + + slots = request.data.get('slots') + if not slots: + return Response({'err': 1, 'reason': 'missing slots'}) + if not isinstance(slots, list): + return Response({'err': 1, 'reason': 'slots must be a list'}) + + # slots came from fake event_pk (meeting_type:start_datetime) + meeting_type_id = slots[0].split(':')[0] + datetimes = set() + for slot in slots: + meeting_type_id_, datetime_str = slots.split(':') + if meeting_type_id_ != meeting_type_id: + return Response({ + 'err': 1, + 'reason': 'all slots must have the same meeting type id (%s)' % meeting_type_id + }) + datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M'))) + + # get all free slots and separate them by desk + all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id)) + all_slots = [slot for slot in all_slots if not slot.full] + datetimes_by_desk = defaultdict(set) + for slot in all_slots: + datetimes_by_desk[slot.desk.id].add(slot.start_datetime) + + # search first desk where all requested slots are free + for available_desk_id in datetimes_by_desk: + if datetimes.issubset(datetimes_by_desk[available_desk_id]): + available_desk = Desk.objects.filter(id=available_desk_id)[0] + break + else: + return Response({'err': 1, 'reason': 'no more desk available'}) + + # all datetimes are free, book them in order + datetimes = list(datetimes) + datetimes.sort() + + # create bookings and relative events (booking requires a real Event object) + first_booking = None + for start_datetime in datetimes: + event = Event.objects.create(agenda=agenda, + meeting_type_id=meeting_type_id, + start_datetime=start_datetime, + full=False, places=1, + desk=available_desk) + booking = Booking(event_id=event_pk, extra_data=request.data) + for attr in ('label', 'user_name', 'backoffice_url'): + if isinstance(request.data.get(attr), basestring): + setattr(booking, attr, request.data.get(attr)) + if first_booking is not None: + additional_booking.primary_booking = first_booking + booking.save() + if first_booking is None: + first_booking = booking + + response = { + 'err': 0, + 'in_waiting_list': 0, + 'booking_id': first_booking.id, + 'datetime': localtime(datetimes[0]), + 'end_datetime': localtime(event.end_datetime), # it's the last created one + 'desk': { + 'label': available_desk.label, + 'slug': available_desk.slug, + }, + 'api': { + 'cancel_url': request.build_absolute_uri( + reverse('api-cancel-booking', kwargs={'booking_pk': first_booking.id})) + } + } + + +fillslots = Fillslots.as_view() + + class BookingAPI(APIView): permission_classes = (permissions.IsAuthenticated,) diff --git a/tests/test_api.py b/tests/test_api.py index 5db8ed1..e814a1e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -105,6 +105,7 @@ def test_agendas_api(app, some_data, meetings_agenda): 'kind': 'meetings', 'api': {'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % meetings_agenda.slug, 'desks_url': 'http://testserver/api/agenda/%s/desks/' % meetings_agenda.slug, + 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug, }, }, {'text': 'Foo bar2', 'id': u'foo-bar2', 'kind': 'events', 'slug': 'foo-bar2', -- 2.16.3