agendas: store computed start/end times on booking check (#80371)

This commit is contained in:
Valentin Deniaud 2023-10-04 17:07:19 +02:00
parent ec497c66d9
commit 3cb80d478a
12 changed files with 109 additions and 85 deletions

View File

@ -20,6 +20,8 @@ class Migration(migrations.Migration):
('presence', models.BooleanField()),
('start_time', models.TimeField(null=True, verbose_name='Arrival')),
('end_time', models.TimeField(null=True, verbose_name='Departure')),
('computed_end_time', models.TimeField(null=True)),
('computed_start_time', models.TimeField(null=True)),
('type_slug', models.CharField(blank=True, max_length=160, null=True)),
('type_label', models.CharField(blank=True, max_length=150, null=True)),
(

View File

@ -15,6 +15,8 @@ def migrate_booking_check_data(apps, schema_editor):
presence=booking.user_was_present,
start_time=booking.user_check_start_time,
end_time=booking.user_check_end_time,
computed_start_time=booking.computed_start_time,
computed_end_time=booking.computed_end_time,
type_slug=booking.user_check_type_slug,
type_label=booking.user_check_type_label,
)
@ -31,6 +33,8 @@ def reverse_migrate_booking_check_data(apps, schema_editor):
booking.user_was_present = booking.user_check.presence
booking.user_check_start_time = booking.user_check.start_time
booking.user_check_end_time = booking.user_check.end_time
booking.computed_start_time = booking.computed_start_time
booking.computed_end_time = booking.computed_end_time
booking.user_check_type_slug = booking.user_check.type_slug
booking.user_check_type_label = booking.user_check.type_label
@ -40,6 +44,8 @@ def reverse_migrate_booking_check_data(apps, schema_editor):
'user_was_present',
'user_check_start_time',
'user_check_end_time',
'computed_start_time',
'computed_end_time',
'user_check_type_slug',
'user_check_type_label',
],

View File

@ -29,4 +29,12 @@ class Migration(migrations.Migration):
model_name='booking',
name='user_was_present',
),
migrations.RemoveField(
model_name='booking',
name='computed_end_time',
),
migrations.RemoveField(
model_name='booking',
name='computed_start_time',
),
]

View File

