agendas: move get_all_slots() and get_min/max_datetime() as Agenda's methods (#76335)
This commit is contained in:
parent
3c25b09fa0
commit
60b1608f93
|
@ -164,6 +164,11 @@ def booking_template_validator(value):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
TimeSlot = collections.namedtuple(
|
||||||
|
'TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk', 'booked_for_external_user']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Agenda(models.Model):
|
class Agenda(models.Model):
|
||||||
label = models.CharField(_('Label'), max_length=150)
|
label = models.CharField(_('Label'), max_length=150)
|
||||||
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
|
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
|
||||||
|
@ -1108,6 +1113,288 @@ class Agenda(models.Model):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_min_datetime(self, start_datetime=None):
|
||||||
|
if self.minimal_booking_delay is None:
|
||||||
|
return start_datetime
|
||||||
|
|
||||||
|
if start_datetime is None:
|
||||||
|
return self.min_booking_datetime
|
||||||
|
return max(self.min_booking_datetime, start_datetime)
|
||||||
|
|
||||||
|
def get_max_datetime(self, end_datetime=None):
|
||||||
|
if self.maximal_booking_delay is None:
|
||||||
|
return end_datetime
|
||||||
|
|
||||||
|
if end_datetime is None:
|
||||||
|
return self.max_booking_datetime
|
||||||
|
return min(self.max_booking_datetime, end_datetime)
|
||||||
|
|
||||||
|
def get_all_slots(
|
||||||
|
self,
|
||||||
|
meeting_type,
|
||||||
|
resources=None,
|
||||||
|
unique=False,
|
||||||
|
start_datetime=None,
|
||||||
|
end_datetime=None,
|
||||||
|
user_external_id=None,
|
||||||
|
):
|
||||||
|
"""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.
|
||||||
|
|
||||||
|
The process is done in four phases:
|
||||||
|
- 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,
|
||||||
|
- 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
|
||||||
|
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.
|
||||||
|
If it is excluded, ignore it completely.
|
||||||
|
It if is booked, report the slot as full.
|
||||||
|
"""
|
||||||
|
assert self.kind != 'events', 'get_all_slots() does not work on events agendas'
|
||||||
|
|
||||||
|
resources = resources or []
|
||||||
|
# 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 = self.get_base_meeting_duration()
|
||||||
|
max_meeting_duration_td = datetime.timedelta(minutes=self.get_max_meeting_duration())
|
||||||
|
base_min_datetime = self.get_min_datetime(start_datetime)
|
||||||
|
base_max_datetime = self.get_max_datetime(end_datetime)
|
||||||
|
|
||||||
|
meeting_duration = meeting_type.duration
|
||||||
|
meeting_duration_td = datetime.timedelta(minutes=meeting_duration)
|
||||||
|
|
||||||
|
now_datetime = now()
|
||||||
|
base_date = now_datetime.date()
|
||||||
|
agendas = self.get_real_agendas()
|
||||||
|
|
||||||
|
# regroup agendas by their opening period
|
||||||
|
agenda_ids_by_min_max_datetimes = collections.defaultdict(set)
|
||||||
|
agenda_id_min_max_datetime = {}
|
||||||
|
for agenda in agendas:
|
||||||
|
used_min_datetime = base_min_datetime
|
||||||
|
if self.minimal_booking_delay is None:
|
||||||
|
used_min_datetime = agenda.get_min_datetime(start_datetime)
|
||||||
|
used_max_datetime = base_max_datetime
|
||||||
|
if self.maximal_booking_delay is None:
|
||||||
|
used_max_datetime = agenda.get_max_datetime(end_datetime)
|
||||||
|
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)
|
||||||
|
.select_related('desk')
|
||||||
|
.order_by('desk_id', 'start_datetime', 'end_datetime'),
|
||||||
|
key=lambda time_period: time_period.desk,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# add exceptions from unavailability calendar
|
||||||
|
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:
|
||||||
|
if desk not in desks_exceptions:
|
||||||
|
desks_exceptions[desk] = IntervalSet()
|
||||||
|
desks_exceptions[desk].add(
|
||||||
|
time_period_exception.start_datetime, time_period_exception.end_datetime
|
||||||
|
)
|
||||||
|
|
||||||
|
# compute reduced min/max_datetime windows by desks based on exceptions
|
||||||
|
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
|
||||||
|
if not base:
|
||||||
|
# ignore this desk, exceptions cover all opening time
|
||||||
|
# use an empty interval (begin == end) for this
|
||||||
|
desk_min_max_datetime[desk] = (now_datetime, now_datetime)
|
||||||
|
continue
|
||||||
|
min_datetime = base.min().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
if base_min_datetime:
|
||||||
|
min_datetime = max(min_datetime, base_min_datetime)
|
||||||
|
max_datetime = base.max()
|
||||||
|
if base_max_datetime:
|
||||||
|
max_datetime = min(max_datetime, base_max_datetime)
|
||||||
|
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,
|
||||||
|
start_datetime__gte=used_min_datetime - max_meeting_duration_td,
|
||||||
|
start_datetime__lte=used_max_datetime,
|
||||||
|
)
|
||||||
|
.exclude(booking__cancellation_datetime__isnull=False)
|
||||||
|
# ordering is important for the later groupby, it works like sort | uniq
|
||||||
|
.order_by('desk_id', 'start_datetime', 'meeting_type__duration')
|
||||||
|
.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])
|
||||||
|
)
|
||||||
|
|
||||||
|
# aggregate already booked time intervals for resources
|
||||||
|
resources_bookings = IntervalSet()
|
||||||
|
if self.kind == 'meetings' and resources:
|
||||||
|
used_min_datetime, used_max_datetime = agenda_id_min_max_datetime[self.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 - max_meeting_duration_td,
|
||||||
|
start_datetime__lte=used_max_datetime,
|
||||||
|
)
|
||||||
|
.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
|
||||||
|
)
|
||||||
|
|
||||||
|
# aggregate already booked time intervals by excluded_user_external_id
|
||||||
|
user_bookings = IntervalSet()
|
||||||
|
if user_external_id:
|
||||||
|
used_min_datetime, used_max_datetime = (
|
||||||
|
min(v[0] for v in agenda_id_min_max_datetime.values()),
|
||||||
|
max(v[1] for v in agenda_id_min_max_datetime.values()),
|
||||||
|
)
|
||||||
|
booked_events = (
|
||||||
|
Event.objects.filter(
|
||||||
|
agenda__in=agendas,
|
||||||
|
start_datetime__gte=used_min_datetime - max_meeting_duration_td,
|
||||||
|
start_datetime__lte=used_max_datetime,
|
||||||
|
booking__user_external_id=user_external_id,
|
||||||
|
)
|
||||||
|
.exclude(booking__cancellation_datetime__isnull=False)
|
||||||
|
# ordering is important for the later groupby, it works like sort | uniq
|
||||||
|
.order_by('start_datetime', 'meeting_type__duration')
|
||||||
|
.values_list('start_datetime', 'meeting_type__duration')
|
||||||
|
)
|
||||||
|
# compute exclusion set by desk from all bookings, using
|
||||||
|
# itertools.groupby() to group them by desk_id
|
||||||
|
user_bookings = IntervalSet.from_ordered(
|
||||||
|
(
|
||||||
|
event_start_datetime,
|
||||||
|
event_start_datetime + datetime.timedelta(minutes=event_duration),
|
||||||
|
)
|
||||||
|
for event_start_datetime, event_duration in booked_events
|
||||||
|
)
|
||||||
|
|
||||||
|
unique_booked = {}
|
||||||
|
for time_period in self.get_effective_time_periods(base_min_datetime, base_max_datetime):
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
):
|
||||||
|
end_datetime = start_datetime + meeting_duration_td
|
||||||
|
timestamp = start_datetime.timestamp()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# check resources first
|
||||||
|
booked = resources_bookings.overlaps(start_datetime, end_datetime)
|
||||||
|
# then check user boookings
|
||||||
|
booked_for_external_user = user_bookings.overlaps(start_datetime, end_datetime)
|
||||||
|
booked = booked or booked_for_external_user
|
||||||
|
# then bookings if resources are free
|
||||||
|
if not booked:
|
||||||
|
booked = desk.id in bookings and bookings[desk.id].overlaps(
|
||||||
|
start_datetime, end_datetime
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
booked_for_external_user=booked_for_external_user,
|
||||||
|
)
|
||||||
|
if unique and not booked:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
class VirtualMember(models.Model):
|
class VirtualMember(models.Model):
|
||||||
"""Trough model to link virtual agendas to their real agendas.
|
"""Trough model to link virtual agendas to their real agendas.
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
import collections
|
import collections
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@ -51,11 +50,9 @@ from chrono.agendas.models import (
|
||||||
MeetingType,
|
MeetingType,
|
||||||
SharedCustodyAgenda,
|
SharedCustodyAgenda,
|
||||||
Subscription,
|
Subscription,
|
||||||
TimePeriodException,
|
|
||||||
)
|
)
|
||||||
from chrono.api import serializers
|
from chrono.api import serializers
|
||||||
from chrono.api.utils import APIError, APIErrorBadRequest, Response
|
from chrono.api.utils import APIError, APIErrorBadRequest, Response
|
||||||
from chrono.interval import IntervalSet
|
|
||||||
from chrono.utils.publik_urls import translate_to_publik_url
|
from chrono.utils.publik_urls import translate_to_publik_url
|
||||||
from chrono.utils.timezone import localtime, make_aware, now
|
from chrono.utils.timezone import localtime, make_aware, now
|
||||||
|
|
||||||
|
@ -68,291 +65,6 @@ def format_response_date(dt):
|
||||||
return localtime(dt).strftime('%Y-%m-%d')
|
return localtime(dt).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
|
||||||
def get_min_datetime(agenda, start_datetime=None):
|
|
||||||
if agenda.minimal_booking_delay is None:
|
|
||||||
return start_datetime
|
|
||||||
|
|
||||||
if start_datetime is None:
|
|
||||||
return agenda.min_booking_datetime
|
|
||||||
return max(agenda.min_booking_datetime, start_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
def get_max_datetime(agenda, end_datetime=None):
|
|
||||||
if agenda.maximal_booking_delay is None:
|
|
||||||
return end_datetime
|
|
||||||
|
|
||||||
if end_datetime is None:
|
|
||||||
return agenda.max_booking_datetime
|
|
||||||
return min(agenda.max_booking_datetime, end_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
TimeSlot = collections.namedtuple(
|
|
||||||
'TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk', 'booked_for_external_user']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_slots(
|
|
||||||
base_agenda,
|
|
||||||
meeting_type,
|
|
||||||
resources=None,
|
|
||||||
unique=False,
|
|
||||||
start_datetime=None,
|
|
||||||
end_datetime=None,
|
|
||||||
user_external_id=None,
|
|
||||||
):
|
|
||||||
"""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.
|
|
||||||
|
|
||||||
The process is done in four phases:
|
|
||||||
- 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,
|
|
||||||
- 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
|
|
||||||
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.
|
|
||||||
If it is excluded, ignore it completely.
|
|
||||||
It if is booked, report the slot as full.
|
|
||||||
"""
|
|
||||||
resources = resources or []
|
|
||||||
# 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()
|
|
||||||
max_meeting_duration_td = datetime.timedelta(minutes=base_agenda.get_max_meeting_duration())
|
|
||||||
base_min_datetime = get_min_datetime(base_agenda, start_datetime)
|
|
||||||
base_max_datetime = get_max_datetime(base_agenda, end_datetime)
|
|
||||||
|
|
||||||
meeting_duration = meeting_type.duration
|
|
||||||
meeting_duration_td = datetime.timedelta(minutes=meeting_duration)
|
|
||||||
|
|
||||||
now_datetime = now()
|
|
||||||
base_date = now_datetime.date()
|
|
||||||
agendas = base_agenda.get_real_agendas()
|
|
||||||
|
|
||||||
# regroup agendas by their opening period
|
|
||||||
agenda_ids_by_min_max_datetimes = collections.defaultdict(set)
|
|
||||||
agenda_id_min_max_datetime = {}
|
|
||||||
for agenda in agendas:
|
|
||||||
used_min_datetime = base_min_datetime
|
|
||||||
if base_agenda.minimal_booking_delay is None:
|
|
||||||
used_min_datetime = get_min_datetime(agenda, start_datetime)
|
|
||||||
used_max_datetime = base_max_datetime
|
|
||||||
if base_agenda.maximal_booking_delay is None:
|
|
||||||
used_max_datetime = get_max_datetime(agenda, end_datetime)
|
|
||||||
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)
|
|
||||||
.select_related('desk')
|
|
||||||
.order_by('desk_id', 'start_datetime', 'end_datetime'),
|
|
||||||
key=lambda time_period: time_period.desk,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
# add exceptions from unavailability calendar
|
|
||||||
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:
|
|
||||||
if desk not in desks_exceptions:
|
|
||||||
desks_exceptions[desk] = IntervalSet()
|
|
||||||
desks_exceptions[desk].add(
|
|
||||||
time_period_exception.start_datetime, time_period_exception.end_datetime
|
|
||||||
)
|
|
||||||
|
|
||||||
# compute reduced min/max_datetime windows by desks based on exceptions
|
|
||||||
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
|
|
||||||
if not base:
|
|
||||||
# ignore this desk, exceptions cover all opening time
|
|
||||||
# use an empty interval (begin == end) for this
|
|
||||||
desk_min_max_datetime[desk] = (now_datetime, now_datetime)
|
|
||||||
continue
|
|
||||||
min_datetime = base.min().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
if base_min_datetime:
|
|
||||||
min_datetime = max(min_datetime, base_min_datetime)
|
|
||||||
max_datetime = base.max()
|
|
||||||
if base_max_datetime:
|
|
||||||
max_datetime = min(max_datetime, base_max_datetime)
|
|
||||||
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,
|
|
||||||
start_datetime__gte=used_min_datetime - max_meeting_duration_td,
|
|
||||||
start_datetime__lte=used_max_datetime,
|
|
||||||
)
|
|
||||||
.exclude(booking__cancellation_datetime__isnull=False)
|
|
||||||
# ordering is important for the later groupby, it works like sort | uniq
|
|
||||||
.order_by('desk_id', 'start_datetime', 'meeting_type__duration')
|
|
||||||
.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])
|
|
||||||
)
|
|
||||||
|
|
||||||
# aggregate already booked time intervals for resources
|
|
||||||
resources_bookings = IntervalSet()
|
|
||||||
if base_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 - max_meeting_duration_td,
|
|
||||||
start_datetime__lte=used_max_datetime,
|
|
||||||
)
|
|
||||||
.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
|
|
||||||
)
|
|
||||||
|
|
||||||
# aggregate already booked time intervals by excluded_user_external_id
|
|
||||||
user_bookings = IntervalSet()
|
|
||||||
if user_external_id:
|
|
||||||
used_min_datetime, used_max_datetime = (
|
|
||||||
min(v[0] for v in agenda_id_min_max_datetime.values()),
|
|
||||||
max(v[1] for v in agenda_id_min_max_datetime.values()),
|
|
||||||
)
|
|
||||||
booked_events = (
|
|
||||||
Event.objects.filter(
|
|
||||||
agenda__in=agendas,
|
|
||||||
start_datetime__gte=used_min_datetime - max_meeting_duration_td,
|
|
||||||
start_datetime__lte=used_max_datetime,
|
|
||||||
booking__user_external_id=user_external_id,
|
|
||||||
)
|
|
||||||
.exclude(booking__cancellation_datetime__isnull=False)
|
|
||||||
# ordering is important for the later groupby, it works like sort | uniq
|
|
||||||
.order_by('start_datetime', 'meeting_type__duration')
|
|
||||||
.values_list('start_datetime', 'meeting_type__duration')
|
|
||||||
)
|
|
||||||
# compute exclusion set by desk from all bookings, using
|
|
||||||
# itertools.groupby() to group them by desk_id
|
|
||||||
user_bookings = IntervalSet.from_ordered(
|
|
||||||
(
|
|
||||||
event_start_datetime,
|
|
||||||
event_start_datetime + datetime.timedelta(minutes=event_duration),
|
|
||||||
)
|
|
||||||
for event_start_datetime, event_duration in booked_events
|
|
||||||
)
|
|
||||||
|
|
||||||
unique_booked = {}
|
|
||||||
for time_period in base_agenda.get_effective_time_periods(base_min_datetime, base_max_datetime):
|
|
||||||
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
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
):
|
|
||||||
end_datetime = start_datetime + meeting_duration_td
|
|
||||||
timestamp = start_datetime.timestamp()
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# check resources first
|
|
||||||
booked = resources_bookings.overlaps(start_datetime, end_datetime)
|
|
||||||
# then check user boookings
|
|
||||||
booked_for_external_user = user_bookings.overlaps(start_datetime, end_datetime)
|
|
||||||
booked = booked or booked_for_external_user
|
|
||||||
# then bookings if resources are free
|
|
||||||
if not booked:
|
|
||||||
booked = desk.id in bookings and bookings[desk.id].overlaps(
|
|
||||||
start_datetime, end_datetime
|
|
||||||
)
|
|
||||||
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,
|
|
||||||
booked_for_external_user=booked_for_external_user,
|
|
||||||
)
|
|
||||||
if unique and not booked:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def get_agenda_detail(request, agenda, check_events=False):
|
def get_agenda_detail(request, agenda, check_events=False):
|
||||||
agenda_detail = {
|
agenda_detail = {
|
||||||
'id': agenda.slug,
|
'id': agenda.slug,
|
||||||
|
@ -1075,8 +787,7 @@ class MeetingDatetimes(APIView):
|
||||||
def unique_slots():
|
def unique_slots():
|
||||||
last_slot = None
|
last_slot = None
|
||||||
all_slots = list(
|
all_slots = list(
|
||||||
get_all_slots(
|
agenda.get_all_slots(
|
||||||
agenda,
|
|
||||||
meeting_type,
|
meeting_type,
|
||||||
resources=resources,
|
resources=resources,
|
||||||
unique=True,
|
unique=True,
|
||||||
|
@ -1498,8 +1209,7 @@ class Fillslots(APIView):
|
||||||
except (MeetingType.DoesNotExist, ValueError):
|
except (MeetingType.DoesNotExist, ValueError):
|
||||||
raise APIErrorBadRequest(N_('invalid meeting type id: %s'), meeting_type_id)
|
raise APIErrorBadRequest(N_('invalid meeting type id: %s'), meeting_type_id)
|
||||||
all_slots = sorted(
|
all_slots = sorted(
|
||||||
get_all_slots(
|
agenda.get_all_slots(
|
||||||
agenda,
|
|
||||||
meeting_type,
|
meeting_type,
|
||||||
resources=resources,
|
resources=resources,
|
||||||
user_external_id=user_external_id if exclude_user else None,
|
user_external_id=user_external_id if exclude_user else None,
|
||||||
|
|
Loading…
Reference in New Issue