api: check resources in datetimes endpoint (#38942)
This commit is contained in:
parent
4cc1bf6c45
commit
a1d38b0a88
|
@ -62,24 +62,26 @@ def get_max_datetime(agenda):
|
|||
TimeSlot = collections.namedtuple('TimeSlot', ['start_datetime', 'end_datetime', 'full', 'desk'])
|
||||
|
||||
|
||||
def get_all_slots(agenda, meeting_type, unique=False):
|
||||
def get_all_slots(base_agenda, meeting_type, resources=None, unique=False):
|
||||
'''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 three phases:
|
||||
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 and las phase: generate time slots from each time period based
|
||||
- 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, ingore it completely.
|
||||
It if is booked, reports the slot as full.
|
||||
If it is excluded, ignore it completely.
|
||||
It if is booked, report the slot as full.
|
||||
'''
|
||||
base_agenda = agenda
|
||||
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
|
||||
|
@ -93,7 +95,7 @@ def get_all_slots(agenda, meeting_type, unique=False):
|
|||
|
||||
now_datetime = now()
|
||||
base_date = now_datetime.date()
|
||||
agendas = agenda.get_real_agendas()
|
||||
agendas = base_agenda.get_real_agendas()
|
||||
|
||||
# regroup agendas by their opening period
|
||||
agenda_ids_by_min_max_datetimes = collections.defaultdict(set)
|
||||
|
@ -119,7 +121,7 @@ def get_all_slots(agenda, meeting_type, unique=False):
|
|||
key=lambda time_period: time_period.desk,
|
||||
)
|
||||
}
|
||||
# compute reduced min/max_datetime windows by desks based on exclusions
|
||||
# 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]])
|
||||
|
@ -164,6 +166,29 @@ def get_all_slots(agenda, meeting_type, unique=False):
|
|||
for desk_id, values in itertools.groupby(booked_events, lambda be: be[0])
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
unique_booked = {}
|
||||
for time_period in base_agenda.get_effective_time_periods():
|
||||
duration = (
|
||||
|
@ -212,7 +237,13 @@ def get_all_slots(agenda, meeting_type, unique=False):
|
|||
continue
|
||||
|
||||
# slot is full if an already booked event overlaps it
|
||||
booked = desk.id in bookings and bookings[desk.id].overlaps(start_datetime, end_datetime)
|
||||
# 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
|
||||
)
|
||||
if unique and unique_booked.get(timestamp) is booked:
|
||||
continue
|
||||
unique_booked[timestamp] = booked
|
||||
|
@ -416,6 +447,22 @@ class MeetingDatetimes(APIView):
|
|||
|
||||
now_datetime = now()
|
||||
|
||||
resources = None
|
||||
if agenda.kind == 'meetings' and 'resources' in request.GET:
|
||||
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))
|
||||
return Response(
|
||||
{
|
||||
'err': 1,
|
||||
'err_class': 'invalid resource: %s' % ', '.join(unknown_slugs),
|
||||
'err_desc': _('invalid resource: %s') % ', '.join(unknown_slugs),
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# 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
|
||||
|
@ -429,7 +476,7 @@ class MeetingDatetimes(APIView):
|
|||
# The generator also remove slots starting before the current time.
|
||||
def unique_slots():
|
||||
last_slot = None
|
||||
all_slots = list(get_all_slots(agenda, meeting_type, unique=True))
|
||||
all_slots = list(get_all_slots(agenda, meeting_type, resources=resources, unique=True))
|
||||
for slot in sorted(all_slots, key=lambda slot: slot[:3]):
|
||||
if slot.start_datetime < now_datetime:
|
||||
continue
|
||||
|
|
|
@ -426,6 +426,217 @@ def test_datetimes_api_meetings_agenda(app, meetings_agenda):
|
|||
assert len(resp.json['data']) == 3
|
||||
|
||||
|
||||
def test_datetimes_api_meetings_agenda_with_resources(app):
|
||||
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
|
||||
tomorrow_str = tomorrow.isoformat()
|
||||
agenda = Agenda.objects.create(
|
||||
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
|
||||
)
|
||||
other_agenda = Agenda.objects.create(label='Other', kind='meetings')
|
||||
other_desk = Desk.objects.create(agenda=other_agenda, slug='desk-1')
|
||||
other_meeting_type = MeetingType.objects.create(agenda=other_agenda, slug='foo-bar', duration=90)
|
||||
resource1 = Resource.objects.create(label='Resource 1')
|
||||
resource2 = Resource.objects.create(label='Resource 2')
|
||||
resource3 = Resource.objects.create(label='Resource 3')
|
||||
agenda.resources.add(resource1, resource2, resource3)
|
||||
desk = Desk.objects.create(agenda=agenda, slug='desk-1')
|
||||
desk2 = Desk.objects.create(agenda=agenda, slug='desk-2')
|
||||
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
|
||||
TimePeriod.objects.create(
|
||||
weekday=tomorrow.weekday(), start_time=datetime.time(9, 0), end_time=datetime.time(17, 00), desk=desk,
|
||||
)
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == []
|
||||
|
||||
# all resources are free
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s,%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource1.slug,
|
||||
resource2.slug,
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == []
|
||||
|
||||
# resource 1 is not available from 10h to 11h30 in another agenda
|
||||
dt = make_aware(datetime.datetime.combine(tomorrow, datetime.time(10, 0)))
|
||||
event1 = Event.objects.create(
|
||||
agenda=other_agenda,
|
||||
meeting_type=other_meeting_type,
|
||||
places=1,
|
||||
full=False,
|
||||
start_datetime=dt,
|
||||
desk=other_desk,
|
||||
)
|
||||
event1.resources.add(resource1)
|
||||
booking_r1 = Booking.objects.create(event=event1)
|
||||
# resource 3 is not available from 9h to 10h in this agenda (but desk-1 is free)
|
||||
dt = make_aware(datetime.datetime.combine(tomorrow, datetime.time(9, 0)))
|
||||
event2 = Event.objects.create(
|
||||
agenda=agenda, meeting_type=meeting_type, places=1, full=False, start_datetime=dt, desk=desk2,
|
||||
)
|
||||
event2.resources.add(resource3)
|
||||
Booking.objects.create(event=event2)
|
||||
dt = make_aware(datetime.datetime.combine(tomorrow, datetime.time(9, 30)))
|
||||
event3 = Event.objects.create(
|
||||
agenda=agenda, meeting_type=meeting_type, places=1, full=False, start_datetime=dt, desk=desk2,
|
||||
)
|
||||
event3.resources.add(resource3)
|
||||
Booking.objects.create(event=event3)
|
||||
|
||||
# check for resource 1 and resource 2: not available from 10H to 11H30
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s,%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource1.slug,
|
||||
resource2.slug,
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
|
||||
'%s 10:00:00' % tomorrow_str,
|
||||
'%s 10:30:00' % tomorrow_str,
|
||||
'%s 11:00:00' % tomorrow_str,
|
||||
]
|
||||
|
||||
# check for resource 2 only ? it's free
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource2.slug,
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == []
|
||||
|
||||
# check for resource 3: not available from 9H to 10H
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource3.slug,
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
|
||||
'%s 09:00:00' % tomorrow_str,
|
||||
'%s 09:30:00' % tomorrow_str,
|
||||
]
|
||||
|
||||
# check for resource 1 and resource 3
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s,%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource1.slug,
|
||||
resource3.slug,
|
||||
)
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
resp = app.get(api_url)
|
||||
assert len(ctx.captured_queries) == 9
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
|
||||
'%s 09:00:00' % tomorrow_str,
|
||||
'%s 09:30:00' % tomorrow_str,
|
||||
'%s 10:00:00' % tomorrow_str,
|
||||
'%s 10:30:00' % tomorrow_str,
|
||||
'%s 11:00:00' % tomorrow_str,
|
||||
]
|
||||
|
||||
# no resources to book
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=' % (agenda.slug, meeting_type.slug,)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == []
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug,)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == []
|
||||
|
||||
# event 3 is booked without resource, only one desk
|
||||
event3.desk = desk
|
||||
event3.save()
|
||||
event3.resources.clear()
|
||||
desk2.delete()
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug,)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
|
||||
'%s 09:30:00' % tomorrow_str
|
||||
]
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource1.slug,
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
|
||||
'%s 09:30:00' % tomorrow_str,
|
||||
'%s 10:00:00' % tomorrow_str,
|
||||
'%s 10:30:00' % tomorrow_str,
|
||||
'%s 11:00:00' % tomorrow_str,
|
||||
]
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource3.slug,
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
|
||||
'%s 09:30:00' % tomorrow_str
|
||||
]
|
||||
|
||||
# resource is unknown or not valid for this agenda
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=foobarbaz' % (agenda.slug, meeting_type.slug,)
|
||||
resp = app.get(api_url, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid resource: foobarbaz' # legacy
|
||||
assert resp.json['err_class'] == 'invalid resource: foobarbaz'
|
||||
assert resp.json['err_desc'] == 'invalid resource: foobarbaz'
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s,foobarbaz' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource3.slug,
|
||||
)
|
||||
resp = app.get(api_url, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid resource: foobarbaz' # legacy
|
||||
assert resp.json['err_class'] == 'invalid resource: foobarbaz'
|
||||
assert resp.json['err_desc'] == 'invalid resource: foobarbaz'
|
||||
agenda.resources.remove(resource3)
|
||||
resp = app.get(api_url, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid resource: foobarbaz, resource-3' # legacy
|
||||
assert resp.json['err_class'] == 'invalid resource: foobarbaz, resource-3'
|
||||
assert resp.json['err_desc'] == 'invalid resource: foobarbaz, resource-3'
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource3.slug,
|
||||
)
|
||||
resp = app.get(api_url, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid resource: resource-3' # legacy
|
||||
assert resp.json['err_class'] == 'invalid resource: resource-3'
|
||||
assert resp.json['err_desc'] == 'invalid resource: resource-3'
|
||||
|
||||
# if booking is canceled the resource is free
|
||||
booking_r1.cancel()
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/?resources=%s' % (
|
||||
agenda.slug,
|
||||
meeting_type.slug,
|
||||
resource1.slug,
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
assert len(resp.json['data']) == 32
|
||||
assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
|
||||
'%s 09:30:00' % tomorrow_str
|
||||
]
|
||||
|
||||
|
||||
def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda, user):
|
||||
meetings_agenda.minimal_booking_delay = 0
|
||||
meetings_agenda.maximal_booking_delay = 10
|
||||
|
|
Loading…
Reference in New Issue