@ -1612,14 +1612,17 @@ class Agenda(models.Model):
event__invoiced=False,
event__cancelled=False,
cancellation_datetime__isnull=True,
).select_related('event__agenda')
)
booking_checks = BookingCheck.objects.filter(booking__in=bookings_queryset).select_related(
'booking', 'booking__event__agenda'
)
to_update = []
for booking in bookings_queryset:
changed = booking.refresh_computed_times()
for booking_check in booking_checks:
changed = booking_check.refresh_computed_times()
if changed:
to_update.append(booking)
to_update.append(booking_check)
if to_update:
Booking.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
BookingCheck.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
class VirtualMember(models.Model):
@ -2214,14 +2217,15 @@ class Event(models.Model):
event__cancelled=False,
cancellation_datetime__isnull=True,
)
booking_checks = BookingCheck.objects.filter(booking__in=bookings_queryset).select_related('booking')
to_update = []
for booking in bookings_queryset:
booking.event = self # to avoid lots of querysets
changed = booking.refresh_computed_times()
for booking_check in booking_checks:
booking_check.booking.event = self # to avoid lots of querysets
changed = booking_check.refresh_computed_times()
if changed:
to_update.append(booking)
to_update.append(booking_check)
if to_update:
Booking.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
BookingCheck.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
def in_bookable_period(self, bypass_delays=False):
if self.publication_datetime and now() < self.publication_datetime:
@ -2810,8 +2814,6 @@ class Booking(models.Model):
start_time = models.TimeField(null=True)
end_time = models.TimeField(null=True)
computed_start_time = models.TimeField(null=True)
computed_end_time = models.TimeField(null=True)
@property
def user_name(self):
@ -2984,12 +2986,26 @@ class Booking(models.Model):
def get_backoffice_url(self):
return translate_from_publik_url(self.backoffice_url)
class BookingCheck(models.Model):
booking = models.OneToOneField(Booking, on_delete=models.CASCADE, related_name='user_check')
presence = models.BooleanField()
start_time = models.TimeField(_('Arrival'), null=True)
end_time = models.TimeField(_('Departure'), null=True)
computed_start_time = models.TimeField(null=True)
computed_end_time = models.TimeField(null=True)
type_slug = models.CharField(max_length=160, blank=True, null=True)
type_label = models.CharField(max_length=150, blank=True, null=True)
def _get_previous_and_next_slots(self, _time):
minutes = {
'hour': 60,
'half_hour': 30,
'quarter': 15,
}[self.event.agenda.invoicing_unit]
}[self.booking.event.agenda.invoicing_unit]
time_minutes = _time.hour * 60 + _time.minute
previous_slot_minutes = math.trunc(time_minutes / minutes) * minutes
@ -2998,16 +3014,16 @@ class Booking(models.Model):
return previous_slot, next_slot
def get_computed_start_time(self):
if not hasattr(self, 'user_check') or self.user_check.start_time is None:
if self.start_time is None:
return None
start_time = self.user_check.start_time
if self.start_time:
start_time = min(start_time, self.start_time)
if self.event.agenda.invoicing_unit == 'minute':
start_time = self.start_time
if self.booking.start_time:
start_time = min(self.start_time, self.booking.start_time)
if self.booking.event.agenda.invoicing_unit == 'minute':
return start_time
tolerance = self.event.agenda.invoicing_tolerance
tolerance = self.booking.event.agenda.invoicing_tolerance
# compute previous and next slot
previous_slot, next_slot = self._get_previous_and_next_slots(start_time)
@ -3020,16 +3036,16 @@ class Booking(models.Model):
return previous_slot
def get_computed_end_time(self):
if not hasattr(self, 'user_check') or self.user_check.end_time is None:
if self.end_time is None:
return None
end_time = self.user_check.end_time
if self.end_time:
end_time = max(end_time, self.end_time)
if self.event.agenda.invoicing_unit == 'minute':
end_time = self.end_time
if self.booking.end_time:
end_time = max(self.end_time, self.booking.end_time)
if self.booking.event.agenda.invoicing_unit == 'minute':
return end_time
tolerance = self.event.agenda.invoicing_tolerance
tolerance = self.booking.event.agenda.invoicing_tolerance
# compute previous and next slot
previous_slot, next_slot = self._get_previous_and_next_slots(end_time)
@ -3055,18 +3071,6 @@ class Booking(models.Model):
return True
class BookingCheck(models.Model):
booking = models.OneToOneField(Booking, on_delete=models.CASCADE, related_name='user_check')
presence = models.BooleanField()
start_time = models.TimeField(_('Arrival'), null=True)
end_time = models.TimeField(_('Departure'), null=True)
type_slug = models.CharField(max_length=160, blank=True, null=True)
type_label = models.CharField(max_length=150, blank=True, null=True)
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])

View File

