agendas: partial bookings, compute double booking check (#82848)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2023-10-26 17:11:43 +02:00 committed by Lauréline Guérin
parent 9d1c33970c
commit e4864ea95b
2 changed files with 250 additions and 8 deletions

View File

@ -2947,10 +2947,43 @@ class Booking(models.Model):
def refresh_computed_times(self, commit=False):
to_update = []
for user_check in self.user_checks.all():
user_checks = sorted(self.user_checks.all(), key=lambda a: a.start_time or datetime.time(0))
many_user_checks = len(user_checks) > 1
assert len(user_checks) < 3 # 2 user_checks max !
if user_checks and not many_user_checks:
# only one user_check
user_check = user_checks[0]
changed = user_check._refresh_computed_times()
if changed:
to_update.append(user_check)
elif many_user_checks:
# two user_checks
user_check1 = user_checks[0]
user_check2 = user_checks[1]
compute_one_first = True
if user_check1.presence != user_check2.presence:
# one user check is presence, the other is absence
if user_check1.presence is False:
# the second check is presence, it should be computed first
compute_one_first = False
if compute_one_first:
changed = user_check1._refresh_computed_times(adjust_end_to_booking=False)
if changed:
to_update.append(user_check1)
changed = user_check2._refresh_computed_times(other_user_check=user_check1)
if changed:
to_update.append(user_check2)
else:
changed = user_check2._refresh_computed_times(adjust_start_to_booking=False)
if changed:
to_update.append(user_check2)
changed = user_check1._refresh_computed_times(other_user_check=user_check2)
if changed:
to_update.append(user_check1)
if commit and to_update:
BookingCheck.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
return to_update
@ -3073,13 +3106,19 @@ class BookingCheck(models.Model):
next_slot = datetime.time(*divmod(previous_slot_minutes + minutes, 60))
return previous_slot, next_slot
def get_computed_start_time(self):
def get_computed_start_time(self, other_user_check=None, adjust_to_booking=True):
if self.start_time is None:
return None
start_time = self.start_time
if self.booking.start_time:
if self.booking.start_time and adjust_to_booking:
# adjust start_time to the start of the booking if requested
start_time = min(self.start_time, self.booking.start_time)
if other_user_check and other_user_check.computed_start_time and other_user_check.computed_end_time:
# if other user_check exists and is completely computed
if other_user_check.start_time < self.start_time:
# if other user_check is the first of the day, start_time is the end of other user_check
start_time = other_user_check.computed_end_time
if self.booking.event.agenda.invoicing_unit == 'minute':
return start_time
@ -3095,13 +3134,19 @@ class BookingCheck(models.Model):
# else take previous_slot
return previous_slot
def get_computed_end_time(self):
def get_computed_end_time(self, other_user_check=None, adjust_to_booking=True):
if self.end_time is None:
return None
end_time = self.end_time
if self.booking.end_time:
if self.booking.end_time and adjust_to_booking:
# adjust end_time to the end of the booking if requested
end_time = max(self.end_time, self.booking.end_time)
if other_user_check and other_user_check.computed_start_time and other_user_check.computed_end_time:
# if other user_check exists and is completely computed
if other_user_check.start_time > self.start_time:
# if other user_check is the second of the day, end_time is the start of other user_check
end_time = other_user_check.computed_start_time
if self.booking.event.agenda.invoicing_unit == 'minute':
return end_time
@ -3117,11 +3162,17 @@ class BookingCheck(models.Model):
# else take next_slot
return next_slot
def _refresh_computed_times(self):
def _refresh_computed_times(
self, other_user_check=None, adjust_start_to_booking=True, adjust_end_to_booking=True
):
old_computed_start_time = self.computed_start_time
old_computed_end_time = self.computed_end_time
self.computed_start_time = self.get_computed_start_time()
self.computed_end_time = self.get_computed_end_time()
self.computed_start_time = self.get_computed_start_time(
other_user_check=other_user_check, adjust_to_booking=adjust_start_to_booking
)
self.computed_end_time = self.get_computed_end_time(
other_user_check=other_user_check, adjust_to_booking=adjust_end_to_booking
)
# return True if changed, else False
if (
old_computed_start_time == self.computed_start_time

View File

@ -4035,6 +4035,197 @@ def test_booking_get_computed_end_time(end_time, user_check_end_time, tolerance,
assert booking.user_check.get_computed_end_time() == expected
def test_booking_refresh_computed_times_only_one_user_check():
agenda = Agenda.objects.create(
label='Agenda', kind='events', invoicing_unit='half_hour', invoicing_tolerance=10
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(
event=event, start_time=datetime.time(8, 0), end_time=datetime.time(17, 0)
)
# only one user_check, presence or absence, same behavior
booking_check = BookingCheck.objects.create(
booking=booking, presence=True, start_time=datetime.time(7, 45), end_time=datetime.time(17, 15)
)
booking.refresh_computed_times(commit=True)
booking_check.refresh_from_db()
assert booking_check.computed_start_time == datetime.time(7, 30)
assert booking_check.computed_end_time == datetime.time(17, 30)
booking_check.presence = False
booking.refresh_computed_times(commit=True)
booking_check.refresh_from_db()
assert booking_check.computed_start_time == datetime.time(7, 30)
assert booking_check.computed_end_time == datetime.time(17, 30)
@pytest.mark.parametrize(
'presence1, start_time1, end_time1, presence2, start_time2, end_time2, expected_start1, expected_end1, expected_start2, expected_end2',
[
# first presence, then absence
(
True,
datetime.time(7, 45),
datetime.time(12, 15),
False,
datetime.time(13, 0), # no overlapping
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30),
datetime.time(17, 0),
),
(
True,
datetime.time(7, 45),
datetime.time(12, 15),
False,
datetime.time(12, 15), # computed start should be 12H00
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30), # but it is adjusted to 12H30
datetime.time(17, 0),
),
# first absence, then presence
(
False,
datetime.time(7, 45),
datetime.time(12, 0), # no overlapping
True,
datetime.time(13, 15),
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(13, 0),
datetime.time(13, 0),
datetime.time(17, 0),
),
(
False,
datetime.time(7, 45),
datetime.time(12, 45), # computed end should be 13H00
True,
datetime.time(12, 45),
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30), # but it is adjusted to 12H30
datetime.time(12, 30),
datetime.time(17, 0),
),
# both absence
(
True,
datetime.time(7, 45),
datetime.time(12, 15),
True,
datetime.time(13, 0), # no overlapping
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30),
datetime.time(17, 0),
),
(
True,
datetime.time(7, 45),
datetime.time(12, 15),
True,
datetime.time(12, 15), # computed start should be 12H00
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30), # but it is adjusted to 12H30
datetime.time(17, 0),
),
# both presence
(
False,
datetime.time(7, 45),
datetime.time(12, 15),
False,
datetime.time(13, 0), # no overlapping
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30),
datetime.time(17, 0),
),
(
False,
datetime.time(7, 45),
datetime.time(12, 15),
False,
datetime.time(12, 15), # computed start should be 12H00
datetime.time(15, 0),
datetime.time(7, 30),
datetime.time(12, 30),
datetime.time(12, 30), # but it is adjusted to 12H30
datetime.time(17, 0),
),
],
)
def test_booking_refresh_computed_times_two_user_check(
presence1,
start_time1,
end_time1,
presence2,
start_time2,
end_time2,
expected_start1,
expected_end1,
expected_start2,
expected_end2,
):
agenda = Agenda.objects.create(
label='Agenda', kind='events', invoicing_unit='half_hour', invoicing_tolerance=10
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(
event=event, start_time=datetime.time(8, 0), end_time=datetime.time(17, 0)
)
user_check1 = BookingCheck.objects.create(
booking=booking, presence=presence1, start_time=start_time1, end_time=end_time1
)
user_check2 = BookingCheck.objects.create(
booking=booking, presence=presence2, start_time=start_time2, end_time=end_time2
)
booking.refresh_computed_times(commit=True)
user_check1.refresh_from_db()
user_check2.refresh_from_db()
assert user_check1.computed_start_time == expected_start1
assert user_check1.computed_end_time == expected_end1
assert user_check2.computed_start_time == expected_start2
assert user_check2.computed_end_time == expected_end2
def test_booking_refresh_computed_times_more_than_two_user_check():
agenda = Agenda.objects.create(
label='Agenda', kind='events', invoicing_unit='half_hour', invoicing_tolerance=10
)
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
booking = Booking.objects.create(
event=event, start_time=datetime.time(8, 0), end_time=datetime.time(17, 0)
)
BookingCheck.objects.create(booking=booking, presence=True)
BookingCheck.objects.create(booking=booking, presence=True)
BookingCheck.objects.create(booking=booking, presence=True)
with pytest.raises(AssertionError):
booking.refresh_computed_times(commit=True)
def test_agenda_refresh_booking_computed_times():
agenda = Agenda.objects.create(
label='Agenda',