Skip to content

Commit dba9eae

Browse files
committed
Fix processing buffers when finding the first reservable time
1 parent 6d066dd commit dba9eae

5 files changed

Lines changed: 641 additions & 61 deletions

File tree

opening_hours/models.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime
22

33
from django.db import models
44
from django.db.models import F, Q
@@ -64,15 +64,9 @@ def _get_datetime_str(self) -> str:
6464
else:
6565
return f"{start.strftime(strformat)}-{end.strftime(strformat)}"
6666

67-
def as_time_span_element(
68-
self,
69-
buffer_time_before: timedelta | None = None,
70-
buffer_time_after: timedelta | None = None,
71-
) -> TimeSpanElement:
67+
def as_time_span_element(self) -> TimeSpanElement:
7268
return TimeSpanElement(
7369
start_datetime=self.start_datetime.astimezone(DEFAULT_TIMEZONE),
7470
end_datetime=self.end_datetime.astimezone(DEFAULT_TIMEZONE),
75-
buffer_time_before=buffer_time_before,
76-
buffer_time_after=buffer_time_after,
7771
is_reservable=True,
7872
)

opening_hours/utils/reservable_time_span_client.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,8 @@ def override_reservable_with_closed_time_spans(
256256
new_reservable_time_span = copy(reservable_time_span)
257257
reservable_time_spans.append(new_reservable_time_span)
258258
# Split the reservable time span in two
259-
# Save the buffers on the reservable times, so that we can check if another buffered timespan
260-
# (i.e. a reservation's timespan) would fit between this and the next/previous time span
261259
reservable_time_span.end_datetime = closed_time_span.buffered_start_datetime
262260
new_reservable_time_span.start_datetime = closed_time_span.buffered_end_datetime
263-
reservable_time_span.buffer_time_after = closed_time_span.buffer_time_before
264-
new_reservable_time_span.buffer_time_before = closed_time_span.buffer_time_after
265261

266262
# Reservable time span starts inside the closed time span.
267263
# Shorten the reservable time span from the beginning
@@ -276,10 +272,7 @@ def override_reservable_with_closed_time_spans(
276272
# │ xxxx -> xxxx │ Handled later in the next step │
277273
# └──────────────────────────┴───────────────────────────────────┘
278274
elif reservable_time_span.starts_inside_of(closed_time_span):
279-
# Save the buffer on the reservable time, so that we can check if another buffered timespan
280-
# (i.e. a reservation's timespan) would fit between this and the previous time span
281275
reservable_time_span.start_datetime = closed_time_span.buffered_end_datetime
282-
reservable_time_span.buffer_time_before = closed_time_span.buffer_time_after
283276

284277
# Reservable time span ends inside the closed time span.
285278
# Shorten the reservable time span from the end
@@ -294,10 +287,7 @@ def override_reservable_with_closed_time_spans(
294287
# │ xxxx -> xxxx │ Already handled in last step │
295288
# └──────────────────────────┴───────────────────────────────────┘
296289
elif reservable_time_span.ends_inside_of(closed_time_span):
297-
# Save the buffer on the reservable time, so that we can check if another buffered timespan
298-
# (i.e. a reservation's timespan) would fit between this and the next time span
299290
reservable_time_span.end_datetime = closed_time_span.buffered_start_datetime
300-
reservable_time_span.buffer_time_after = closed_time_span.buffer_time_before
301291

302292
# If the duration of the reservable time span is negative or zero after adjustments
303293
# (buffered time is ignored here), remove it.

reservation_units/querysets.py

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,74 @@ def _get_soft_closed_time_spans_for_reservation_unit(reservation_unit: "Reservat
115115
def _find_first_reservable_time_span_for_reservation_unit(
116116
reservation_unit: "ReservationUnit",
117117
normalised_reservable_time_spans: list[TimeSpanElement],
118+
reservations: list[TimeSpanElement],
118119
minimum_duration_minutes: int,
119120
) -> datetime | None:
120121
"""
121-
Find the first reservable time span for the ReservationUnit that is long enough to fit the minimum duration
122-
123-
Loop through the normalised time spans to find the first one that is long enough to fit the minimum duration
122+
Find the first reservable time span for the ReservationUnit.
123+
124+
In this function we know that normalised_reservable_time_spans (without buffers) and reservations (with buffers)
125+
never overlap due to earlier normalisation.
126+
However, normalised_reservable_time_spans (with buffers from reservation_unit) and
127+
reservations (without buffers) can overlap, so we need to process the normalised_reservable_time_spans one-by-one
128+
and check if they overlap with reservations.
129+
If they do, we need to shorten the reservable_time_span so that its buffer doesn't overlap with the reservation,
130+
while leaving the buffer size unchanged.
131+
After processing, we can check if the reservable_time_span is still long enough to fit the minimum duration.
124132
"""
125133
for reservable_time_span in normalised_reservable_time_spans:
126-
# Move time span start time to the next valid start time
134+
if reservable_time_span is None:
135+
continue
136+
137+
reservable_time_span.buffer_time_before = reservation_unit.buffer_time_before
138+
reservable_time_span.buffer_time_after = reservation_unit.buffer_time_after
139+
140+
for reservation in reservations:
141+
# Reservation (with buffers) can not overlap with the reservable time span (without buffers).
142+
if reservable_time_span.overlaps_with(reservation):
143+
raise ValueError("Reservable Time Span overlaps with Reservations buffer, this should never happen.")
144+
145+
# Only continue forward if a buffered time span overlaps with a reservation (without buffers)
146+
if not reservation.overlaps_with(reservable_time_span):
147+
# No overlapping, skip
148+
continue
149+
150+
# Reservation is inside the Before-buffer, shorten the reservable time span from the start
151+
# ┌──────────────────────────┬─────────────────────────────────────┐
152+
# │ ----oooooo -> ----oo │ Reservation starts in Before-buffer │
153+
# │ xxxx -> xxxx │ │
154+
# ├──────────────────────────┼─────────────────────────────────────┤
155+
# │ --oooo -> --oo │ Reservation ends in Before-buffer │
156+
# │ xxxx -> xxxx │ │
157+
# └──────────────────────────┴─────────────────────────────────────┘
158+
elif (
159+
reservable_time_span.buffered_start_datetime
160+
<= reservation.end_datetime
161+
<= reservable_time_span.start_datetime
162+
):
163+
overlap = reservation.end_datetime - reservable_time_span.buffered_start_datetime
164+
reservable_time_span.start_datetime += overlap
165+
166+
# Reservation is inside the After-buffer, shorten the reservable time span from the end
167+
# ┌──────────────────────────┬─────────────────────────────────────┐
168+
# │ oooo---- -> oo---- │ Reservation starts in After-buffer │
169+
# │ xxxx -> xxxx │ │
170+
# ├──────────────────────────┼─────────────────────────────────────┤
171+
# │ oooooo---- -> oo---- │ Reservation ends in After-buffer │
172+
# │ xxxx -> xxxx │ │
173+
# └──────────────────────────┴─────────────────────────────────────┘
174+
elif (
175+
reservable_time_span.end_datetime
176+
<= reservation.start_datetime
177+
<= reservable_time_span.buffered_end_datetime
178+
):
179+
overlap = reservable_time_span.buffered_end_datetime - reservation.start_datetime
180+
reservable_time_span.end_datetime -= overlap
181+
182+
if reservable_time_span.duration_minutes <= 0:
183+
break
184+
185+
# In case of an invalid start time due to normalisation, move to the next valid start time
127186
reservable_time_span.move_to_next_valid_start_time(reservation_unit)
128187

129188
if reservable_time_span.can_fit_reservation_for_reservation_unit(
@@ -305,15 +364,15 @@ def with_first_reservable_time(
305364
# Affect closed status
306365
# Can overlap with buffers
307366
reservation_unit_hard_closed_time_spans: list[TimeSpanElement] = (
308-
_get_hard_closed_time_spans_for_reservation_unit(reservation_unit) #
367+
_get_hard_closed_time_spans_for_reservation_unit(reservation_unit) # Formatting :)
309368
+ shared_hard_closed_time_spans
310369
)
311370

312371
# Reservation Unit Soft Closed Time Spans
313372
# Don't affect closed status
314373
# Can overlap with buffers
315374
reservation_unit_soft_closed_time_spans: list[TimeSpanElement] = (
316-
_get_soft_closed_time_spans_for_reservation_unit(reservation_unit) #
375+
_get_soft_closed_time_spans_for_reservation_unit(reservation_unit) # Formatting :)
317376
)
318377

319378
# Reservation Closed Time Spans
@@ -326,7 +385,7 @@ def with_first_reservable_time(
326385
# These time spans have no effect on the closed status of the ReservationUnit,
327386
# meaning that the ReservationUnit can be shown as open, even if there are no reservable time spans.
328387
soft_closed_time_spans = (
329-
reservation_unit_soft_closed_time_spans #
388+
reservation_unit_soft_closed_time_spans # Formatting :)
330389
+ reservation_unit_reservation_closed_time_spans
331390
)
332391

@@ -339,10 +398,7 @@ def with_first_reservable_time(
339398

340399
# Go through each ReservableTimeSpan individually one-by-one
341400
for reservable_time_span in reservation_unit.origin_hauki_resource.reservable_time_spans.all():
342-
current_time_span = reservable_time_span.as_time_span_element(
343-
buffer_time_before=reservation_unit.buffer_time_before,
344-
buffer_time_after=reservation_unit.buffer_time_after,
345-
)
401+
current_time_span = reservable_time_span.as_time_span_element()
346402

347403
# Combine all Hard-Closed time spans into one list
348404
hard_closed_time_spans: list[TimeSpanElement] = (
@@ -377,6 +433,7 @@ def with_first_reservable_time(
377433
first_reservable_times[reservation_unit.pk] = _find_first_reservable_time_span_for_reservation_unit(
378434
reservation_unit=reservation_unit,
379435
normalised_reservable_time_spans=soft_normalised_reservable_time_spans,
436+
reservations=reservation_unit_reservation_closed_time_spans,
380437
minimum_duration_minutes=minimum_duration_minutes,
381438
)
382439
# Suitable timespan was found, select and continue to next reservation unit

0 commit comments

Comments
 (0)