api: exclude slots already booked by user - meetings (#51341)

This commit is contained in:
Lauréline Guérin 2021-02-25 09:57:26 +01:00
parent e15439bd3c
commit 6d57af612e
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
2 changed files with 178 additions and 2 deletions

View File

@ -72,7 +72,13 @@ TimeSlot = collections.namedtuple('TimeSlot', ['start_datetime', 'end_datetime',
def get_all_slots(
base_agenda, meeting_type, resources=None, unique=False, start_datetime=None, end_datetime=None
base_agenda,
meeting_type,
resources=None,
unique=False,
start_datetime=None,
end_datetime=None,
excluded_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.
@ -225,6 +231,32 @@ def get_all_slots(
for event_start_datetime, event_duration in booked_events
)
# aggregate already booked time intervals by excluded_user_external_id
user_bookings = IntervalSet()
if excluded_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=agenda_ids,
start_datetime__gte=used_min_datetime,
start_datetime__lte=used_max_datetime + meeting_duration_td,
booking__user_external_id=excluded_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():
duration = (
@ -272,8 +304,11 @@ def get_all_slots(
# 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
if not booked:
booked = user_bookings.overlaps(start_datetime, end_datetime)
# then bookings if resources are free
if not booked:
# then bookings if resources are free
booked = desk.id in bookings and bookings[desk.id].overlaps(
start_datetime, end_datetime
)
@ -629,6 +664,8 @@ class MeetingDatetimes(APIView):
http_status=status.HTTP_400_BAD_REQUEST,
)
user_external_id = request.GET.get('exclude_user_external_id') or None
# 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
@ -650,6 +687,7 @@ class MeetingDatetimes(APIView):
unique=True,
start_datetime=start_datetime,
end_datetime=end_datetime,
excluded_user_external_id=user_external_id,
)
)
for slot in sorted(all_slots, key=lambda slot: slot[:3]):

View File

@ -1007,6 +1007,66 @@ def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda,
assert resp.json['err_desc'] == 'no more desk available'
@pytest.mark.freeze_time('2021-02-25')
def test_datetimes_api_meetings_agenda_exclude_slots(app):
tomorrow = now() + datetime.timedelta(days=1)
agenda = Agenda.objects.create(
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
)
desk = Desk.objects.create(agenda=agenda, slug='desk')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
TimePeriod.objects.create(
weekday=tomorrow.date().weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
desk.duplicate()
event = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=9, minute=0),
desk=desk,
)
Booking.objects.create(event=event, user_external_id='42')
event2 = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=10, minute=0),
desk=desk,
)
cancelled = Booking.objects.create(event=event2, user_external_id='35')
cancelled.cancel()
resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug))
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is False
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug),
params={'exclude_user_external_id': '35'},
)
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is False
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
with CaptureQueriesContext(connection) as ctx:
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug),
params={'exclude_user_external_id': '42'},
)
assert len(ctx.captured_queries) == 9
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is True
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
def test_booking_api(app, some_data, user):
agenda = Agenda.objects.filter(label=u'Foo bar')[0]
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
@ -4831,6 +4891,84 @@ def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, mock_now):
assert len(resp.json['data']) == 20
@pytest.mark.freeze_time('2021-02-25')
def test_virtual_agendas_meetings_datetimes_exclude_slots(app):
tomorrow = now() + datetime.timedelta(days=1)
agenda = Agenda.objects.create(
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
)
desk = Desk.objects.create(agenda=agenda, slug='desk')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
TimePeriod.objects.create(
weekday=tomorrow.date().weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
agenda2 = agenda.duplicate()
virt_agenda = Agenda.objects.create(
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=10
)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
event = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=9, minute=0),
desk=desk,
)
Booking.objects.create(event=event, user_external_id='42')
event2 = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=10, minute=0),
desk=desk,
)
cancelled = Booking.objects.create(event=event2, user_external_id='35')
cancelled.cancel()
resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug))
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is False
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug),
params={'exclude_user_external_id': '35'},
)
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is False
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
with CaptureQueriesContext(connection) as ctx:
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug),
params={'exclude_user_external_id': '42'},
)
assert len(ctx.captured_queries) == 13
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is True
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
virt_agenda.minimal_booking_delay = None
virt_agenda.maximal_booking_delay = None
virt_agenda.save()
resp = app.get(
'/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug),
params={'exclude_user_external_id': '42'},
)
assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
assert resp.json['data'][0]['disabled'] is True
assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
assert resp.json['data'][2]['disabled'] is False
def test_virtual_agendas_meetings_booking(app, mock_now, user):
foo_agenda = Agenda.objects.create(
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5