@ -344,6 +344,8 @@ class BookingSerializer(serializers.ModelSerializer):
if self.instance.event.agenda.kind == 'events' and self.instance.event.agenda.partial_bookings:
self.instance.user_check_start_time = self.instance.user_check.start_time
self.instance.user_check_end_time = self.instance.user_check.end_time
self.instance.computed_start_time = self.instance.user_check.computed_start_time
self.instance.computed_end_time = self.instance.user_check.computed_end_time
for key in ['', 'user_check_', 'computed_']:
start_key, end_key, minutes_key = (
'%sstart_time' % key,

View File

@ -652,13 +652,9 @@ class PartialBookingCheckForm(forms.ModelForm):
self.instance.delete()
return self.instance
super().save()
self.instance.refresh_computed_times()
self.instance.booking.refresh_from_db()
if self.instance.booking.refresh_computed_times():
self.instance.booking.save()
return self.instance
return super().save()
class EventsTimesheetForm(forms.Form):

View File

@ -103,15 +103,15 @@
</div>
<div class="registrant--bar-container">
{% for booking in user.bookings %}
{% if booking.user_check and booking.computed_start_time != None and booking.computed_end_time != None %}
{% if booking.user_check.computed_start_time and booking.user_check.computed_end_time %}
<p
class="registrant--bar clearfix computed {{ booking.check_css_class }}"
title="{% trans "Computed period" %}"
style="left: {{ booking.computed_css_left }}%; width: {{ booking.computed_css_width }}%;"
style="left: {{ booking.user_check.computed_css_left }}%; width: {{ booking.user_check.computed_css_width }}%;"
>
<strong class="sr-only">{% trans "Computed period:" %}</strong>
<time class="start-time" datetime="{{ booking.computed_start_time|time:"H:i" }}">{{ booking.computed_start_time|time:"H:i" }}</time>
<time class="end-time" datetime="{{ booking.computed_end_time|time:"H:i" }}">{{ booking.computed_end_time|time:"H:i" }}</time>
<time class="start-time" datetime="{{ booking.user_check.computed_start_time|time:"H:i" }}">{{ booking.user_check.computed_start_time|time:"H:i" }}</time>
<time class="end-time" datetime="{{ booking.user_check.computed_end_time|time:"H:i" }}">{{ booking.user_check.computed_end_time|time:"H:i" }}</time>
</p>
{% endif %}
{% endfor %}

View File

@ -1662,10 +1662,12 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
booking.check_css_width = get_time_ratio(
booking.user_check.end_time, booking.user_check.start_time
)
if booking.computed_start_time is not None and booking.computed_end_time is not None:
booking.computed_css_left = get_time_ratio(booking.computed_start_time, start_time)
booking.computed_css_width = get_time_ratio(
booking.computed_end_time, booking.computed_start_time
if booking.user_check.computed_start_time and booking.user_check.computed_end_time:
booking.user_check.computed_css_left = get_time_ratio(
booking.user_check.computed_start_time, start_time
)
booking.user_check.computed_css_width = get_time_ratio(
booking.user_check.computed_end_time, booking.user_check.computed_start_time
)
users_info = {}

View File

@ -1398,10 +1398,10 @@ def test_events_check_status_events(app, user, partial_bookings):
booking1.mark_user_presence(
check_type_slug='foo-reason', start_time=datetime.time(7, 55), end_time=datetime.time(17, 15)
)
booking1.refresh_computed_times()
booking1.save()
assert booking1.computed_start_time == datetime.time(8, 0)
assert booking1.computed_end_time == datetime.time(17, 30)
booking1.user_check.refresh_computed_times()
booking1.user_check.save()
assert booking1.user_check.computed_start_time == datetime.time(8, 0)
assert booking1.user_check.computed_end_time == datetime.time(17, 30)
booking2 = Booking.objects.create(event=event, user_external_id='child:42')
booking2.mark_user_presence(check_type_slug='foo-reason')
@ -1863,8 +1863,8 @@ def test_events_check_lock(app, user, partial_bookings):
end_time=datetime.time(17, 15),
)
# computed times are None, because refresh_computed_times was not called in this test
assert booking.computed_start_time is None
assert booking.computed_end_time is None
assert booking.user_check.computed_start_time is None
assert booking.user_check.computed_end_time is None
app.authorization = ('Basic', ('john.doe', 'password'))
url = '/api/agendas/events/check-lock/'
@ -1880,8 +1880,8 @@ def test_events_check_lock(app, user, partial_bookings):
assert event.check_locked is True
booking.refresh_from_db()
# computed times are still None, refresh_computed_times is not called on lock
assert booking.computed_start_time is None
assert booking.computed_end_time is None
assert booking.user_check.computed_start_time is None
assert booking.user_check.computed_end_time is None
params['check_locked'] = False
resp = app.post_json(url, params=params)
@ -1890,12 +1890,12 @@ def test_events_check_lock(app, user, partial_bookings):
assert event.check_locked is False
booking.refresh_from_db()
if partial_bookings:
assert booking.computed_start_time == datetime.time(7, 0)
assert booking.computed_end_time == datetime.time(18, 0)
assert booking.user_check.computed_start_time == datetime.time(7, 0)
assert booking.user_check.computed_end_time == datetime.time(18, 0)
else:
# not refreshed, not a partial_bookings agenda
assert booking.computed_start_time is None
assert booking.computed_end_time is None
assert booking.user_check.computed_start_time is None
assert booking.user_check.computed_end_time is None
@pytest.mark.freeze_time('2022-05-30 14:00')

View File

@ -56,8 +56,8 @@ def test_options_partial_bookings_invoicing_settings(app, admin_user):
booking.mark_user_presence(start_time=datetime.time(10, 55), end_time=datetime.time(14, 4))
agenda.refresh_booking_computed_times()
booking.refresh_from_db()
assert booking.computed_start_time == datetime.time(10, 0)
assert booking.computed_end_time == datetime.time(15, 0)
assert booking.user_check.computed_start_time == datetime.time(10, 0)
assert booking.user_check.computed_end_time == datetime.time(15, 0)
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
@ -76,8 +76,8 @@ def test_options_partial_bookings_invoicing_settings(app, admin_user):
assert agenda.invoicing_unit == 'half_hour'
assert agenda.invoicing_tolerance == 10
booking.refresh_from_db()
assert booking.computed_start_time == datetime.time(11, 0)
assert booking.computed_end_time == datetime.time(14, 0)
assert booking.user_check.computed_start_time == datetime.time(11, 0)
assert booking.user_check.computed_end_time == datetime.time(14, 0)
# check kind
agenda.partial_bookings = False
@ -357,8 +357,8 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
booking.refresh_from_db()
assert booking.user_check.start_time == datetime.time(11, 1)
assert booking.user_check.end_time == datetime.time(13, 15)
assert booking.computed_start_time == datetime.time(11, 0)
assert booking.computed_end_time == datetime.time(14, 0)
assert booking.user_check.computed_start_time == datetime.time(11, 0)
assert booking.user_check.computed_end_time == datetime.time(14, 0)
assert len(resp.pyquery('.registrant--bar')) == 3
assert len(resp.pyquery('.registrant--bar.booking')) == 1
@ -379,8 +379,8 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
booking.refresh_from_db()
assert booking.user_check.start_time == datetime.time(11, 1)
assert booking.user_check.end_time == datetime.time(13, 15)
assert booking.computed_start_time == datetime.time(11, 0)
assert booking.computed_end_time == datetime.time(13, 30)
assert booking.user_check.computed_start_time == datetime.time(11, 0)
assert booking.user_check.computed_end_time == datetime.time(13, 30)
check_types.return_value = [
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),

View File

@ -3973,7 +3973,7 @@ def test_booking_get_computed_start_time(start_time, user_check_start_time, tole
booking = Booking.objects.create(event=event, start_time=start_time)
booking.mark_user_presence(start_time=user_check_start_time)
assert booking.get_computed_start_time() == expected
assert booking.user_check.get_computed_start_time() == expected
@pytest.mark.parametrize(
@ -4030,7 +4030,7 @@ def test_booking_get_computed_end_time(end_time, user_check_end_time, tolerance,
booking = Booking.objects.create(event=event, end_time=end_time)
booking.mark_user_presence(end_time=user_check_end_time)
assert booking.get_computed_end_time() == expected
assert booking.user_check.get_computed_end_time() == expected
def test_agenda_refresh_booking_computed_times():
@ -4055,19 +4055,19 @@ def test_agenda_refresh_booking_computed_times():
booking.mark_user_presence(start_time=datetime.time(7, 55), end_time=datetime.time(17, 15))
def reset_booking():
booking.computed_start_time = None
booking.computed_end_time = None
booking.save()
booking.user_check.computed_start_time = None
booking.user_check.computed_end_time = None
booking.user_check.save()
def test_booking(success):
agenda.refresh_booking_computed_times()
booking.refresh_from_db()
if success is True:
assert booking.computed_start_time == datetime.time(7, 0)
assert booking.computed_end_time == datetime.time(18, 0)
assert booking.user_check.computed_start_time == datetime.time(7, 0)
assert booking.user_check.computed_end_time == datetime.time(18, 0)
reset_booking()
else:
assert booking.computed_start_time is booking.computed_end_time is None
assert booking.user_check.computed_start_time is booking.user_check.computed_end_time is None
test_booking(True)
@ -4152,19 +4152,19 @@ def test_event_refresh_booking_computed_times():
booking.mark_user_presence(start_time=datetime.time(7, 55), end_time=datetime.time(17, 15))
def reset_booking():
booking.computed_start_time = None
booking.computed_end_time = None
booking.save()
booking.user_check.computed_start_time = None
booking.user_check.computed_end_time = None
booking.user_check.save()
def test_booking(success):
event.refresh_booking_computed_times()
booking.refresh_from_db()
if success is True:
assert booking.computed_start_time == datetime.time(7, 0)
assert booking.computed_end_time == datetime.time(18, 0)
assert booking.user_check.computed_start_time == datetime.time(7, 0)
assert booking.user_check.computed_end_time == datetime.time(18, 0)
reset_booking()
else:
assert booking.computed_start_time is booking.computed_end_time is None
assert booking.user_check.computed_start_time is booking.user_check.computed_end_time is None
test_booking(True)

View File

@ -418,6 +418,8 @@ def test_migration_booking_check_data(transactional_db):
user_check_type_label='XXX',
user_check_start_time=datetime.time(12, 0),
user_check_end_time=datetime.time(14, 0),
computed_start_time=datetime.time(12, 30),
computed_end_time=datetime.time(14, 30),
)
executor = MigrationExecutor(connection)
@ -437,3 +439,5 @@ def test_migration_booking_check_data(transactional_db):
assert with_check_type.user_check.type_label == 'XXX'
assert with_check_type.user_check.start_time == datetime.time(12, 0)
assert with_check_type.user_check.end_time == datetime.time(14, 0)
assert with_check_type.computed_start_time == datetime.time(12, 30)
assert with_check_type.computed_end_time == datetime.time(14, 30)