agendas: store computed times for partial bookings (#80877)
This commit is contained in:
parent
33e53a694a
commit
918903fc8c
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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})
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue