2016-02-13 16:31:14 +01:00
|
|
|
# chrono - agendas system
|
|
|
|
# Copyright (C) 2016 Entr'ouvert
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
|
|
# under the terms of the GNU Affero General Public License as published
|
|
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
import collections
|
2016-09-11 11:31:29 +02:00
|
|
|
import datetime
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
import itertools
|
2020-06-23 15:15:26 +02:00
|
|
|
import uuid
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
|
2017-09-01 15:01:07 +02:00
|
|
|
|
2019-05-28 16:32:30 +02:00
|
|
|
from django.db import transaction
|
2020-08-31 14:37:43 +02:00
|
|
|
from django.db.models import Prefetch, Q
|
2018-07-17 10:51:58 +02:00
|
|
|
from django.http import Http404, HttpResponse
|
2016-07-20 13:22:58 +02:00
|
|
|
from django.shortcuts import get_object_or_404
|
2019-09-21 21:16:23 +02:00
|
|
|
from django.urls import reverse
|
2017-05-15 16:37:20 +02:00
|
|
|
from django.utils.dateparse import parse_date
|
2018-03-25 11:26:47 +02:00
|
|
|
from django.utils.encoding import force_text
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
from django.utils.formats import date_format
|
2017-04-25 18:37:36 +02:00
|
|
|
from django.utils.timezone import now, make_aware, localtime
|
2019-10-31 09:42:22 +01:00
|
|
|
from django.utils.translation import gettext_noop
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2016-02-13 16:31:14 +01:00
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
from rest_framework import permissions, serializers, status
|
2019-10-31 09:42:22 +01:00
|
|
|
|
2016-03-30 00:51:34 +02:00
|
|
|
from rest_framework.views import APIView
|
2016-02-13 16:31:14 +01:00
|
|
|
|
2019-10-31 09:42:22 +01:00
|
|
|
from chrono.api.utils import Response
|
2020-11-10 14:58:09 +01:00
|
|
|
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriodException, Desk, BookingColor
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
from ..interval import IntervalSet
|
2017-09-01 15:01:07 +02:00
|
|
|
|
|
|
|
|
2020-05-26 15:11:39 +02:00
|
|
|
class APIError(Exception):
|
|
|
|
err = 1
|
|
|
|
http_status = 200
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self.__dict__.update(kwargs)
|
|
|
|
super(APIError, self).__init__(*args)
|
|
|
|
|
|
|
|
def to_response(self):
|
|
|
|
data = {
|
|
|
|
'err': self.err,
|
|
|
|
'err_class': self.err_class,
|
|
|
|
'err_desc': force_text(self),
|
|
|
|
}
|
|
|
|
if hasattr(self, 'errors'):
|
|
|
|
data['errors'] = self.errors
|
|
|
|
return Response(data, status=self.http_status)
|
|
|
|
|
|
|
|
|
2019-05-16 12:56:22 +02:00
|
|
|
def format_response_datetime(dt):
|
|
|
|
return localtime(dt).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
|
|
|
2020-11-02 13:55:48 +01:00
|
|
|
def get_min_datetime(agenda, start_datetime=None):
|
2020-02-25 10:30:47 +01:00
|
|
|
if agenda.minimal_booking_delay is None:
|
2020-11-02 13:55:48 +01:00
|
|
|
return start_datetime
|
|
|
|
|
2020-11-12 15:26:49 +01:00
|
|
|
min_datetime = localtime(now()) + datetime.timedelta(days=agenda.minimal_booking_delay)
|
2020-11-02 13:55:48 +01:00
|
|
|
min_datetime = min_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
if start_datetime is None:
|
|
|
|
return min_datetime
|
|
|
|
return max(min_datetime, start_datetime)
|
2020-02-25 10:30:47 +01:00
|
|
|
|
|
|
|
|
2020-11-02 13:55:48 +01:00
|
|
|
def get_max_datetime(agenda, end_datetime=None):
|
2020-02-25 10:30:47 +01:00
|
|
|
if agenda.maximal_booking_delay is None:
|
2020-11-02 13:55:48 +01:00
|
|
|
return end_datetime
|
|
|
|
|
2020-11-12 15:26:49 +01:00
|
|
|
max_datetime = localtime(now()) + datetime.timedelta(days=agenda.maximal_booking_delay)
|
2017-09-06 19:24:15 +02:00
|
|
|
max_datetime = max_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
|
2020-11-02 13:55:48 +01:00
|
|
|
if end_datetime is None:
|
|
|
|
return max_datetime
|
|
|
|
return min(max_datetime, end_datetime)
|
2020-02-25 10:30:47 +01:00
|
|
|
|
2017-09-06 19:24:15 +02:00
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
TimeSlot = collections.namedtuple('TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk'])
|
2017-09-01 15:01:07 +02:00
|
|
|
|
2020-02-06 17:49:38 +01:00
|
|
|
|
2020-11-02 13:55:48 +01:00
|
|
|
def get_all_slots(
|
|
|
|
base_agenda, meeting_type, resources=None, unique=False, start_datetime=None, end_datetime=None
|
|
|
|
):
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
"""Get all occupation state of all possible slots for the given agenda (of
|
|
|
|
its real agendas for a virtual agenda) and the given meeting_type.
|
2020-12-29 10:42:33 +01:00
|
|
|
|
2020-05-26 11:51:40 +02:00
|
|
|
The process is done in four phases:
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
- first phase: aggregate time intervals, during which a meeting is impossible
|
|
|
|
due to TimePeriodException models, by desk in IntervalSet (compressed
|
|
|
|
and ordered list of intervals).
|
|
|
|
- second phase: aggregate time intervals by desk for already booked slots, again
|
|
|
|
to make IntervalSet,
|
2020-05-26 11:51:40 +02:00
|
|
|
- third phase: for a meetings agenda, if resources has to be booked,
|
|
|
|
aggregate time intervals for already booked resources, to make IntervalSet.
|
|
|
|
- fourth and last phase: generate time slots from each time period based
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
on the time period definition and on the desk's respective agenda real
|
|
|
|
min/max_datetime; for each time slot check its status in the exclusion
|
|
|
|
and bookings sets.
|
2020-05-26 11:51:40 +02:00
|
|
|
If it is excluded, ignore it completely.
|
|
|
|
It if is booked, report the slot as full.
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2020-05-26 11:51:40 +02:00
|
|
|
resources = resources or []
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
# virtual agendas have one constraint :
|
|
|
|
# all the real agendas MUST have the same meetingstypes, the consequence is
|
|
|
|
# that the base_meeting_duration for the virtual agenda is always the same
|
|
|
|
# as the base meeting duration of each real agenda.
|
|
|
|
base_meeting_duration = base_agenda.get_base_meeting_duration()
|
2020-11-02 13:55:48 +01:00
|
|
|
base_min_datetime = get_min_datetime(base_agenda, start_datetime)
|
|
|
|
base_max_datetime = get_max_datetime(base_agenda, end_datetime)
|
2017-08-24 14:15:18 +02:00
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
meeting_duration = meeting_type.duration
|
|
|
|
meeting_duration_td = datetime.timedelta(minutes=meeting_duration)
|
2020-02-06 17:49:38 +01:00
|
|
|
|
2020-05-21 10:39:28 +02:00
|
|
|
now_datetime = now()
|
|
|
|
base_date = now_datetime.date()
|
2020-05-26 11:51:40 +02:00
|
|
|
agendas = base_agenda.get_real_agendas()
|
2020-02-06 17:49:38 +01:00
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
# regroup agendas by their opening period
|
|
|
|
agenda_ids_by_min_max_datetimes = collections.defaultdict(set)
|
|
|
|
agenda_id_min_max_datetime = {}
|
2020-02-06 17:49:38 +01:00
|
|
|
for agenda in agendas:
|
2020-11-02 13:55:48 +01:00
|
|
|
used_min_datetime = base_min_datetime or get_min_datetime(agenda, start_datetime)
|
|
|
|
used_max_datetime = base_max_datetime or get_max_datetime(agenda, end_datetime)
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
agenda_ids_by_min_max_datetimes[(used_min_datetime, used_max_datetime)].add(agenda.id)
|
|
|
|
agenda_id_min_max_datetime[agenda.id] = (used_min_datetime, used_max_datetime)
|
|
|
|
|
|
|
|
# aggregate time period exceptions by desk as IntervalSet for fast querying
|
|
|
|
# 1. sort exceptions by start_datetime
|
|
|
|
# 2. group them by desk
|
|
|
|
# 3. convert each desk's list of exception to intervals then IntervalSet
|
|
|
|
desks_exceptions = {
|
|
|
|
time_period_desk: IntervalSet.from_ordered(
|
|
|
|
map(TimePeriodException.as_interval, time_period_exceptions)
|
|
|
|
)
|
|
|
|
for time_period_desk, time_period_exceptions in itertools.groupby(
|
|
|
|
TimePeriodException.objects.filter(desk__agenda__in=agendas).order_by(
|
2020-10-27 15:10:19 +01:00
|
|
|
'desk_id', 'start_datetime', 'end_datetime'
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
),
|
|
|
|
key=lambda time_period: time_period.desk,
|
|
|
|
)
|
|
|
|
}
|
2020-09-30 17:53:42 +02:00
|
|
|
|
|
|
|
# add exceptions from unavailability calendar
|
2020-10-15 14:44:06 +02:00
|
|
|
time_period_exception_queryset = (
|
|
|
|
TimePeriodException.objects.all()
|
|
|
|
.select_related('unavailability_calendar')
|
|
|
|
.prefetch_related(
|
|
|
|
Prefetch(
|
|
|
|
'unavailability_calendar__desks',
|
|
|
|
queryset=Desk.objects.filter(agenda__in=agendas),
|
|
|
|
to_attr='prefetched_desks',
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.filter(unavailability_calendar__desks__agenda__in=agendas)
|
|
|
|
.order_by('start_datetime', 'end_datetime')
|
|
|
|
)
|
|
|
|
for time_period_exception in time_period_exception_queryset:
|
|
|
|
# unavailability calendar can be used in all desks;
|
|
|
|
# ignore desks outside of current agenda(s)
|
|
|
|
for desk in time_period_exception.unavailability_calendar.prefetched_desks:
|
2020-09-30 17:53:42 +02:00
|
|
|
if desk not in desks_exceptions:
|
|
|
|
desks_exceptions[desk] = IntervalSet()
|
|
|
|
desks_exceptions[desk].add(
|
|
|
|
time_period_exception.start_datetime, time_period_exception.end_datetime
|
|
|
|
)
|
|
|
|
|
2020-05-26 11:51:40 +02:00
|
|
|
# compute reduced min/max_datetime windows by desks based on exceptions
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
desk_min_max_datetime = {}
|
|
|
|
for desk, desk_exception in desks_exceptions.items():
|
|
|
|
base = IntervalSet([agenda_id_min_max_datetime[desk.agenda_id]])
|
|
|
|
base = base - desk_exception
|
2020-05-20 09:19:41 +02:00
|
|
|
if not base:
|
|
|
|
# ignore this desk, exceptions cover all opening time
|
|
|
|
# use an empty interval (begin == end) for this
|
2020-05-21 10:39:28 +02:00
|
|
|
desk_min_max_datetime[desk] = (now_datetime, now_datetime)
|
2020-05-20 09:19:41 +02:00
|
|
|
continue
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
min_datetime = base.min().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
max_datetime = base.max()
|
|
|
|
start_of_day = max_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
# move to end of the day if max_datetime is not on a day boundary
|
|
|
|
if max_datetime != start_of_day:
|
|
|
|
max_datetime = start_of_day + datetime.timedelta(days=1)
|
|
|
|
desk_min_max_datetime[desk] = (min_datetime, max_datetime)
|
|
|
|
|
|
|
|
# aggregate already booked time intervals by desk
|
|
|
|
bookings = {}
|
|
|
|
for (used_min_datetime, used_max_datetime), agenda_ids in agenda_ids_by_min_max_datetimes.items():
|
|
|
|
booked_events = (
|
|
|
|
Event.objects.filter(
|
|
|
|
agenda__in=agenda_ids,
|
2020-02-25 10:30:47 +01:00
|
|
|
start_datetime__gte=used_min_datetime,
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
start_datetime__lte=used_max_datetime + meeting_duration_td,
|
2020-02-06 17:49:38 +01:00
|
|
|
)
|
|
|
|
.exclude(booking__cancellation_datetime__isnull=False)
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
# ordering is important for the later groupby, it works like sort | uniq
|
2020-06-30 18:13:59 +02:00
|
|
|
.order_by('desk_id', 'start_datetime', 'meeting_type__duration')
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
.values_list('desk_id', 'start_datetime', 'meeting_type__duration')
|
|
|
|
)
|
|
|
|
# compute exclusion set by desk from all bookings, using
|
|
|
|
# itertools.groupby() to group them by desk_id
|
|
|
|
bookings.update(
|
|
|
|
(
|
|
|
|
desk_id,
|
|
|
|
IntervalSet.from_ordered(
|
|
|
|
(event_start_datetime, event_start_datetime + datetime.timedelta(minutes=event_duration))
|
|
|
|
for desk_id, event_start_datetime, event_duration in values
|
|
|
|
),
|
|
|
|
)
|
|
|
|
for desk_id, values in itertools.groupby(booked_events, lambda be: be[0])
|
|
|
|
)
|
|
|
|
|
2020-05-26 11:51:40 +02:00
|
|
|
# aggregate already booked time intervals for resources
|
|
|
|
resources_bookings = IntervalSet()
|
|
|
|
if agenda.kind == 'meetings' and resources:
|
|
|
|
used_min_datetime, used_max_datetime = agenda_id_min_max_datetime[base_agenda.pk]
|
|
|
|
event_ids_queryset = Event.resources.through.objects.filter(
|
|
|
|
resource__in=[r.pk for r in resources]
|
|
|
|
).values('event')
|
|
|
|
booked_events = (
|
|
|
|
Event.objects.filter(
|
|
|
|
pk__in=event_ids_queryset,
|
|
|
|
start_datetime__gte=used_min_datetime,
|
|
|
|
start_datetime__lte=used_max_datetime + meeting_duration_td,
|
|
|
|
)
|
|
|
|
.exclude(booking__cancellation_datetime__isnull=False)
|
|
|
|
.order_by('start_datetime', 'meeting_type__duration')
|
|
|
|
.values_list('start_datetime', 'meeting_type__duration')
|
|
|
|
)
|
|
|
|
# compute exclusion set
|
|
|
|
resources_bookings = IntervalSet.from_ordered(
|
|
|
|
(event_start_datetime, event_start_datetime + datetime.timedelta(minutes=event_duration))
|
|
|
|
for event_start_datetime, event_duration in booked_events
|
|
|
|
)
|
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
unique_booked = {}
|
|
|
|
for time_period in base_agenda.get_effective_time_periods():
|
|
|
|
duration = (
|
|
|
|
datetime.datetime.combine(base_date, time_period.end_time)
|
|
|
|
- datetime.datetime.combine(base_date, time_period.start_time)
|
|
|
|
).seconds / 60
|
|
|
|
|
|
|
|
if duration < meeting_type.duration:
|
|
|
|
# skip time period that can't even hold a single meeting
|
|
|
|
continue
|
|
|
|
|
2020-08-27 16:46:13 +02:00
|
|
|
desks_by_min_max_datetime = collections.defaultdict(list)
|
|
|
|
for desk in time_period.desks:
|
|
|
|
min_max = desk_min_max_datetime.get(desk, agenda_id_min_max_datetime[desk.agenda_id])
|
|
|
|
desks_by_min_max_datetime[min_max].append(desk)
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
|
|
|
|
# aggregate agendas based on their real min/max_datetime :
|
|
|
|
# the get_time_slots() result is dependant upon these values, so even
|
|
|
|
# if we deduplicated a TimePeriod for some desks, if their respective
|
|
|
|
# agendas have different real min/max_datetime we must unduplicate them
|
|
|
|
# at time slot generation phase.
|
|
|
|
for (used_min_datetime, used_max_datetime), desks in desks_by_min_max_datetime.items():
|
|
|
|
for start_datetime in time_period.get_time_slots(
|
|
|
|
min_datetime=used_min_datetime,
|
|
|
|
max_datetime=used_max_datetime,
|
|
|
|
meeting_duration=meeting_duration,
|
|
|
|
base_duration=base_meeting_duration,
|
2020-02-06 17:49:38 +01:00
|
|
|
):
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
end_datetime = start_datetime + meeting_duration_td
|
|
|
|
timestamp = start_datetime.timestamp
|
2017-09-01 15:01:07 +02:00
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
# skip generating datetimes if we already know that this
|
|
|
|
# datetime is available
|
|
|
|
if unique and unique_booked.get(timestamp) is False:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for desk in sorted(desks, key=lambda desk: desk.label):
|
|
|
|
# ignore the slot for this desk, if it overlaps and exclusion period for this desk
|
|
|
|
excluded = desk in desks_exceptions and desks_exceptions[desk].overlaps(
|
|
|
|
start_datetime, end_datetime
|
|
|
|
)
|
|
|
|
if excluded:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# slot is full if an already booked event overlaps it
|
2020-05-26 11:51:40 +02:00
|
|
|
# check resources first
|
|
|
|
booked = resources_bookings.overlaps(start_datetime, end_datetime)
|
|
|
|
if not booked:
|
|
|
|
# then bookings if resources are free
|
|
|
|
booked = desk.id in bookings and bookings[desk.id].overlaps(
|
|
|
|
start_datetime, end_datetime
|
|
|
|
)
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
if unique and unique_booked.get(timestamp) is booked:
|
|
|
|
continue
|
|
|
|
unique_booked[timestamp] = booked
|
|
|
|
yield TimeSlot(
|
|
|
|
start_datetime=start_datetime, end_datetime=end_datetime, desk=desk, full=booked
|
|
|
|
)
|
|
|
|
if unique and not booked:
|
|
|
|
break
|
2016-06-19 21:23:23 +02:00
|
|
|
|
|
|
|
|
2020-06-12 16:12:58 +02:00
|
|
|
def get_agenda_detail(request, agenda, check_events=False):
|
2017-06-26 14:51:36 +02:00
|
|
|
agenda_detail = {
|
2017-10-07 11:38:06 +02:00
|
|
|
'id': agenda.slug,
|
|
|
|
'slug': agenda.slug, # kept for compatibility
|
2017-06-26 14:51:36 +02:00
|
|
|
'text': agenda.label,
|
|
|
|
'kind': agenda.kind,
|
2019-02-06 09:35:10 +01:00
|
|
|
'minimal_booking_delay': agenda.minimal_booking_delay,
|
|
|
|
'maximal_booking_delay': agenda.maximal_booking_delay,
|
2017-06-26 14:51:36 +02:00
|
|
|
}
|
|
|
|
|
2020-05-25 10:09:41 +02:00
|
|
|
if agenda.kind == 'meetings':
|
|
|
|
agenda_detail['resources'] = [
|
|
|
|
{'id': r.slug, 'text': r.label, 'description': r.description} for r in agenda.resources.all()
|
|
|
|
]
|
2017-06-26 14:51:36 +02:00
|
|
|
if agenda.kind == 'events':
|
|
|
|
agenda_detail['api'] = {
|
|
|
|
'datetimes_url': request.build_absolute_uri(
|
|
|
|
reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug})
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2017-06-26 14:51:36 +02:00
|
|
|
}
|
2020-06-12 16:12:58 +02:00
|
|
|
if check_events:
|
2020-07-09 10:17:40 +02:00
|
|
|
agenda_detail['opened_events_available'] = agenda.get_open_events().filter(full=False).exists()
|
2020-02-06 17:49:38 +01:00
|
|
|
elif agenda.accept_meetings():
|
2017-06-26 14:51:36 +02:00
|
|
|
agenda_detail['api'] = {
|
|
|
|
'meetings_url': request.build_absolute_uri(
|
2017-10-07 11:33:47 +02:00
|
|
|
reverse('api-agenda-meetings', kwargs={'agenda_identifier': agenda.slug})
|
|
|
|
),
|
|
|
|
'desks_url': request.build_absolute_uri(
|
2017-06-26 14:51:36 +02:00
|
|
|
reverse('api-agenda-desks', kwargs={'agenda_identifier': agenda.slug})
|
2019-12-16 16:21:24 +01:00
|
|
|
),
|
2017-06-26 14:51:36 +02:00
|
|
|
}
|
2018-04-04 19:54:34 +02:00
|
|
|
agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
|
|
|
|
reverse('api-agenda-fillslots', kwargs={'agenda_identifier': agenda.slug})
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2017-06-26 14:51:36 +02:00
|
|
|
|
|
|
|
return agenda_detail
|
|
|
|
|
|
|
|
|
2019-10-29 14:30:12 +01:00
|
|
|
def get_event_places(event):
|
2020-03-04 17:25:45 +01:00
|
|
|
available = event.places - event.booked_places
|
2019-10-29 14:30:12 +01:00
|
|
|
places = {
|
|
|
|
'total': event.places,
|
|
|
|
'reserved': event.booked_places,
|
2020-03-04 17:25:45 +01:00
|
|
|
'available': available,
|
|
|
|
'full': event.full,
|
|
|
|
'has_waiting_list': False,
|
2019-10-29 14:30:12 +01:00
|
|
|
}
|
|
|
|
if event.waiting_list_places:
|
2020-03-04 17:25:45 +01:00
|
|
|
places['has_waiting_list'] = True
|
2019-10-29 14:30:12 +01:00
|
|
|
places['waiting_list_total'] = event.waiting_list_places
|
|
|
|
places['waiting_list_reserved'] = event.waiting_list
|
|
|
|
places['waiting_list_available'] = event.waiting_list_places - event.waiting_list
|
2020-03-04 17:25:45 +01:00
|
|
|
places['waiting_list_activated'] = event.waiting_list > 0 or available <= 0
|
|
|
|
# 'waiting_list_activated' means next booking will go into the waiting list
|
|
|
|
|
2019-10-29 14:30:12 +01:00
|
|
|
return places
|
|
|
|
|
|
|
|
|
2020-12-03 16:14:30 +01:00
|
|
|
def get_event_detail(request, event, agenda=None):
|
|
|
|
agenda = agenda or event.agenda
|
|
|
|
return {
|
|
|
|
'id': event.slug,
|
|
|
|
'slug': event.slug, # kept for compatibility
|
|
|
|
'text': force_text(event),
|
|
|
|
'datetime': format_response_datetime(event.start_datetime),
|
|
|
|
'description': event.description,
|
|
|
|
'pricing': event.pricing,
|
|
|
|
'url': event.url,
|
|
|
|
'disabled': bool(event.full),
|
|
|
|
'api': {
|
|
|
|
'bookings_url': request.build_absolute_uri(
|
|
|
|
reverse(
|
|
|
|
'api-event-bookings',
|
|
|
|
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
|
|
|
|
)
|
|
|
|
),
|
|
|
|
'fillslot_url': request.build_absolute_uri(
|
|
|
|
reverse(
|
|
|
|
'api-fillslot',
|
|
|
|
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
|
|
|
|
)
|
|
|
|
),
|
|
|
|
'status_url': request.build_absolute_uri(
|
|
|
|
reverse(
|
|
|
|
'api-event-status',
|
|
|
|
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': event.slug},
|
|
|
|
)
|
|
|
|
),
|
|
|
|
},
|
|
|
|
'places': get_event_places(event),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-05-26 15:11:39 +02:00
|
|
|
def get_resources_from_request(request, agenda):
|
|
|
|
if agenda.kind != 'meetings' or 'resources' not in request.GET:
|
|
|
|
return []
|
|
|
|
resources_slugs = [s for s in request.GET['resources'].split(',') if s]
|
|
|
|
resources = list(agenda.resources.filter(slug__in=resources_slugs))
|
|
|
|
if len(resources) != len(resources_slugs):
|
|
|
|
unknown_slugs = set(resources_slugs) - set([r.slug for r in resources])
|
|
|
|
unknown_slugs = sorted(list(unknown_slugs))
|
|
|
|
raise APIError(
|
|
|
|
_('invalid resource: %s') % ', '.join(unknown_slugs),
|
|
|
|
err_class='invalid resource: %s' % ', '.join(unknown_slugs),
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
)
|
|
|
|
return resources
|
|
|
|
|
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
class Agendas(APIView):
|
2017-01-13 14:59:10 +01:00
|
|
|
permission_classes = ()
|
|
|
|
|
2017-06-10 15:52:40 +02:00
|
|
|
def get(self, request, format=None):
|
2020-05-25 10:09:41 +02:00
|
|
|
agendas_queryset = Agenda.objects.all().prefetch_related('resources').order_by('label')
|
2020-08-31 14:37:43 +02:00
|
|
|
|
2020-03-06 11:51:44 +01:00
|
|
|
if 'q' in request.GET:
|
|
|
|
if not request.GET['q']:
|
|
|
|
return Response({'data': []})
|
|
|
|
agendas_queryset = agendas_queryset.filter(slug__icontains=request.GET['q'])
|
2020-08-31 14:37:43 +02:00
|
|
|
|
|
|
|
with_open_events = request.GET.get('with_open_events') in ['1', 'true']
|
|
|
|
if with_open_events:
|
|
|
|
# return only events agenda
|
|
|
|
event_queryset = Event.objects.filter(
|
|
|
|
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
|
|
|
|
cancelled=False,
|
|
|
|
start_datetime__gte=localtime(now()),
|
|
|
|
).order_by()
|
|
|
|
agendas_queryset = agendas_queryset.filter(kind='events').prefetch_related(
|
|
|
|
Prefetch(
|
|
|
|
'event_set',
|
|
|
|
queryset=event_queryset,
|
|
|
|
to_attr='prefetched_events',
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
agendas = []
|
|
|
|
for agenda in agendas_queryset:
|
|
|
|
if with_open_events and not any(
|
|
|
|
not e.full for e in agenda.get_open_events(prefetched_queryset=True)
|
|
|
|
):
|
|
|
|
# exclude agendas without open events
|
|
|
|
continue
|
|
|
|
agendas.append(get_agenda_detail(request, agenda))
|
|
|
|
|
2017-06-10 15:52:40 +02:00
|
|
|
return Response({'data': agendas})
|
2016-06-19 21:23:23 +02:00
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-06-19 21:23:23 +02:00
|
|
|
agendas = Agendas.as_view()
|
2016-02-13 16:31:14 +01:00
|
|
|
|
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
class AgendaDetail(APIView):
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2017-06-26 14:51:36 +02:00
|
|
|
Retrieve an agenda instance.
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2017-06-26 14:51:36 +02:00
|
|
|
permission_classes = ()
|
|
|
|
|
|
|
|
def get(self, request, agenda_identifier):
|
|
|
|
agenda = get_object_or_404(Agenda, slug=agenda_identifier)
|
2020-06-12 16:12:58 +02:00
|
|
|
return Response({'data': get_agenda_detail(request, agenda, check_events=True)})
|
2017-06-26 14:51:36 +02:00
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2017-06-26 14:51:36 +02:00
|
|
|
agenda_detail = AgendaDetail.as_view()
|
|
|
|
|
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
class Datetimes(APIView):
|
2017-01-13 14:59:10 +01:00
|
|
|
permission_classes = ()
|
|
|
|
|
2016-10-28 17:52:21 +02:00
|
|
|
def get(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))
|
2017-06-09 20:03:21 +02:00
|
|
|
except (ValueError, Agenda.DoesNotExist):
|
2016-10-28 17:52:21 +02:00
|
|
|
raise Http404()
|
2016-09-11 11:31:29 +02:00
|
|
|
if agenda.kind != 'events':
|
2017-06-10 21:22:29 +02:00
|
|
|
raise Http404('agenda found, but it was not an events agenda')
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2020-11-13 09:03:24 +01:00
|
|
|
entries = Event.annotate_queryset(agenda.get_open_events())
|
2017-05-15 16:37:20 +02:00
|
|
|
|
|
|
|
if 'date_start' in request.GET:
|
2017-12-30 15:48:48 +01:00
|
|
|
entries = entries.filter(
|
|
|
|
start_datetime__gte=make_aware(
|
|
|
|
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
|
|
|
)
|
2016-09-11 19:16:15 +02:00
|
|
|
|
2017-05-15 16:37:20 +02:00
|
|
|
if 'date_end' in request.GET:
|
2017-12-30 15:48:48 +01:00
|
|
|
entries = entries.filter(
|
|
|
|
start_datetime__lt=make_aware(
|
|
|
|
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2020-12-03 16:14:30 +01:00
|
|
|
response = {'data': [get_event_detail(request, x, agenda=agenda) for x in entries]}
|
2016-02-13 16:31:14 +01:00
|
|
|
return Response(response)
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-02-13 16:31:14 +01:00
|
|
|
datetimes = Datetimes.as_view()
|
2016-02-13 16:52:04 +01:00
|
|
|
|
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
class MeetingDatetimes(APIView):
|
2017-01-13 14:59:10 +01:00
|
|
|
permission_classes = ()
|
|
|
|
|
2016-10-28 19:36:29 +02:00
|
|
|
def get(self, request, agenda_identifier=None, meeting_identifier=None, format=None):
|
|
|
|
try:
|
|
|
|
if agenda_identifier is None:
|
|
|
|
# legacy access by meeting id
|
2020-06-17 17:39:26 +02:00
|
|
|
meeting_type = MeetingType.objects.get(id=meeting_identifier, deleted=False)
|
2020-02-06 17:49:38 +01:00
|
|
|
agenda = meeting_type.agenda
|
2016-10-28 19:36:29 +02:00
|
|
|
else:
|
2020-02-06 17:49:38 +01:00
|
|
|
agenda = Agenda.objects.get(slug=agenda_identifier)
|
|
|
|
meeting_type = agenda.get_meetingtype(slug=meeting_identifier)
|
2016-10-28 19:36:29 +02:00
|
|
|
|
2020-02-06 17:49:38 +01:00
|
|
|
except (ValueError, MeetingType.DoesNotExist, Agenda.DoesNotExist):
|
|
|
|
raise Http404()
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2016-09-11 19:16:15 +02:00
|
|
|
now_datetime = now()
|
2017-09-01 15:01:07 +02:00
|
|
|
|
2020-05-26 15:11:39 +02:00
|
|
|
try:
|
|
|
|
resources = get_resources_from_request(request, agenda)
|
|
|
|
except APIError as e:
|
|
|
|
return e.to_response()
|
2020-05-26 11:51:40 +02:00
|
|
|
|
2020-11-02 13:55:48 +01:00
|
|
|
start_datetime = None
|
|
|
|
if 'date_start' in request.GET:
|
|
|
|
try:
|
|
|
|
start_datetime = make_aware(
|
|
|
|
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))
|
|
|
|
)
|
|
|
|
except TypeError as e:
|
|
|
|
raise APIError(
|
|
|
|
_('date_start format must be YYYY-MM-DD'),
|
|
|
|
err_class='date_start format must be YYYY-MM-DD',
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
)
|
|
|
|
|
|
|
|
end_datetime = None
|
|
|
|
if 'date_end' in request.GET:
|
|
|
|
try:
|
|
|
|
end_datetime = make_aware(
|
|
|
|
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
|
|
|
|
)
|
|
|
|
except TypeError as e:
|
|
|
|
raise APIError(
|
|
|
|
_('date_end format must be YYYY-MM-DD'),
|
|
|
|
err_class='date_end format must be YYYY-MM-DD',
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
)
|
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
# Generate an unique slot for each possible meeting [start_datetime,
|
|
|
|
# end_datetime] range.
|
|
|
|
# First use get_all_slots() to get each possible meeting by desk and
|
|
|
|
# its current status (full = booked, or not).
|
|
|
|
# Then order them by (start, end, full) where full is False for
|
|
|
|
# bookable slot, so bookable slot come first.
|
|
|
|
# Traverse them and remove duplicates, if a slot is bookable we will
|
|
|
|
# only see it (since it comes first), so it also remove "full/booked"
|
|
|
|
# slot from the list if there is still a bookable slot on a desk at the
|
|
|
|
# same time.
|
|
|
|
# The generator also remove slots starting before the current time.
|
|
|
|
def unique_slots():
|
|
|
|
last_slot = None
|
2020-11-02 13:55:48 +01:00
|
|
|
all_slots = list(
|
|
|
|
get_all_slots(
|
|
|
|
agenda,
|
|
|
|
meeting_type,
|
|
|
|
resources=resources,
|
|
|
|
unique=True,
|
|
|
|
start_datetime=start_datetime,
|
|
|
|
end_datetime=end_datetime,
|
|
|
|
)
|
|
|
|
)
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
for slot in sorted(all_slots, key=lambda slot: slot[:3]):
|
|
|
|
if slot.start_datetime < now_datetime:
|
|
|
|
continue
|
|
|
|
if last_slot and last_slot[:2] == slot[:2]:
|
|
|
|
continue
|
|
|
|
last_slot = slot
|
|
|
|
yield slot
|
|
|
|
|
|
|
|
generator_of_unique_slots = unique_slots()
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2018-04-04 10:47:16 +02:00
|
|
|
# create fillslot API URL as a template, to avoid expensive calls
|
|
|
|
# to request.build_absolute_uri()
|
2019-11-06 17:00:12 +01:00
|
|
|
fake_event_identifier = '__event_identifier__'
|
2017-08-10 09:25:14 +02:00
|
|
|
fillslot_url = request.build_absolute_uri(
|
|
|
|
reverse(
|
|
|
|
'api-fillslot',
|
2020-06-23 15:15:26 +02:00
|
|
|
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': fake_event_identifier},
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2017-08-10 09:25:14 +02:00
|
|
|
)
|
2020-06-05 15:16:11 +02:00
|
|
|
if resources:
|
|
|
|
fillslot_url += '?resources=%s' % ','.join(r.slug for r in resources)
|
2019-12-16 16:21:24 +01:00
|
|
|
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
def make_id(start_datetime, meeting_type):
|
|
|
|
"""Make virtual id for a slot, combining meeting_type.id and
|
|
|
|
iso-format of date and time.
|
|
|
|
!!! The datetime must always be in the local timezone and the local
|
|
|
|
timezone must not change if we want the id to be stable.
|
|
|
|
It MUST be a garanty of SharedTimePeriod.get_time_slots(),
|
|
|
|
!!!
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2020-09-24 09:10:41 +02:00
|
|
|
return '%s:%s' % (
|
|
|
|
meeting_type.slug,
|
|
|
|
start_datetime.strftime('%Y-%m-%d-%H%M'),
|
|
|
|
)
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
|
2017-01-20 10:50:07 +01:00
|
|
|
response = {
|
|
|
|
'data': [
|
2019-12-16 16:21:24 +01:00
|
|
|
{
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
'id': slot_id,
|
|
|
|
'datetime': format_response_datetime(slot.start_datetime),
|
|
|
|
'text': date_format(slot.start_datetime, format='DATETIME_FORMAT'),
|
|
|
|
'disabled': bool(slot.full),
|
2020-06-23 15:15:26 +02:00
|
|
|
'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, slot_id)},
|
2017-12-16 05:37:00 +01:00
|
|
|
}
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
for slot in generator_of_unique_slots
|
|
|
|
# we do not have the := operator, so we do that
|
|
|
|
for slot_id in [make_id(slot.start_datetime, meeting_type)]
|
2019-12-16 16:21:24 +01:00
|
|
|
]
|
2017-12-16 05:37:00 +01:00
|
|
|
}
|
2016-09-11 11:31:29 +02:00
|
|
|
return Response(response)
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
meeting_datetimes = MeetingDatetimes.as_view()
|
|
|
|
|
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
class MeetingList(APIView):
|
2017-06-10 16:04:32 +02:00
|
|
|
permission_classes = ()
|
|
|
|
|
|
|
|
def get(self, request, agenda_identifier=None, format=None):
|
|
|
|
try:
|
|
|
|
agenda = Agenda.objects.get(slug=agenda_identifier)
|
|
|
|
except Agenda.DoesNotExist:
|
|
|
|
raise Http404()
|
2020-02-06 17:49:38 +01:00
|
|
|
if not agenda.accept_meetings():
|
|
|
|
raise Http404('agenda found, but it does not accept meetings')
|
2017-06-10 16:04:32 +02:00
|
|
|
|
|
|
|
meeting_types = []
|
2020-02-06 17:49:38 +01:00
|
|
|
for meeting_type in agenda.iter_meetingtypes():
|
2017-06-10 16:04:32 +02:00
|
|
|
meeting_types.append(
|
|
|
|
{
|
|
|
|
'text': meeting_type.label,
|
|
|
|
'id': meeting_type.slug,
|
2018-07-20 20:37:08 +02:00
|
|
|
'duration': meeting_type.duration,
|
2017-06-10 16:04:32 +02:00
|
|
|
'api': {
|
|
|
|
'datetimes_url': request.build_absolute_uri(
|
|
|
|
reverse(
|
|
|
|
'api-agenda-meeting-datetimes',
|
|
|
|
kwargs={
|
|
|
|
'agenda_identifier': agenda.slug,
|
|
|
|
'meeting_identifier': meeting_type.slug,
|
2019-12-16 16:21:24 +01:00
|
|
|
},
|
|
|
|
)
|
2017-06-10 16:04:32 +02:00
|
|
|
),
|
2019-12-16 16:21:24 +01:00
|
|
|
},
|
2017-06-10 16:04:32 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
return Response({'data': meeting_types})
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2017-06-10 16:04:32 +02:00
|
|
|
meeting_list = MeetingList.as_view()
|
|
|
|
|
|
|
|
|
2020-06-17 17:50:02 +02:00
|
|
|
class MeetingInfo(APIView):
|
|
|
|
permission_classes = ()
|
|
|
|
|
|
|
|
def get(self, request, agenda_identifier=None, meeting_identifier=None, format=None):
|
|
|
|
try:
|
|
|
|
agenda = Agenda.objects.get(slug=agenda_identifier)
|
|
|
|
except Agenda.DoesNotExist:
|
|
|
|
raise Http404()
|
|
|
|
if not agenda.accept_meetings():
|
|
|
|
raise Http404('agenda found, but it does not accept meetings')
|
|
|
|
|
|
|
|
try:
|
|
|
|
meeting_type = agenda.get_meetingtype(slug=meeting_identifier)
|
|
|
|
except MeetingType.DoesNotExist:
|
|
|
|
raise Http404()
|
|
|
|
|
|
|
|
datetimes_url = request.build_absolute_uri(
|
|
|
|
reverse(
|
|
|
|
'api-agenda-meeting-datetimes',
|
|
|
|
kwargs={'agenda_identifier': agenda.slug, 'meeting_identifier': meeting_type.slug},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return Response(
|
|
|
|
{
|
|
|
|
'data': {
|
|
|
|
'text': meeting_type.label,
|
|
|
|
'id': meeting_type.slug,
|
|
|
|
'duration': meeting_type.duration,
|
|
|
|
'api': {'datetimes_url': datetimes_url},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
meeting_info = MeetingInfo.as_view()
|
|
|
|
|
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
class AgendaDeskList(APIView):
|
2017-10-07 11:33:47 +02:00
|
|
|
permission_classes = ()
|
|
|
|
|
|
|
|
def get(self, request, agenda_identifier=None, format=None):
|
|
|
|
try:
|
|
|
|
agenda = Agenda.objects.get(slug=agenda_identifier)
|
|
|
|
except Agenda.DoesNotExist:
|
|
|
|
raise Http404()
|
|
|
|
if agenda.kind != 'meetings':
|
|
|
|
raise Http404('agenda found, but it was not a meetings agenda')
|
|
|
|
|
|
|
|
desks = [{'id': x.slug, 'text': x.label} for x in agenda.desk_set.all()]
|
|
|
|
return Response({'data': desks})
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2017-10-07 11:33:47 +02:00
|
|
|
agenda_desk_list = AgendaDeskList.as_view()
|
|
|
|
|
|
|
|
|
2016-02-13 16:52:04 +01:00
|
|
|
class SlotSerializer(serializers.Serializer):
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2018-04-04 19:54:34 +02:00
|
|
|
payload to fill one slot. The slot (event id) is in the URL.
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2019-03-14 15:25:08 +01:00
|
|
|
label = serializers.CharField(max_length=250, allow_blank=True)
|
2020-05-11 16:32:49 +02:00
|
|
|
user_external_id = serializers.CharField(max_length=250, allow_blank=True)
|
2019-03-14 15:25:08 +01:00
|
|
|
user_name = serializers.CharField(max_length=250, allow_blank=True)
|
2019-03-14 14:13:05 +01:00
|
|
|
user_display_label = serializers.CharField(max_length=250, allow_blank=True)
|
2020-09-09 17:52:03 +02:00
|
|
|
user_email = serializers.CharField(max_length=250, allow_blank=True)
|
|
|
|
user_phone_number = serializers.CharField(max_length=16, allow_blank=True)
|
|
|
|
form_url = serializers.CharField(max_length=250, allow_blank=True)
|
2019-03-14 15:25:08 +01:00
|
|
|
backoffice_url = serializers.URLField(allow_blank=True)
|
2020-07-08 16:10:53 +02:00
|
|
|
cancel_callback_url = serializers.URLField(allow_blank=True)
|
2019-03-14 15:25:08 +01:00
|
|
|
count = serializers.IntegerField(min_value=1)
|
2019-06-11 09:56:03 +02:00
|
|
|
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
|
2020-03-03 13:43:00 +01:00
|
|
|
force_waiting_list = serializers.BooleanField(default=False)
|
2020-11-10 14:58:09 +01:00
|
|
|
use_color_for = serializers.CharField(max_length=250, allow_blank=True)
|
2018-04-04 19:54:34 +02:00
|
|
|
|
2016-02-13 16:52:04 +01:00
|
|
|
|
2019-12-10 15:28:25 +01:00
|
|
|
class StringOrListField(serializers.ListField):
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
if isinstance(data, str):
|
|
|
|
data = [s.strip() for s in data.split(',')]
|
|
|
|
return super(StringOrListField, self).to_internal_value(data)
|
|
|
|
|
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
class SlotsSerializer(SlotSerializer):
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2018-04-04 19:54:34 +02:00
|
|
|
payload to fill multiple slots: same as SlotSerializer, but the
|
|
|
|
slots list is in the payload.
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2020-12-17 14:27:57 +01:00
|
|
|
slots = StringOrListField(required=True, child=serializers.CharField(max_length=160, allow_blank=False))
|
2016-02-13 16:52:04 +01:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
|
|
|
|
class Fillslots(APIView):
|
2016-06-18 11:59:26 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
2018-04-04 19:54:34 +02:00
|
|
|
serializer_class = SlotsSerializer
|
2016-02-13 16:52:04 +01:00
|
|
|
|
2019-11-06 17:00:12 +01:00
|
|
|
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
2020-05-19 17:53:34 +02:00
|
|
|
try:
|
|
|
|
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
|
|
|
|
except APIError as e:
|
|
|
|
return e.to_response()
|
2018-04-04 19:54:34 +02:00
|
|
|
|
|
|
|
def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
|
2019-10-29 14:30:12 +01:00
|
|
|
multiple_booking = bool(not slots)
|
2016-10-28 17:52:21 +02:00
|
|
|
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))
|
2017-06-09 20:03:21 +02:00
|
|
|
except (ValueError, Agenda.DoesNotExist):
|
2016-10-28 17:52:21 +02:00
|
|
|
raise Http404()
|
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
serializer = self.serializer_class(data=request.data, partial=True)
|
2018-04-04 16:07:00 +02:00
|
|
|
if not serializer.is_valid():
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('invalid payload'),
|
|
|
|
err_class='invalid payload',
|
|
|
|
errors=serializer.errors,
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2018-04-04 16:07:00 +02:00
|
|
|
)
|
2018-04-04 19:54:34 +02:00
|
|
|
payload = serializer.validated_data
|
2018-04-04 16:07:00 +02:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
if 'slots' in payload:
|
|
|
|
slots = payload['slots']
|
|
|
|
if not slots:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('slots list cannot be empty'),
|
|
|
|
err_class='slots list cannot be empty',
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2018-04-04 19:54:34 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if 'count' in payload:
|
|
|
|
places_count = payload['count']
|
|
|
|
elif 'count' in request.query_params:
|
|
|
|
# legacy: count in the query string
|
2017-10-19 13:21:06 +02:00
|
|
|
try:
|
2018-04-04 19:54:34 +02:00
|
|
|
places_count = int(request.query_params['count'])
|
2017-10-19 13:21:06 +02:00
|
|
|
except ValueError:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('invalid value for count (%s)') % request.query_params['count'],
|
|
|
|
err_class='invalid value for count (%s)' % request.query_params['count'],
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2018-04-04 16:07:00 +02:00
|
|
|
)
|
2018-04-04 19:54:34 +02:00
|
|
|
else:
|
|
|
|
places_count = 1
|
|
|
|
|
2019-03-06 10:21:29 +01:00
|
|
|
if places_count <= 0:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('count cannot be less than or equal to zero'),
|
|
|
|
err_class='count cannot be less than or equal to zero',
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2019-03-06 10:21:29 +01:00
|
|
|
)
|
|
|
|
|
2019-05-28 16:32:30 +02:00
|
|
|
to_cancel_booking = None
|
2019-06-11 09:56:03 +02:00
|
|
|
cancel_booking_id = None
|
|
|
|
if payload.get('cancel_booking_id'):
|
|
|
|
try:
|
|
|
|
cancel_booking_id = int(payload.get('cancel_booking_id'))
|
|
|
|
except (ValueError, TypeError):
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('cancel_booking_id is not an integer'),
|
|
|
|
err_class='cancel_booking_id is not an integer',
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2019-06-11 09:56:03 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if cancel_booking_id is not None:
|
2019-05-28 16:32:30 +02:00
|
|
|
cancel_error = None
|
|
|
|
try:
|
2019-06-11 09:56:03 +02:00
|
|
|
to_cancel_booking = Booking.objects.get(pk=cancel_booking_id)
|
2019-05-28 16:32:30 +02:00
|
|
|
if to_cancel_booking.cancellation_datetime:
|
2019-10-31 09:42:22 +01:00
|
|
|
cancel_error = gettext_noop('cancel booking: booking already cancelled')
|
2019-05-28 16:32:30 +02:00
|
|
|
else:
|
2019-12-16 16:50:38 +01:00
|
|
|
to_cancel_places_count = (
|
|
|
|
to_cancel_booking.secondary_booking_set.filter(event=to_cancel_booking.event).count()
|
|
|
|
+ 1
|
|
|
|
)
|
2019-05-28 16:32:30 +02:00
|
|
|
if places_count != to_cancel_places_count:
|
2019-10-31 09:42:22 +01:00
|
|
|
cancel_error = gettext_noop('cancel booking: count is different')
|
2019-05-28 16:32:30 +02:00
|
|
|
except Booking.DoesNotExist:
|
2019-10-31 09:42:22 +01:00
|
|
|
cancel_error = gettext_noop('cancel booking: booking does no exist')
|
2019-05-28 16:32:30 +02:00
|
|
|
|
|
|
|
if cancel_error:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(_(cancel_error), err_class=cancel_error)
|
2019-05-28 16:32:30 +02:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
extra_data = {}
|
|
|
|
for k, v in request.data.items():
|
|
|
|
if k not in serializer.validated_data:
|
|
|
|
extra_data[k] = v
|
2017-06-11 12:42:10 +02:00
|
|
|
|
2017-09-26 16:17:05 +02:00
|
|
|
available_desk = None
|
2020-11-10 14:58:09 +01:00
|
|
|
color = None
|
2018-04-04 19:54:34 +02:00
|
|
|
|
2020-02-06 17:49:38 +01:00
|
|
|
if agenda.accept_meetings():
|
2018-04-04 19:54:34 +02:00
|
|
|
# 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:
|
2019-08-19 15:34:03 +02:00
|
|
|
try:
|
|
|
|
meeting_type_id_, datetime_str = slot.split(':')
|
|
|
|
except ValueError:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('invalid slot: %s') % slot,
|
|
|
|
err_class='invalid slot: %s' % slot,
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2019-08-19 15:34:03 +02:00
|
|
|
)
|
2018-04-04 19:54:34 +02:00
|
|
|
if meeting_type_id_ != meeting_type_id:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('all slots must have the same meeting type id (%s)') % meeting_type_id,
|
|
|
|
err_class='all slots must have the same meeting type id (%s)' % meeting_type_id,
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2018-04-04 19:54:34 +02:00
|
|
|
)
|
2020-11-09 09:22:35 +01:00
|
|
|
try:
|
|
|
|
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
|
|
|
|
except ValueError:
|
|
|
|
raise APIError(
|
|
|
|
_('bad datetime format: %s') % datetime_str,
|
|
|
|
err_class='bad datetime format: %s' % datetime_str,
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
)
|
2018-04-04 19:54:34 +02:00
|
|
|
|
2020-05-26 15:11:39 +02:00
|
|
|
try:
|
|
|
|
resources = get_resources_from_request(request, agenda)
|
|
|
|
except APIError as e:
|
|
|
|
return e.to_response()
|
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
# get all free slots and separate them by desk
|
2020-05-07 14:26:12 +02:00
|
|
|
try:
|
2020-09-24 09:10:41 +02:00
|
|
|
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)
|
2020-05-07 14:26:12 +02:00
|
|
|
except (MeetingType.DoesNotExist, ValueError):
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('invalid meeting type id: %s') % meeting_type_id,
|
|
|
|
err_class='invalid meeting type id: %s' % meeting_type_id,
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2020-05-07 14:26:12 +02:00
|
|
|
)
|
2020-09-24 09:10:41 +02:00
|
|
|
all_slots = sorted(
|
|
|
|
get_all_slots(agenda, meeting_type, resources=resources),
|
|
|
|
key=lambda slot: slot.start_datetime,
|
|
|
|
)
|
2020-05-07 14:26:12 +02:00
|
|
|
|
2020-02-24 15:02:42 +01:00
|
|
|
all_free_slots = [slot for slot in all_slots if not slot.full]
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
datetimes_by_desk = collections.defaultdict(set)
|
2020-02-24 15:02:42 +01:00
|
|
|
for slot in all_free_slots:
|
2018-04-04 19:54:34 +02:00
|
|
|
datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
|
|
|
|
|
2020-11-10 14:58:09 +01:00
|
|
|
color_label = payload.get('use_color_for')
|
|
|
|
if color_label:
|
|
|
|
color = BookingColor.objects.get_or_create(agenda=agenda, label=color_label)[0]
|
|
|
|
|
2020-02-24 15:02:42 +01:00
|
|
|
available_desk = None
|
|
|
|
|
|
|
|
if agenda.kind == 'virtual':
|
|
|
|
# Compute fill_rate by agenda/date
|
api: optimize get_all_slots() and around it (#42169)
Workflow in get_all_slots() is simplified :
* first we accumulate, for each desk, the set of time slots when a booking cannot
occur or is already booked,
* then we generate the list of possible time slots and match them to the
exclusion and already booked set.
Intervals is replaced by a simpler data-structure, IntervalSet, it does
not need to be a map, a simple set is enough.
Also :
* moved TimePeriod.get_effective_timeperiods() to the agenda level , it
deduplictes TimePeriod between desks and remove excluded TimePeriod for
virtual agendas.
* added a named-tuple WeekTime to represent a TimePeriod base unit, so
we can use them in IntervalSet easily (as they can be compared) to
compute the effective time periods,
* the fact that base_duration is unique for a given virtual agenda is
now accounted and stated everywhere,
* the fact that generated time slots must have time in the local
timezone for the API to work is now stated everywhere,
* In get_all_slots(), also :
* integrated the code of get_exceptions_by_desk() into get_all_slots()
to further reduce the number of SQL queries.
* used_min/max_datetime is reduced by the exclusion periods, and
effective time periods are grouped based on the used_min/max_datetime of
each agenda.
* pre-filter slots for uniqueness when generating available datetimes
(but for filling slot we still need exact availability information
for each desk)
2020-04-30 12:16:38 +02:00
|
|
|
fill_rates = collections.defaultdict(dict)
|
2020-02-24 15:02:42 +01:00
|
|
|
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
|
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
else:
|
2020-02-24 15:02:42 +01:00
|
|
|
# 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:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('no more desk available'),
|
|
|
|
err_class='no more desk available',
|
2019-10-31 09:42:22 +01:00
|
|
|
)
|
2017-09-01 15:01:07 +02:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
# all datetimes are free, book them in order
|
|
|
|
datetimes = list(datetimes)
|
|
|
|
datetimes.sort()
|
|
|
|
|
2020-02-06 17:49:38 +01:00
|
|
|
# get a real meeting_type for virtual agenda
|
|
|
|
if agenda.kind == 'virtual':
|
2020-09-24 09:10:41 +02:00
|
|
|
meeting_type = MeetingType.objects.get(agenda=available_desk.agenda, slug=meeting_type.slug)
|
2020-02-06 17:49:38 +01:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
# 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:
|
2020-05-26 15:11:39 +02:00
|
|
|
event = Event.objects.create(
|
|
|
|
agenda=available_desk.agenda,
|
2020-06-23 15:15:26 +02:00
|
|
|
slug=str(uuid.uuid4()), # set slug to avoid queries during slug generation
|
2020-09-24 09:10:41 +02:00
|
|
|
meeting_type=meeting_type,
|
2020-05-26 15:11:39 +02:00
|
|
|
start_datetime=start_datetime,
|
|
|
|
full=False,
|
|
|
|
places=1,
|
|
|
|
desk=available_desk,
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2020-05-26 15:11:39 +02:00
|
|
|
if resources:
|
|
|
|
event.resources.add(*resources)
|
|
|
|
events.append(event)
|
2018-04-04 19:54:34 +02:00
|
|
|
else:
|
2019-11-06 17:00:12 +01:00
|
|
|
try:
|
2020-06-23 15:15:26 +02:00
|
|
|
events = agenda.event_set.filter(id__in=[int(s) for s in slots]).order_by('start_datetime')
|
2019-11-06 17:00:12 +01:00
|
|
|
except ValueError:
|
2020-06-23 15:15:26 +02:00
|
|
|
events = agenda.event_set.filter(slug__in=slots).order_by('start_datetime')
|
2018-04-04 19:54:34 +02:00
|
|
|
|
2020-05-12 16:57:11 +02:00
|
|
|
for event in events:
|
|
|
|
if not event.in_bookable_period():
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(_('event not bookable'), err_class='event not bookable')
|
2020-07-09 12:46:13 +02:00
|
|
|
if event.cancelled:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(_('event is cancelled'), err_class='event is cancelled')
|
2020-05-12 16:57:11 +02:00
|
|
|
|
2019-12-11 11:47:21 +01:00
|
|
|
if not events.count():
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('unknown event identifiers or slugs'),
|
|
|
|
err_class='unknown event identifiers or slugs',
|
|
|
|
http_status=status.HTTP_400_BAD_REQUEST,
|
2019-12-11 11:47:21 +01:00
|
|
|
)
|
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
# search free places. Switch to waiting list if necessary.
|
|
|
|
in_waiting_list = False
|
|
|
|
for event in events:
|
2020-03-03 13:43:00 +01:00
|
|
|
if payload.get('force_waiting_list') and not event.waiting_list_places:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('no waiting list'),
|
|
|
|
err_class='no waiting list',
|
|
|
|
)
|
2020-03-03 13:43:00 +01:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
if event.waiting_list_places:
|
2020-03-03 13:43:00 +01:00
|
|
|
if (
|
|
|
|
payload.get('force_waiting_list')
|
|
|
|
or (event.booked_places + places_count) > event.places
|
|
|
|
or event.waiting_list
|
|
|
|
):
|
2018-04-04 19:54:34 +02:00
|
|
|
# if this is full or there are people waiting, put new bookings
|
|
|
|
# in the waiting list.
|
|
|
|
in_waiting_list = True
|
|
|
|
if (event.waiting_list + places_count) > event.waiting_list_places:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('sold out'),
|
|
|
|
err_class='sold out',
|
|
|
|
)
|
2018-04-04 19:54:34 +02:00
|
|
|
else:
|
|
|
|
if (event.booked_places + places_count) > event.places:
|
2020-05-19 17:53:34 +02:00
|
|
|
raise APIError(
|
|
|
|
_('sold out'),
|
|
|
|
err_class='sold out',
|
|
|
|
)
|
2017-07-10 18:38:44 +02:00
|
|
|
|
2019-05-28 16:32:30 +02:00
|
|
|
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:
|
|
|
|
for i in range(places_count):
|
|
|
|
new_booking = Booking(
|
|
|
|
event_id=event.id,
|
|
|
|
in_waiting_list=in_waiting_list,
|
|
|
|
label=payload.get('label', ''),
|
2020-05-11 16:32:49 +02:00
|
|
|
user_external_id=payload.get('user_external_id', ''),
|
2019-05-28 16:32:30 +02:00
|
|
|
user_name=payload.get('user_name', ''),
|
2020-09-09 17:52:03 +02:00
|
|
|
user_email=payload.get('user_email', ''),
|
|
|
|
user_phone_number=payload.get('user_phone_number', ''),
|
|
|
|
form_url=payload.get('form_url', ''),
|
2019-05-28 16:32:30 +02:00
|
|
|
backoffice_url=payload.get('backoffice_url', ''),
|
2020-07-08 16:10:53 +02:00
|
|
|
cancel_callback_url=payload.get('cancel_callback_url', ''),
|
2019-05-28 16:32:30 +02:00
|
|
|
user_display_label=payload.get('user_display_label', ''),
|
|
|
|
extra_data=extra_data,
|
2020-11-10 14:58:09 +01:00
|
|
|
color=color,
|
2019-05-28 16:32:30 +02:00
|
|
|
)
|
|
|
|
if primary_booking is not None:
|
|
|
|
new_booking.primary_booking = primary_booking
|
|
|
|
new_booking.save()
|
|
|
|
if primary_booking is None:
|
|
|
|
primary_booking = new_booking
|
2017-06-11 12:42:10 +02:00
|
|
|
|
2016-07-07 16:21:55 +02:00
|
|
|
response = {
|
|
|
|
'err': 0,
|
2018-04-04 19:54:34 +02:00
|
|
|
'in_waiting_list': in_waiting_list,
|
|
|
|
'booking_id': primary_booking.id,
|
2019-05-16 12:56:22 +02:00
|
|
|
'datetime': format_response_datetime(events[0].start_datetime),
|
2020-05-01 09:21:56 +02:00
|
|
|
'agenda': {
|
|
|
|
'label': primary_booking.event.agenda.label,
|
|
|
|
'slug': primary_booking.event.agenda.slug,
|
|
|
|
},
|
2017-05-02 16:52:50 +02:00
|
|
|
'api': {
|
2017-06-23 11:30:53 +02:00
|
|
|
'cancel_url': request.build_absolute_uri(
|
2018-07-17 10:51:58 +02:00
|
|
|
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})
|
|
|
|
),
|
2017-05-02 16:52:50 +02:00
|
|
|
},
|
2016-07-07 16:21:55 +02:00
|
|
|
}
|
2020-08-24 15:14:39 +02:00
|
|
|
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})
|
|
|
|
)
|
2020-02-06 17:49:38 +01:00
|
|
|
if agenda.accept_meetings():
|
2019-05-16 12:56:22 +02:00
|
|
|
response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
|
2018-11-13 19:36:05 +01:00
|
|
|
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
|
2017-09-26 16:17:05 +02:00
|
|
|
if available_desk:
|
|
|
|
response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
|
2019-05-28 16:32:30 +02:00
|
|
|
if to_cancel_booking:
|
|
|
|
response['cancelled_booking_id'] = cancelled_booking_id
|
2019-10-29 14:30:12 +01:00
|
|
|
if agenda.kind == 'events' and not multiple_booking:
|
|
|
|
event = events[0]
|
2020-03-04 17:25:45 +01:00
|
|
|
# event.full is not up to date, it might have been changed by previous new_booking.save().
|
|
|
|
event.refresh_from_db()
|
2019-10-29 14:30:12 +01:00
|
|
|
response['places'] = get_event_places(event)
|
2020-05-30 14:49:58 +02:00
|
|
|
if event.end_datetime:
|
|
|
|
response['end_datetime'] = format_response_datetime(event.end_datetime)
|
|
|
|
else:
|
|
|
|
response['end_datetime'] = None
|
2019-12-09 18:50:04 +01:00
|
|
|
if agenda.kind == 'events' and multiple_booking:
|
|
|
|
response['events'] = [
|
|
|
|
{
|
|
|
|
'slug': x.slug,
|
|
|
|
'text': str(x),
|
|
|
|
'datetime': format_response_datetime(x.start_datetime),
|
2020-05-30 14:49:58 +02:00
|
|
|
'end_datetime': format_response_datetime(x.end_datetime) if x.end_datetime else None,
|
2019-12-09 18:50:04 +01:00
|
|
|
'description': x.description,
|
|
|
|
}
|
|
|
|
for x in events
|
|
|
|
]
|
2020-05-26 15:11:39 +02:00
|
|
|
if agenda.kind == 'meetings':
|
|
|
|
response['resources'] = [r.slug for r in resources]
|
2017-05-02 16:52:50 +02:00
|
|
|
|
2016-02-13 16:52:04 +01:00
|
|
|
return Response(response)
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
fillslots = Fillslots.as_view()
|
|
|
|
|
|
|
|
|
|
|
|
class Fillslot(Fillslots):
|
|
|
|
serializer_class = SlotSerializer
|
|
|
|
|
2019-11-06 17:00:12 +01:00
|
|
|
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
2020-05-19 17:53:34 +02:00
|
|
|
try:
|
|
|
|
return self.fillslot(
|
|
|
|
request=request,
|
|
|
|
agenda_identifier=agenda_identifier,
|
|
|
|
slots=[event_identifier], # fill a "list on one slot"
|
|
|
|
format=format,
|
|
|
|
)
|
|
|
|
except APIError as e:
|
|
|
|
return e.to_response()
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2018-04-04 19:54:34 +02:00
|
|
|
|
2016-02-13 16:52:04 +01:00
|
|
|
fillslot = Fillslot.as_view()
|
2016-03-30 00:51:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
class BookingAPI(APIView):
|
2016-06-18 11:59:26 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
2016-03-30 00:51:34 +02:00
|
|
|
def initial(self, request, *args, **kwargs):
|
|
|
|
super(BookingAPI, self).initial(request, *args, **kwargs)
|
|
|
|
self.booking = Booking.objects.get(id=kwargs.get('booking_pk'), cancellation_datetime__isnull=True)
|
|
|
|
|
|
|
|
def delete(self, request, *args, **kwargs):
|
2016-06-23 19:31:12 +02:00
|
|
|
self.booking.cancel()
|
2016-03-30 00:51:34 +02:00
|
|
|
response = {'err': 0, 'booking_id': self.booking.id}
|
|
|
|
return Response(response)
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-03-30 00:51:34 +02:00
|
|
|
booking = BookingAPI.as_view()
|
2016-06-18 12:24:21 +02:00
|
|
|
|
|
|
|
|
2016-06-23 19:31:12 +02:00
|
|
|
class CancelBooking(APIView):
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2016-07-20 13:22:58 +02:00
|
|
|
Cancel a booking.
|
|
|
|
|
2020-03-05 09:43:17 +01:00
|
|
|
It will return error codes if the booking was cancelled before (code 1) or
|
|
|
|
if the booking is not primary (code 2).
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-06-23 19:31:12 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
|
|
|
def post(self, request, booking_pk=None, format=None):
|
2016-07-20 13:22:58 +02:00
|
|
|
booking = get_object_or_404(Booking, id=booking_pk)
|
|
|
|
if booking.cancellation_datetime:
|
2019-10-31 09:42:22 +01:00
|
|
|
response = {
|
|
|
|
'err': 1,
|
|
|
|
'err_class': 'already cancelled',
|
|
|
|
'err_desc': _('already cancelled'),
|
|
|
|
}
|
2016-07-20 13:22:58 +02:00
|
|
|
return Response(response)
|
2020-03-05 09:43:17 +01:00
|
|
|
if booking.primary_booking is not None:
|
|
|
|
response = {
|
|
|
|
'err': 2,
|
|
|
|
'err_class': 'secondary booking',
|
|
|
|
'err_desc': _('secondary booking'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
2016-06-23 19:31:12 +02:00
|
|
|
booking.cancel()
|
|
|
|
response = {'err': 0, 'booking_id': booking.id}
|
|
|
|
return Response(response)
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-06-23 19:31:12 +02:00
|
|
|
cancel_booking = CancelBooking.as_view()
|
|
|
|
|
|
|
|
|
2016-07-07 16:21:55 +02:00
|
|
|
class AcceptBooking(APIView):
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2016-07-20 13:22:58 +02:00
|
|
|
Accept a booking currently in the waiting list.
|
|
|
|
|
2020-03-05 09:43:17 +01:00
|
|
|
It will return error codes if the booking was cancelled before (code 1),
|
|
|
|
if the booking is not primary (code 2) or
|
|
|
|
if the booking was not in waiting list (code 3).
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-07-07 16:21:55 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
|
|
|
def post(self, request, booking_pk=None, format=None):
|
2020-08-24 15:14:39 +02:00
|
|
|
booking = get_object_or_404(Booking, id=booking_pk, event__agenda__kind='events')
|
2016-07-20 13:22:58 +02:00
|
|
|
if booking.cancellation_datetime:
|
2019-10-31 09:42:22 +01:00
|
|
|
response = {
|
|
|
|
'err': 1,
|
|
|
|
'err_class': 'booking is cancelled',
|
|
|
|
'err_desc': _('booking is cancelled'),
|
|
|
|
}
|
2016-07-20 13:22:58 +02:00
|
|
|
return Response(response)
|
2020-03-05 09:43:17 +01:00
|
|
|
if booking.primary_booking is not None:
|
2019-10-31 09:42:22 +01:00
|
|
|
response = {
|
|
|
|
'err': 2,
|
2020-03-05 09:43:17 +01:00
|
|
|
'err_class': 'secondary booking',
|
|
|
|
'err_desc': _('secondary booking'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
if not booking.in_waiting_list:
|
|
|
|
response = {
|
|
|
|
'err': 3,
|
2019-10-31 09:42:22 +01:00
|
|
|
'err_class': 'booking is not in waiting list',
|
|
|
|
'err_desc': _('booking is not in waiting list'),
|
|
|
|
}
|
2016-07-20 13:22:58 +02:00
|
|
|
return Response(response)
|
2016-07-07 16:21:55 +02:00
|
|
|
booking.accept()
|
2020-03-05 16:18:09 +01:00
|
|
|
event = booking.event
|
|
|
|
response = {
|
|
|
|
'err': 0,
|
|
|
|
'booking_id': booking.pk,
|
|
|
|
'overbooked_places': max(0, event.booked_places - event.places),
|
|
|
|
}
|
2016-07-07 16:21:55 +02:00
|
|
|
return Response(response)
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2016-07-07 16:21:55 +02:00
|
|
|
accept_booking = AcceptBooking.as_view()
|
|
|
|
|
|
|
|
|
2020-02-21 16:15:12 +01:00
|
|
|
class SuspendBooking(APIView):
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2020-02-21 16:15:12 +01:00
|
|
|
Suspend a accepted booking.
|
|
|
|
|
2020-03-05 09:43:17 +01:00
|
|
|
It will return error codes if the booking was cancelled before (code 1)
|
|
|
|
if the booking is not primary (code 2) or
|
|
|
|
if the booking is already in waiting list (code 3).
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2020-02-21 16:15:12 +01:00
|
|
|
|
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
|
|
|
def post(self, request, booking_pk=None, format=None):
|
2020-08-24 15:14:39 +02:00
|
|
|
booking = get_object_or_404(Booking, pk=booking_pk, event__agenda__kind='events')
|
2020-02-21 16:15:12 +01:00
|
|
|
if booking.cancellation_datetime:
|
|
|
|
response = {
|
|
|
|
'err': 1,
|
|
|
|
'err_class': 'booking is cancelled',
|
|
|
|
'err_desc': _('booking is cancelled'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
2020-03-05 09:43:17 +01:00
|
|
|
if booking.primary_booking is not None:
|
2020-02-21 16:15:12 +01:00
|
|
|
response = {
|
|
|
|
'err': 2,
|
2020-03-05 09:43:17 +01:00
|
|
|
'err_class': 'secondary booking',
|
|
|
|
'err_desc': _('secondary booking'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
if booking.in_waiting_list:
|
|
|
|
response = {
|
|
|
|
'err': 3,
|
2020-02-21 16:15:12 +01:00
|
|
|
'err_class': 'booking is already in waiting list',
|
|
|
|
'err_desc': _('booking is already in waiting list'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
booking.suspend()
|
|
|
|
response = {'err': 0, 'booking_id': booking.pk}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
|
|
|
|
suspend_booking = SuspendBooking.as_view()
|
|
|
|
|
|
|
|
|
2020-03-05 14:02:52 +01:00
|
|
|
class ResizeSerializer(serializers.Serializer):
|
|
|
|
count = serializers.IntegerField(min_value=1)
|
|
|
|
|
|
|
|
|
|
|
|
class ResizeBooking(APIView):
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2020-03-05 14:02:52 +01:00
|
|
|
Resize a booking.
|
|
|
|
|
|
|
|
It will return error codes if the booking was cancelled before (code 1)
|
|
|
|
if the booking is not primary (code 2)
|
|
|
|
if the event is sold out (code 3) or
|
|
|
|
if the booking is on multi events (code 4).
|
2020-12-29 10:42:33 +01:00
|
|
|
"""
|
2020-03-05 14:02:52 +01:00
|
|
|
|
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
serializer_class = ResizeSerializer
|
|
|
|
|
|
|
|
def post(self, request, booking_pk=None, format=None):
|
|
|
|
serializer = self.serializer_class(data=request.data)
|
|
|
|
if not serializer.is_valid():
|
|
|
|
return Response(
|
|
|
|
{
|
|
|
|
'err': 1,
|
|
|
|
'err_class': 'invalid payload',
|
|
|
|
'err_desc': _('invalid payload'),
|
|
|
|
'errors': serializer.errors,
|
|
|
|
},
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
)
|
|
|
|
payload = serializer.validated_data
|
|
|
|
|
2020-08-24 15:14:39 +02:00
|
|
|
booking = get_object_or_404(Booking, pk=booking_pk, event__agenda__kind='events')
|
2020-03-05 14:02:52 +01:00
|
|
|
event = booking.event
|
|
|
|
if booking.cancellation_datetime:
|
|
|
|
response = {
|
|
|
|
'err': 1,
|
|
|
|
'err_class': 'booking is cancelled',
|
|
|
|
'err_desc': _('booking is cancelled'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
if booking.primary_booking is not None:
|
|
|
|
response = {
|
|
|
|
'err': 2,
|
|
|
|
'err_class': 'secondary booking',
|
|
|
|
'err_desc': _('secondary booking'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
event_ids = set([event.pk])
|
2020-07-20 17:39:45 +02:00
|
|
|
in_waiting_list = set([booking.in_waiting_list])
|
2020-03-05 14:02:52 +01:00
|
|
|
secondary_bookings = booking.secondary_booking_set.all().order_by('-creation_datetime')
|
|
|
|
for secondary in secondary_bookings:
|
|
|
|
event_ids.add(secondary.event_id)
|
2020-07-20 17:39:45 +02:00
|
|
|
in_waiting_list.add(secondary.in_waiting_list)
|
2020-03-05 14:02:52 +01:00
|
|
|
if len(event_ids) > 1:
|
|
|
|
response = {
|
|
|
|
'err': 4,
|
|
|
|
'err_class': 'can not resize multi event booking',
|
|
|
|
'err_desc': _('can not resize multi event booking'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
2020-07-20 17:39:45 +02:00
|
|
|
if len(in_waiting_list) > 1:
|
|
|
|
response = {
|
|
|
|
'err': 5,
|
|
|
|
'err_class': 'can not resize booking: waiting list inconsistency',
|
|
|
|
'err_desc': _('can not resize booking: waiting list inconsistency'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
2020-03-05 14:02:52 +01:00
|
|
|
|
2020-07-20 17:39:45 +02:00
|
|
|
# total places for the event (in waiting or main list, depending on the primary booking location)
|
|
|
|
places = event.waiting_list_places if booking.in_waiting_list else event.places
|
|
|
|
# total booked places for the event (in waiting or main list, depending on the primary booking location)
|
|
|
|
booked_places = event.waiting_list if booking.in_waiting_list else event.booked_places
|
|
|
|
|
|
|
|
# places to book for this primary booking
|
|
|
|
primary_wanted_places = payload['count']
|
|
|
|
# already booked places for this primary booking
|
|
|
|
primary_booked_places = 1 + len(secondary_bookings)
|
|
|
|
|
|
|
|
if primary_booked_places > primary_wanted_places:
|
|
|
|
# it is always ok to decrease booking
|
|
|
|
return self.decrease(booking, secondary_bookings, primary_booked_places, primary_wanted_places)
|
|
|
|
if primary_booked_places == primary_wanted_places:
|
|
|
|
# it is always ok to do nothing
|
|
|
|
return self.success(booking)
|
|
|
|
|
|
|
|
# else, increase places if allowed
|
|
|
|
if booked_places - primary_booked_places + primary_wanted_places > places:
|
|
|
|
# oversized request
|
|
|
|
if booking.in_waiting_list:
|
|
|
|
# booking in waiting list: can not be overbooked
|
|
|
|
response = {
|
|
|
|
'err': 3,
|
|
|
|
'err_class': 'sold out',
|
|
|
|
'err_desc': _('sold out'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
if event.booked_places <= event.places:
|
|
|
|
# in main list and no overbooking for the moment: can not be overbooked
|
2020-03-05 14:02:52 +01:00
|
|
|
response = {
|
|
|
|
'err': 3,
|
|
|
|
'err_class': 'sold out',
|
|
|
|
'err_desc': _('sold out'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
2020-07-20 17:39:45 +02:00
|
|
|
return self.increase(booking, secondary_bookings, primary_booked_places, primary_wanted_places)
|
2020-03-05 14:02:52 +01:00
|
|
|
|
2020-07-20 17:39:45 +02:00
|
|
|
def increase(self, booking, secondary_bookings, primary_booked_places, primary_wanted_places):
|
2020-03-05 14:02:52 +01:00
|
|
|
with transaction.atomic():
|
2020-07-20 17:39:45 +02:00
|
|
|
bulk_bookings = []
|
|
|
|
for i in range(0, primary_wanted_places - primary_booked_places):
|
|
|
|
bulk_bookings.append(
|
|
|
|
booking.clone(
|
|
|
|
primary_booking=booking,
|
|
|
|
save=False,
|
|
|
|
)
|
2020-12-29 10:42:33 +01:00
|
|
|
)
|
2020-07-20 17:39:45 +02:00
|
|
|
Booking.objects.bulk_create(bulk_bookings)
|
|
|
|
|
|
|
|
return self.success(booking)
|
|
|
|
|
|
|
|
def decrease(self, booking, secondary_bookings, primary_booked_places, primary_wanted_places):
|
|
|
|
with transaction.atomic():
|
|
|
|
for secondary in secondary_bookings[: primary_booked_places - primary_wanted_places]:
|
|
|
|
secondary.delete()
|
|
|
|
|
|
|
|
return self.success(booking)
|
2020-03-05 14:02:52 +01:00
|
|
|
|
2020-07-20 17:39:45 +02:00
|
|
|
def success(self, booking):
|
2020-03-05 14:02:52 +01:00
|
|
|
response = {'err': 0, 'booking_id': booking.pk}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
|
|
|
|
resize_booking = ResizeBooking.as_view()
|
|
|
|
|
|
|
|
|
2018-04-04 16:07:00 +02:00
|
|
|
class SlotStatus(APIView):
|
2016-06-18 12:24:21 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
2020-08-24 14:45:20 +02:00
|
|
|
def get_object(self, agenda_identifier, event_identifier):
|
2019-11-06 17:00:12 +01:00
|
|
|
try:
|
2020-09-24 09:49:49 +02:00
|
|
|
agenda = Agenda.objects.get(slug=agenda_identifier, kind='events')
|
|
|
|
except Agenda.DoesNotExist:
|
|
|
|
try:
|
|
|
|
# legacy access by agenda id
|
|
|
|
agenda = Agenda.objects.get(pk=agenda_identifier, kind='events')
|
|
|
|
except (ValueError, Agenda.DoesNotExist):
|
|
|
|
raise Http404()
|
|
|
|
try:
|
|
|
|
return agenda.event_set.get(slug=event_identifier)
|
2019-11-06 17:00:12 +01:00
|
|
|
except Event.DoesNotExist:
|
|
|
|
try:
|
|
|
|
# legacy access by event id
|
2020-09-24 09:49:49 +02:00
|
|
|
return agenda.event_set.get(pk=event_identifier)
|
2019-11-06 17:00:12 +01:00
|
|
|
except (ValueError, Event.DoesNotExist):
|
|
|
|
raise Http404()
|
|
|
|
|
|
|
|
def get(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
2020-08-24 14:45:20 +02:00
|
|
|
event = self.get_object(agenda_identifier, event_identifier)
|
2016-06-18 12:24:21 +02:00
|
|
|
response = {
|
|
|
|
'err': 0,
|
|
|
|
}
|
2020-12-03 16:14:30 +01:00
|
|
|
response.update(get_event_detail(request, event))
|
2016-06-18 12:24:21 +02:00
|
|
|
return Response(response)
|
|
|
|
|
2019-11-06 17:00:12 +01:00
|
|
|
|
2016-06-18 12:24:21 +02:00
|
|
|
slot_status = SlotStatus.as_view()
|
2018-07-17 10:51:58 +02:00
|
|
|
|
|
|
|
|
2020-05-12 10:26:43 +02:00
|
|
|
class SlotBookings(APIView):
|
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
|
|
|
def get_object(self, agenda_identifier, event_identifier):
|
2020-06-23 16:53:20 +02:00
|
|
|
return get_object_or_404(
|
|
|
|
Event, slug=event_identifier, agenda__slug=agenda_identifier, agenda__kind='events'
|
|
|
|
)
|
2020-05-12 10:26:43 +02:00
|
|
|
|
|
|
|
def get(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
|
|
|
if not request.GET.get('user_external_id'):
|
|
|
|
response = {
|
|
|
|
'err': 1,
|
|
|
|
'err_class': 'missing param user_external_id',
|
|
|
|
'err_desc': _('missing param user_external_id'),
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
event = self.get_object(agenda_identifier, event_identifier)
|
|
|
|
booking_queryset = event.booking_set.filter(
|
2020-06-18 09:25:44 +02:00
|
|
|
user_external_id=request.GET['user_external_id'],
|
|
|
|
primary_booking__isnull=True,
|
|
|
|
cancellation_datetime__isnull=True,
|
2020-05-12 10:26:43 +02:00
|
|
|
).order_by('pk')
|
|
|
|
response = {
|
|
|
|
'err': 0,
|
|
|
|
'data': [{'booking_id': b.pk, 'in_waiting_list': b.in_waiting_list} for b in booking_queryset],
|
|
|
|
}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
|
|
|
|
slot_bookings = SlotBookings.as_view()
|
|
|
|
|
|
|
|
|
2018-07-17 10:51:58 +02:00
|
|
|
class BookingICS(APIView):
|
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
|
|
|
def get(self, request, booking_pk=None, format=None):
|
|
|
|
booking = get_object_or_404(Booking, id=booking_pk)
|
|
|
|
response = HttpResponse(booking.get_ics(request), content_type='text/calendar')
|
|
|
|
return response
|
|
|
|
|
2019-12-16 16:21:24 +01:00
|
|
|
|
2018-07-17 10:51:58 +02:00
|
|
|
booking_ics = BookingICS.as_view()
|