agendas: store computed times for partial bookings (#80877)

This commit is contained in:
Lauréline Guérin 2023-09-19 15:10:10 +02:00
parent 33e53a694a
commit 918903fc8c
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
10 changed files with 405 additions and 9 deletions

View File

@ -0,0 +1,20 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0159_partial_bookings_invoicing'),
]
operations = [
migrations.AddField(
model_name='booking',
name='computed_end_time',
field=models.TimeField(null=True),
),
migrations.AddField(
model_name='booking',
name='computed_start_time',
field=models.TimeField(null=True),
),
]

View File

@ -1586,6 +1586,41 @@ class Agenda(models.Model):
free_time += desk_free_time
return free_time
def async_refresh_booking_computed_times(self):
if self.kind != 'events' or not self.partial_bookings:
return
if 'uwsgi' in sys.modules:
from chrono.utils.spooler import refresh_booking_computed_times_from_agenda
tenant = getattr(connection, 'tenant', None)
transaction.on_commit(
lambda: refresh_booking_computed_times_from_agenda.spool(
agenda_id=str(self.pk), domain=getattr(tenant, 'domain_url', None)
)
)
return
self.refresh_booking_computed_times()
def refresh_booking_computed_times(self):
bookings_queryset = Booking.objects.filter(
event__agenda__kind='events',
event__agenda__partial_bookings=True,
event__agenda=self,
event__check_locked=False,
event__invoiced=False,
event__cancelled=False,
cancellation_datetime__isnull=True,
).select_related('event__agenda')
to_update = []
for booking in bookings_queryset:
changed = booking.refresh_computed_times()
if changed:
to_update.append(booking)
if to_update:
Booking.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
class VirtualMember(models.Model):
"""Trough model to link virtual agendas to their real agendas.
@ -2149,6 +2184,44 @@ class Event(models.Model):
except Exception as e: # noqa pylint: disable=broad-except
logging.error('error (%s) notifying checked booking (%s)', e, booking.id)
def async_refresh_booking_computed_times(self):
if self.agenda.kind != 'events' or not self.agenda.partial_bookings:
return
if self.check_locked or self.invoiced or self.cancelled:
return
if 'uwsgi' in sys.modules:
from chrono.utils.spooler import refresh_booking_computed_times_from_event
tenant = getattr(connection, 'tenant', None)
transaction.on_commit(
lambda: refresh_booking_computed_times_from_event.spool(
event_id=str(self.pk), domain=getattr(tenant, 'domain_url', None)
)
)
return
self.refresh_booking_computed_times()
def refresh_booking_computed_times(self):
bookings_queryset = Booking.objects.filter(
event__agenda__kind='events',
event__agenda__partial_bookings=True,
event=self,
event__check_locked=False,
event__invoiced=False,
event__cancelled=False,
cancellation_datetime__isnull=True,
)
to_update = []
for booking in bookings_queryset:
booking.event = self # to avoid lots of querysets
changed = booking.refresh_computed_times()
if changed:
to_update.append(booking)
if to_update:
Booking.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:
return False
@ -2737,6 +2810,8 @@ 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):
@ -2970,6 +3045,19 @@ class Booking(models.Model):
kwargs={'pk': agenda.pk, 'booking_pk': self.pk},
)
def refresh_computed_times(self):
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()
# return True if changed, else False
if (
old_computed_start_time == self.computed_start_time
and old_computed_end_time == self.computed_end_time
):
return False
return True
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])

View File

@ -285,8 +285,6 @@ class BookingSerializer(serializers.ModelSerializer):
self.instance.user_check_type_slug if self.instance.user_was_present is True else None
)
if self.instance.event.agenda.kind == 'events' and self.instance.event.agenda.partial_bookings:
self.instance.computed_start_time = self.instance.get_computed_start_time()
self.instance.computed_end_time = self.instance.get_computed_end_time()
for key in ['', 'user_check_', 'computed_']:
start_key, end_key, minutes_key = (
'%sstart_time' % key,

View File

@ -2248,6 +2248,9 @@ class MultipleAgendasEventsCheckLock(APIView):
start_datetime__lt=date_end,
)
events.update(check_locked=check_locked)
if check_locked is False:
for event in events:
event.async_refresh_booking_computed_times()
return Response({'err': 0})

View File

@ -649,6 +649,7 @@ class PartialBookingCheckForm(forms.ModelForm):
if hasattr(self, 'check_type_slug'):
self.instance.user_check_type_slug = self.check_type_slug
self.instance.user_check_type_label = self.check_type_label
self.instance.refresh_computed_times()
return super().save()
@ -1560,6 +1561,11 @@ class AgendaInvoicingSettingsForm(forms.ModelForm):
'invoicing_tolerance',
]
def save(self):
super().save()
self.instance.async_refresh_booking_computed_times()
return self.instance
class AgendaNotificationsForm(forms.ModelForm):
class Meta:

View File

@ -1662,8 +1662,6 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
booking.check_css_width = get_time_ratio(
booking.user_check_end_time, booking.user_check_start_time
)
booking.computed_start_time = booking.get_computed_start_time()
booking.computed_end_time = booking.get_computed_end_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(

View File

@ -17,7 +17,7 @@
from django.db import connection
from uwsgidecorators import spool # pylint: disable=import-error
from chrono.agendas.models import Event, ICSError, TimePeriodExceptionSource
from chrono.agendas.models import Agenda, Event, ICSError, TimePeriodExceptionSource
def set_connection(domain):
@ -83,3 +83,31 @@ def ants_hub_city_push(args):
City.push()
except Exception: # noqa pylint: disable=broad-except
pass
@spool
def refresh_booking_computed_times_from_agenda(args):
if args.get('domain'):
# multitenant installation
set_connection(args['domain'])
try:
agenda = Agenda.objects.get(pk=args['agenda_id'])
except Agenda.DoesNotExist:
return
agenda.refresh_booking_computed_times()
@spool
def refresh_booking_computed_times_from_event(args):
if args.get('domain'):
# multitenant installation
set_connection(args['domain'])
try:
event = Event.objects.get(pk=args['event_id'])
except Event.DoesNotExist:
return
event.refresh_booking_computed_times()

View File

@ -1404,8 +1404,10 @@ def test_events_check_status_events(app, user, partial_bookings):
user_check_start_time=datetime.time(7, 55),
user_check_end_time=datetime.time(17, 15),
)
assert booking1.get_computed_start_time() == datetime.time(8, 0)
assert booking1.get_computed_end_time() == datetime.time(17, 30)
booking1.refresh_computed_times()
booking1.save()
assert booking1.computed_start_time == datetime.time(8, 0)
assert booking1.computed_end_time == datetime.time(17, 30)
booking2 = Booking.objects.create(
event=event, user_external_id='child:42', user_was_present=True, user_check_type_slug='foo-reason'
)
@ -1842,9 +1844,12 @@ def test_events_check_lock_params(app, user):
assert 'wrong format' in resp.json['errors']['date_end'][0]
@pytest.mark.parametrize('partial_bookings', [True, False])
@pytest.mark.freeze_time('2022-05-30 14:00')
def test_events_check_lock(app, user):
agenda = Agenda.objects.create(label='Foo')
def test_events_check_lock(app, user, partial_bookings):
agenda = Agenda.objects.create(label='Foo', partial_bookings=partial_bookings)
assert agenda.invoicing_unit == 'hour'
assert agenda.invoicing_tolerance == 0
event = Event.objects.create(
slug='event-slug',
label='Event Label',
@ -1853,6 +1858,19 @@ def test_events_check_lock(app, user):
agenda=agenda,
checked=True,
)
booking = Booking.objects.create(
event=event,
user_external_id='child:42',
user_was_present=True,
user_check_type_slug='foo-reason',
start_time=datetime.time(8, 0),
end_time=datetime.time(17, 0),
user_check_start_time=datetime.time(7, 55),
user_check_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
app.authorization = ('Basic', ('john.doe', 'password'))
url = '/api/agendas/events/check-lock/'
@ -1866,12 +1884,24 @@ def test_events_check_lock(app, user):
assert resp.json['err'] == 0
event.refresh_from_db()
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
params['check_locked'] = False
resp = app.post_json(url, params=params)
assert resp.json['err'] == 0
event.refresh_from_db()
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)
else:
# not refreshed, not a partial_bookings agenda
assert booking.computed_start_time is None
assert booking.computed_end_time is None
@pytest.mark.freeze_time('2022-05-30 14:00')

View File

@ -40,6 +40,24 @@ def test_options_partial_bookings_invoicing_settings(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
assert agenda.invoicing_unit == 'hour'
assert agenda.invoicing_tolerance == 0
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
event = Event.objects.create(
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
)
booking = Booking.objects.create(
user_external_id='xxx',
user_first_name='Jane',
user_last_name='Doe',
start_time=datetime.time(11, 00),
end_time=datetime.time(13, 30),
user_check_start_time=datetime.time(10, 55),
user_check_end_time=datetime.time(14, 5),
event=event,
)
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)
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
@ -57,6 +75,9 @@ def test_options_partial_bookings_invoicing_settings(app, admin_user):
agenda.refresh_from_db()
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)
# check kind
agenda.partial_bookings = False
@ -284,6 +305,11 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
resp.form['user_check_end_time'] = '13:15'
resp.form['user_was_present'] = 'True'
resp = resp.form.submit().follow()
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 len(resp.pyquery('.registrant--bar')) == 3
assert len(resp.pyquery('.registrant--bar.booking')) == 1
@ -300,6 +326,12 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
agenda.invoicing_unit = 'half_hour'
agenda.invoicing_tolerance = 10
agenda.save()
agenda.refresh_booking_computed_times()
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)
check_types.return_value = [
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),

View File

@ -4031,3 +4031,196 @@ def test_booking_get_computed_end_time(end_time, user_check_end_time, tolerance,
booking = Booking.objects.create(event=event, end_time=end_time, user_check_end_time=user_check_end_time)
assert booking.get_computed_end_time() == expected
def test_agenda_refresh_booking_computed_times():
agenda = Agenda.objects.create(
label='Agenda',
kind='events',
partial_bookings=True,
)
agenda2 = Agenda.objects.create(label='other')
assert agenda.invoicing_unit == 'hour'
assert agenda.invoicing_tolerance == 0
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_check_start_time=datetime.time(7, 55),
user_check_end_time=datetime.time(17, 15),
)
def reset_booking():
booking.computed_start_time = None
booking.computed_end_time = None
booking.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)
reset_booking()
else:
assert booking.computed_start_time is booking.computed_end_time is None
test_booking(True)
# wrong agenda kind
agenda.partial_bookings = False
agenda.save()
test_booking(False)
agenda.partial_bookings = True
agenda.kind = 'meetings'
agenda.save()
test_booking(False)
agenda.kind = 'virtual'
agenda.save()
test_booking(False)
# reset kind
agenda.kind = 'events'
agenda.save()
test_booking(True)
# wrong agenda
event.agenda = agenda2
event.save()
test_booking(False)
# event check locked
event.agenda = agenda
event.check_locked = True
event.save()
test_booking(False)
# event invoiced
event.check_locked = False
event.invoiced = True
event.save()
test_booking(False)
# event cancelled
event.invoiced = False
event.cancelled = True
event.save()
test_booking(False)
# booking cancelled
event.cancelled = False
event.save()
booking.cancellation_datetime = now()
booking.save()
test_booking(False)
# ok
booking.cancellation_datetime = None
booking.save()
test_booking(True)
def test_event_refresh_booking_computed_times():
agenda = Agenda.objects.create(
label='Agenda',
kind='events',
partial_bookings=True,
)
assert agenda.invoicing_unit == 'hour'
assert agenda.invoicing_tolerance == 0
event = Event.objects.create(
agenda=agenda,
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
places=10,
)
event2 = 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_check_start_time=datetime.time(7, 55),
user_check_end_time=datetime.time(17, 15),
)
def reset_booking():
booking.computed_start_time = None
booking.computed_end_time = None
booking.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)
reset_booking()
else:
assert booking.computed_start_time is booking.computed_end_time is None
test_booking(True)
# wrong agenda kind
agenda.partial_bookings = False
agenda.save()
test_booking(False)
agenda.partial_bookings = True
agenda.kind = 'meetings'
agenda.save()
test_booking(False)
agenda.kind = 'virtual'
agenda.save()
test_booking(False)
# reset kind
agenda.kind = 'events'
agenda.save()
test_booking(True)
# wrong event
booking.event = event2
booking.save()
test_booking(False)
# event check locked
booking.event = event
booking.save()
event.check_locked = True
event.save()
test_booking(False)
# event invoiced
event.check_locked = False
event.invoiced = True
event.save()
test_booking(False)
# event cancelled
event.invoiced = False
event.cancelled = True
event.save()
test_booking(False)
# booking cancelled
event.cancelled = False
event.save()
booking.cancellation_datetime = now()
booking.save()
test_booking(False)
# ok
booking.cancellation_datetime = None
booking.save()
test_booking(True)