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
|
||||
|
||||
|
||||
TimeSlot = collections.namedtuple(
|
||||
'TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk', 'booked_for_external_user']
|
||||
)
|
||||
|
||||
|
||||
class Agenda(models.Model):
|
||||
label = models.CharField(_('Label'), max_length=150)
|
||||
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
|
||||
|
@ -1108,6 +1113,288 @@ class Agenda(models.Model):
|
|||
|
||||
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):
|
||||
"""Trough model to link virtual agendas to their real agendas.
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
import itertools
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
@ -51,11 +50,9 @@ from chrono.agendas.models import (
|
|||
MeetingType,
|
||||
SharedCustodyAgenda,
|
||||
Subscription,
|
||||
TimePeriodException,
|
||||
)
|
||||
from chrono.api import serializers
|
||||
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.timezone import localtime, make_aware, now
|
||||
|
||||
|
@ -68,291 +65,6 @@ def format_response_date(dt):
|
|||
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):
|
||||
agenda_detail = {
|
||||
'id': agenda.slug,
|
||||
|
@ -1075,8 +787,7 @@ class MeetingDatetimes(APIView):
|
|||
def unique_slots():
|
||||
last_slot = None
|
||||
all_slots = list(
|
||||
get_all_slots(
|
||||
agenda,
|
||||
agenda.get_all_slots(
|
||||
meeting_type,
|
||||
resources=resources,
|
||||
unique=True,
|
||||
|
@ -1498,8 +1209,7 @@ class Fillslots(APIView):
|
|||
except (MeetingType.DoesNotExist, ValueError):
|
||||
raise APIErrorBadRequest(N_('invalid meeting type id: %s'), meeting_type_id)
|
||||
all_slots = sorted(
|
||||
get_all_slots(
|
||||
agenda,
|
||||
agenda.get_all_slots(
|
||||
meeting_type,
|
||||
resources=resources,
|
||||
user_external_id=user_external_id if exclude_user else None,
|
||||
|
|
Loading…
Reference in New Issue