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 |
|