api: dispatch fillslots views in other views (#79300)
This commit is contained in:
parent
cdcb663f85
commit
848d014720
|
@ -1171,27 +1171,19 @@ class AgendaResourceList(APIView):
|
|||
agenda_resource_list = AgendaResourceList.as_view()
|
||||
|
||||
|
||||
class Fillslots(APIView):
|
||||
class EventsAgendaFillslots(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.FillSlotsSerializer
|
||||
|
||||
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
||||
if not settings.LEGACY_FILLSLOTS_ENABLED:
|
||||
def post(self, request, agenda, slots):
|
||||
if not settings.LEGACY_FILLSLOTS_ENABLED and slots is None:
|
||||
raise APIErrorBadRequest(N_('deprecated call'))
|
||||
|
||||
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots)
|
||||
|
||||
def fillslot(self, request, agenda_identifier=None, slots=None, format=None, retry=False):
|
||||
def fillslot(self, request, agenda, slots=None, retry=False):
|
||||
slots = slots or []
|
||||
multiple_booking = bool(not slots)
|
||||
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()
|
||||
|
||||
known_body_params = set(request.query_params).intersection(
|
||||
{'label', 'user_name', 'backoffice_url', 'user_display_label'}
|
||||
|
@ -1440,7 +1432,7 @@ class Fillslots(APIView):
|
|||
# of fillslot().
|
||||
if retry:
|
||||
raise APIError(N_('no more desk available'))
|
||||
return self.fillslot(request, agenda_identifier=agenda_identifier, slots=slots, retry=True)
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots, retry=True)
|
||||
raise
|
||||
|
||||
response = {
|
||||
|
@ -1507,18 +1499,378 @@ class Fillslots(APIView):
|
|||
return Response(response)
|
||||
|
||||
|
||||
class EventsAgendaFillslot(EventsAgendaFillslots):
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
|
||||
class MeetingsAgendaFillslots(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.FillSlotsSerializer
|
||||
|
||||
def post(self, request, agenda, slots):
|
||||
if not settings.LEGACY_FILLSLOTS_ENABLED and slots is None:
|
||||
raise APIErrorBadRequest(N_('deprecated call'))
|
||||
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots)
|
||||
|
||||
def fillslot(self, request, agenda, slots=None, retry=False):
|
||||
slots = slots or []
|
||||
multiple_booking = bool(not slots)
|
||||
|
||||
known_body_params = set(request.query_params).intersection(
|
||||
{'label', 'user_name', 'backoffice_url', 'user_display_label'}
|
||||
)
|
||||
if known_body_params:
|
||||
params = ', '.join(sorted(list(known_body_params)))
|
||||
raise APIErrorBadRequest(
|
||||
N_('parameters "%s" must be included in request body, not query'), params
|
||||
)
|
||||
|
||||
serializer = self.serializer_class(data=request.data, partial=True)
|
||||
if not serializer.is_valid():
|
||||
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
|
||||
payload = serializer.validated_data
|
||||
|
||||
if 'slots' in payload:
|
||||
slots = payload['slots']
|
||||
|
||||
if 'count' in payload:
|
||||
places_count = payload['count']
|
||||
elif 'count' in request.query_params:
|
||||
# legacy: count in the query string
|
||||
try:
|
||||
places_count = int(request.query_params['count'])
|
||||
except ValueError:
|
||||
raise APIErrorBadRequest(N_('invalid value for count (%s)'), request.query_params['count'])
|
||||
else:
|
||||
places_count = 1
|
||||
|
||||
if places_count <= 0:
|
||||
raise APIErrorBadRequest(N_('count cannot be less than or equal to zero'))
|
||||
|
||||
to_cancel_booking = None
|
||||
cancel_booking_id = None
|
||||
if payload.get('cancel_booking_id'):
|
||||
try:
|
||||
cancel_booking_id = int(payload.get('cancel_booking_id'))
|
||||
except (ValueError, TypeError):
|
||||
raise APIErrorBadRequest(N_('cancel_booking_id is not an integer'))
|
||||
|
||||
if cancel_booking_id is not None:
|
||||
cancel_error = None
|
||||
try:
|
||||
to_cancel_booking = Booking.objects.get(pk=cancel_booking_id)
|
||||
if to_cancel_booking.cancellation_datetime:
|
||||
cancel_error = N_('cancel booking: booking already cancelled')
|
||||
else:
|
||||
to_cancel_places_count = (
|
||||
to_cancel_booking.secondary_booking_set.filter(event=to_cancel_booking.event).count()
|
||||
+ 1
|
||||
)
|
||||
if places_count != to_cancel_places_count:
|
||||
cancel_error = N_('cancel booking: count is different')
|
||||
except Booking.DoesNotExist:
|
||||
cancel_error = N_('cancel booking: booking does no exist')
|
||||
|
||||
if cancel_error:
|
||||
raise APIError(N_(cancel_error))
|
||||
|
||||
extra_data = get_extra_data(request, serializer.validated_data)
|
||||
|
||||
available_desk = None
|
||||
color = None
|
||||
user_external_id = payload.get('user_external_id') or None
|
||||
exclude_user = payload.get('exclude_user')
|
||||
|
||||
if agenda.accept_meetings():
|
||||
# slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
|
||||
# split them back to get both parts
|
||||
meeting_type_id = slots[0].split(':')[0]
|
||||
datetimes = set()
|
||||
for slot in slots:
|
||||
try:
|
||||
meeting_type_id_, datetime_str = slot.split(':')
|
||||
except ValueError:
|
||||
raise APIErrorBadRequest(N_('invalid slot: %s'), slot)
|
||||
if meeting_type_id_ != meeting_type_id:
|
||||
raise APIErrorBadRequest(
|
||||
N_('all slots must have the same meeting type id (%s)'), meeting_type_id
|
||||
)
|
||||
try:
|
||||
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
|
||||
except ValueError:
|
||||
raise APIErrorBadRequest(N_('bad datetime format: %s'), datetime_str)
|
||||
|
||||
resources = get_resources_from_request(request, agenda)
|
||||
|
||||
# get all free slots and separate them by desk
|
||||
try:
|
||||
try:
|
||||
meeting_type = agenda.get_meetingtype(slug=meeting_type_id)
|
||||
except MeetingType.DoesNotExist:
|
||||
# legacy access by id
|
||||
meeting_type = agenda.get_meetingtype(id_=meeting_type_id)
|
||||
except (MeetingType.DoesNotExist, ValueError):
|
||||
raise APIErrorBadRequest(N_('invalid meeting type id: %s'), meeting_type_id)
|
||||
all_slots = sorted(
|
||||
agenda.get_all_slots(
|
||||
meeting_type,
|
||||
resources=resources,
|
||||
user_external_id=user_external_id if exclude_user else None,
|
||||
start_datetime=min(datetimes),
|
||||
end_datetime=max(datetimes) + datetime.timedelta(minutes=meeting_type.duration),
|
||||
),
|
||||
key=lambda slot: slot.start_datetime,
|
||||
)
|
||||
|
||||
all_free_slots = [slot for slot in all_slots if not slot.full]
|
||||
datetimes_by_desk = collections.defaultdict(set)
|
||||
for slot in all_free_slots:
|
||||
datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
|
||||
|
||||
color_label = payload.get('use_color_for')
|
||||
if color_label:
|
||||
color = BookingColor.objects.get_or_create(label=color_label)[0]
|
||||
|
||||
available_desk = None
|
||||
|
||||
if agenda.kind == 'virtual':
|
||||
# Compute fill_rate by agenda/date
|
||||
fill_rates = collections.defaultdict(dict)
|
||||
for slot in all_slots:
|
||||
ref_date = slot.start_datetime.date()
|
||||
if ref_date not in fill_rates[slot.desk.agenda]:
|
||||
date_dict = fill_rates[slot.desk.agenda][ref_date] = {'free': 0, 'full': 0}
|
||||
else:
|
||||
date_dict = fill_rates[slot.desk.agenda][ref_date]
|
||||
if slot.full:
|
||||
date_dict['full'] += 1
|
||||
else:
|
||||
date_dict['free'] += 1
|
||||
for dd in fill_rates.values():
|
||||
for date_dict in dd.values():
|
||||
date_dict['fill_rate'] = date_dict['full'] / (date_dict['full'] + date_dict['free'])
|
||||
|
||||
# select a desk on the agenda with min fill_rate on the given date
|
||||
for available_desk_id in sorted(datetimes_by_desk.keys()):
|
||||
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
|
||||
desk = Desk.objects.get(id=available_desk_id)
|
||||
if available_desk is None:
|
||||
available_desk = desk
|
||||
available_desk_rate = 0
|
||||
for dt in datetimes:
|
||||
available_desk_rate += fill_rates[available_desk.agenda][dt.date()][
|
||||
'fill_rate'
|
||||
]
|
||||
else:
|
||||
for dt in datetimes:
|
||||
desk_rate = 0
|
||||
for dt in datetimes:
|
||||
desk_rate += fill_rates[desk.agenda][dt.date()]['fill_rate']
|
||||
if desk_rate < available_desk_rate:
|
||||
available_desk = desk
|
||||
available_desk_rate = desk_rate
|
||||
|
||||
else:
|
||||
# meeting agenda
|
||||
# search first desk where all requested slots are free
|
||||
for available_desk_id in sorted(datetimes_by_desk.keys()):
|
||||
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
|
||||
available_desk = Desk.objects.get(id=available_desk_id)
|
||||
break
|
||||
|
||||
if available_desk is None:
|
||||
raise APIError(N_('no more desk available'))
|
||||
|
||||
# all datetimes are free, book them in order
|
||||
datetimes = list(datetimes)
|
||||
datetimes.sort()
|
||||
|
||||
# get a real meeting_type for virtual agenda
|
||||
if agenda.kind == 'virtual':
|
||||
meeting_type = MeetingType.objects.get(agenda=available_desk.agenda, slug=meeting_type.slug)
|
||||
|
||||
# booking requires real Event objects (not lazy Timeslots);
|
||||
# create them now, with data from the slots and the desk we found.
|
||||
events = []
|
||||
for start_datetime in datetimes:
|
||||
events.append(
|
||||
Event(
|
||||
agenda=available_desk.agenda,
|
||||
slug=str(uuid.uuid4()), # set slug to avoid queries during slug generation
|
||||
meeting_type=meeting_type,
|
||||
start_datetime=start_datetime,
|
||||
full=False,
|
||||
places=1,
|
||||
desk=available_desk,
|
||||
)
|
||||
)
|
||||
in_waiting_list = False
|
||||
else:
|
||||
events = get_events_from_slots(slots, request, agenda, payload)
|
||||
|
||||
# search free places. Switch to waiting list if necessary.
|
||||
in_waiting_list = False
|
||||
for event in events:
|
||||
if event.start_datetime > now():
|
||||
if payload.get('force_waiting_list') and not event.waiting_list_places:
|
||||
raise APIError(N_('no waiting list'))
|
||||
|
||||
if event.waiting_list_places:
|
||||
if (
|
||||
payload.get('force_waiting_list')
|
||||
or (event.booked_places + places_count) > event.places
|
||||
or event.booked_waiting_list_places
|
||||
):
|
||||
# if this is full or there are people waiting, put new bookings
|
||||
# in the waiting list.
|
||||
in_waiting_list = True
|
||||
if (event.booked_waiting_list_places + places_count) > event.waiting_list_places:
|
||||
raise APIError(N_('sold out'))
|
||||
else:
|
||||
if (event.booked_places + places_count) > event.places:
|
||||
raise APIError(N_('sold out'))
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if to_cancel_booking:
|
||||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
|
||||
# now we have a list of events, book them.
|
||||
primary_booking = None
|
||||
for event in events:
|
||||
if agenda.accept_meetings():
|
||||
event.save()
|
||||
if resources:
|
||||
event.resources.add(*resources)
|
||||
for dummy in range(places_count):
|
||||
new_booking = make_booking(
|
||||
event, payload, extra_data, primary_booking, in_waiting_list, color
|
||||
)
|
||||
new_booking.save()
|
||||
if primary_booking is None:
|
||||
primary_booking = new_booking
|
||||
except IntegrityError as e:
|
||||
if 'tstzrange_constraint' in str(e):
|
||||
# "optimistic concurrency control", between our availability
|
||||
# check with get_all_slots() and now, new event can have been
|
||||
# created and conflict with the events we want to create, and
|
||||
# so we get an IntegrityError exception. In this case we
|
||||
# restart the fillslot() from the begginning to redo the
|
||||
# availability check and return a proper error to the client.
|
||||
#
|
||||
# To prevent looping, we raise an APIError during the second run
|
||||
# of fillslot().
|
||||
if retry:
|
||||
raise APIError(N_('no more desk available'))
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots, retry=True)
|
||||
raise
|
||||
|
||||
response = {
|
||||
'err': 0,
|
||||
'in_waiting_list': in_waiting_list,
|
||||
'booking_id': primary_booking.id,
|
||||
'datetime': format_response_datetime(events[0].start_datetime),
|
||||
'agenda': {
|
||||
'label': primary_booking.event.agenda.label,
|
||||
'slug': primary_booking.event.agenda.slug,
|
||||
},
|
||||
'api': {
|
||||
'booking_url': request.build_absolute_uri(
|
||||
reverse('api-booking', kwargs={'booking_pk': primary_booking.id})
|
||||
),
|
||||
'cancel_url': request.build_absolute_uri(
|
||||
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id})
|
||||
),
|
||||
'ics_url': request.build_absolute_uri(
|
||||
reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id})
|
||||
),
|
||||
'anonymize_url': request.build_absolute_uri(
|
||||
reverse('api-anonymize-booking', kwargs={'booking_pk': primary_booking.id})
|
||||
),
|
||||
},
|
||||
}
|
||||
if agenda.kind == 'events':
|
||||
response['api']['accept_url'] = request.build_absolute_uri(
|
||||
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
)
|
||||
response['api']['suspend_url'] = request.build_absolute_uri(
|
||||
reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
)
|
||||
if agenda.accept_meetings():
|
||||
response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
|
||||
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
|
||||
if available_desk:
|
||||
response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
|
||||
if to_cancel_booking:
|
||||
response['cancelled_booking_id'] = cancelled_booking_id
|
||||
if agenda.kind == 'events' and not multiple_booking:
|
||||
event = events[0]
|
||||
# event.full is not up to date, it might have been changed by previous new_booking.save().
|
||||
event.refresh_from_db()
|
||||
response['places'] = get_event_places(event)
|
||||
if event.end_datetime:
|
||||
response['end_datetime'] = format_response_datetime(event.end_datetime)
|
||||
else:
|
||||
response['end_datetime'] = None
|
||||
if agenda.kind == 'events' and multiple_booking:
|
||||
response['events'] = [
|
||||
{
|
||||
'slug': x.slug,
|
||||
'text': str(x),
|
||||
'datetime': format_response_datetime(x.start_datetime),
|
||||
'end_datetime': format_response_datetime(x.end_datetime) if x.end_datetime else None,
|
||||
'description': x.description,
|
||||
}
|
||||
for x in events
|
||||
]
|
||||
if agenda.kind == 'meetings':
|
||||
response['resources'] = [r.slug for r in resources]
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
class MeetingsAgendaFillslot(MeetingsAgendaFillslots):
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
|
||||
class Fillslots(APIView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
agenda_identifier = kwargs['agenda_identifier']
|
||||
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 kwargs.get('slots'):
|
||||
if agenda.accept_meetings():
|
||||
api_view = MeetingsAgendaFillslot()
|
||||
else:
|
||||
api_view = EventsAgendaFillslot()
|
||||
else:
|
||||
if agenda.accept_meetings():
|
||||
api_view = MeetingsAgendaFillslots()
|
||||
else:
|
||||
api_view = EventsAgendaFillslots()
|
||||
return api_view.dispatch(request=request, agenda=agenda, slots=kwargs.get('slots'))
|
||||
|
||||
|
||||
fillslots = Fillslots.as_view()
|
||||
|
||||
|
||||
class Fillslot(Fillslots):
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
||||
return self.fillslot(
|
||||
def dispatch(self, request, agenda_identifier=None, event_identifier=None):
|
||||
return super().dispatch(
|
||||
request=request,
|
||||
agenda_identifier=agenda_identifier,
|
||||
slots=[event_identifier], # fill a "list on one slot"
|
||||
format=format,
|
||||
)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue