manager: allow adding second check to partial booking (#80371)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Valentin Deniaud 2023-10-05 16:29:14 +02:00
parent 2e22706c08
commit 2a56ba5432
6 changed files with 141 additions and 41 deletions

View File

@ -3084,6 +3084,16 @@ class BookingCheck(models.Model):
return False
return True
def overlaps_existing_check(self, start_time, end_time):
booking_checks = BookingCheck.objects.filter(booking=self.booking).exclude(pk=self.pk)
if not booking_checks:
return False
if len(booking_checks) > 1:
raise ValueError('too many booking checks') # should not happen
return bool(start_time < booking_checks[0].end_time and end_time > booking_checks[0].start_time)
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])

View File

@ -639,6 +639,11 @@ class PartialBookingCheckForm(forms.ModelForm):
if self.cleaned_data['end_time'] <= self.cleaned_data['start_time']:
raise ValidationError(_('Arrival must be before departure.'))
if self.instance.overlaps_existing_check(
self.cleaned_data['start_time'], self.cleaned_data['end_time']
):
raise ValidationError(_('Booking check hours overlap existing check.'))
if self.cleaned_data['presence'] is not None:
kind = 'presence' if self.cleaned_data['presence'] else 'absence'
if f'{kind}_check_type' in self.cleaned_data:

View File

@ -17,6 +17,16 @@
data-fill-start_time="{{ object.booking.start_time|time:"H:i" }}"
data-fill-end_time="{{ object.booking.end_time|time:"H:i" }}"
>
{% if allow_adding_check %}
<p>
<a
rel="popup"
href="{% url 'chrono-manager-partial-booking-check' pk=agenda.pk booking_pk=object.booking.pk %}"
>
{% trans "Add second booking check" %}
</a>
</p>
{% endif %}
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">

View File

@ -82,36 +82,34 @@
</div>
{% if user.bookings %}
<div class="registrant--bar-container">
{% for booking in user.bookings %}
{% if booking.user_check %}
<a
class="registrant--bar clearfix check {{ booking.check_css_class }}"
title="{% trans "Checked period:" %}"
style="left: {{ booking.check_css_left }}%; width: {{ booking.check_css_width }}%;"
{% if allow_check %}
rel="popup"
href="{% url 'chrono-manager-partial-booking-update-check' pk=agenda.pk check_pk=booking.user_check.pk %}"
{% endif %}
>
<strong class="sr-only">{% trans "Checked period:" %}</strong>
<time class="start-time" datetime="{{ booking.user_check.start_time|time:"H:i" }}">{{ booking.user_check.start_time|time:"H:i" }}</time>
<time class="end-time" datetime="{{ booking.user_check.end_time|time:"H:i" }}">{{ booking.user_check.end_time|time:"H:i" }}</time>
{% if booking.user_check.type_label %}<span>{{ booking.user_check.type_label }}</span>{% endif %}
</a>
{% endif %}
{% for check in user.booking_checks %}
<a
class="registrant--bar clearfix check {{ check.css_class }}"
title="{% trans "Checked period:" %}"
style="left: {{ check.css_left }}%; width: {{ check.css_width }}%;"
{% if allow_check %}
rel="popup"
href="{% url 'chrono-manager-partial-booking-update-check' pk=agenda.pk check_pk=check.pk %}"
{% endif %}
>
<strong class="sr-only">{% trans "Checked period:" %}</strong>
<time class="start-time" datetime="{{ check.start_time|time:"H:i" }}">{{ check.start_time|time:"H:i" }}</time>
<time class="end-time" datetime="{{ check.end_time|time:"H:i" }}">{{ check.end_time|time:"H:i" }}</time>
{% if check.type_label %}<span>{{ check.type_label }}</span>{% endif %}
</a>
{% endfor %}
</div>
<div class="registrant--bar-container">
{% for booking in user.bookings %}
{% if booking.user_check.computed_start_time and booking.user_check.computed_end_time %}
{% for check in user.booking_checks %}
{% if check.computed_start_time and check.computed_end_time %}
<p
class="registrant--bar clearfix computed {{ booking.check_css_class }}"
class="registrant--bar clearfix computed {{ check.css_class }}"
title="{% trans "Computed period" %}"
style="left: {{ booking.user_check.computed_css_left }}%; width: {{ booking.user_check.computed_css_width }}%;"
style="left: {{ check.computed_css_left }}%; width: {{ check.computed_css_width }}%;"
>
<strong class="sr-only">{% trans "Computed period:" %}</strong>
<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>
<time class="start-time" datetime="{{ check.computed_start_time|time:"H:i" }}">{{ check.computed_start_time|time:"H:i" }}</time>
<time class="end-time" datetime="{{ check.computed_end_time|time:"H:i" }}">{{ check.computed_end_time|time:"H:i" }}</time>
</p>
{% endif %}
{% endfor %}

View File

@ -1394,7 +1394,7 @@ class EventChecksMixin:
filters = {k: sorted(list(v)) for k, v in filters}
return filters
def add_filters_context(self, context, event):
def add_filters_context(self, context, event, add_check_forms=True):
# booking base queryset
booking_qs_kwargs = {}
if not self.agenda.subscriptions.exists():
@ -1441,6 +1441,12 @@ class EventChecksMixin:
results = []
booked_without_status = False
for booking in booked_filterset.qs:
booking.kind = 'booking'
results.append(booking)
if not add_check_forms:
continue
if booking.cancellation_datetime is None and not booking.user_check:
booked_without_status = True
booking.absence_form = BookingCheckAbsenceForm(
@ -1451,17 +1457,19 @@ class EventChecksMixin:
agenda=self.agenda,
initial={'check_type': booking.user_check.type_slug if booking.user_check else None},
)
booking.kind = 'booking'
results.append(booking)
for subscription in subscription_filterset.qs:
subscription.kind = 'subscription'
results.append(subscription)
if not add_check_forms:
continue
subscription.absence_form = BookingCheckAbsenceForm(
agenda=self.agenda,
)
subscription.presence_form = BookingCheckPresenceForm(
agenda=self.agenda,
)
subscription.kind = 'subscription'
results.append(subscription)
# sort results
if (
booked_filterset.form.is_valid()
@ -1628,7 +1636,7 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
context['allow_check'] = (
not event.checked or not self.agenda.disable_check_update
) and not event.check_locked
self.add_filters_context(context, event)
self.add_filters_context(context, event, add_check_forms=False)
min_time = localtime(event.start_datetime).time()
max_time = event.end_time
@ -1652,18 +1660,15 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
booking.css_left = get_time_ratio(booking.start_time, start_time)
booking.css_width = get_time_ratio(booking.end_time, booking.start_time)
if booking.user_check:
booking.check_css_class = 'present' if booking.user_check.presence else 'absent'
booking.check_css_left = get_time_ratio(booking.user_check.start_time, start_time)
booking.check_css_width = get_time_ratio(
booking.user_check.end_time, booking.user_check.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
booking.user_check_list = list(booking.user_checks.all()) # queryset is prefetched
for check in booking.user_check_list:
check.css_class = 'present' if check.presence else 'absent'
check.css_left = get_time_ratio(check.start_time, start_time)
check.css_width = get_time_ratio(check.end_time, check.start_time)
if check.computed_start_time and check.computed_end_time:
check.computed_css_left = get_time_ratio(check.computed_start_time, start_time)
check.computed_css_width = get_time_ratio(
check.computed_end_time, check.computed_start_time
)
users_info = {}
@ -1673,6 +1678,7 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
{
'name': result.user_name,
'bookings': [],
'booking_checks': [],
},
)
if result.kind == 'subscription':
@ -1682,6 +1688,7 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
)
continue
user_info['bookings'].append(result)
user_info['booking_checks'].extend(result.user_check_list)
context['users'] = users_info.values()
@ -4559,6 +4566,11 @@ class PartialBookingUpdateCheckView(PartialBookingCheckMixin, UpdateView):
def get_object(self):
return super(PartialBookingCheckMixin, self).get_object()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['allow_adding_check'] = bool(self.object.booking.user_checks.count() == 1)
return context
partial_booking_update_check_view = PartialBookingUpdateCheckView.as_view()

View File

@ -456,6 +456,71 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=404)
def test_manager_partial_bookings_multiple_checks(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
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.objects.create(
user_external_id='xxx',
user_first_name='Jane',
user_last_name='Doe',
start_time=datetime.time(9, 00),
end_time=datetime.time(18, 00),
event=event,
)
app = login(app)
today = start_datetime.date()
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
resp = resp.click('Booked period')
assert 'Add second booking check' not in resp.text
resp.form['start_time'] = '09:30'
resp.form['end_time'] = '12:00'
resp.form['presence'] = 'True'
resp = resp.form.submit().follow()
assert len(resp.pyquery('.registrant--bar')) == 3
assert len(resp.pyquery('.registrant--bar.check')) == 1
assert len(resp.pyquery('.registrant--bar.computed')) == 1
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[0].text == '09:30'
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[1].text == '12:00'
resp = resp.click('Checked period')
resp = resp.click('Add second booking check')
resp.form['start_time'] = '12:30'
resp.form['end_time'] = '17:30'
resp.form['presence'] = 'False'
resp = resp.form.submit().follow()
assert len(resp.pyquery('.registrant--bar')) == 5
assert len(resp.pyquery('.registrant--bar.check')) == 2
assert len(resp.pyquery('.registrant--bar.computed')) == 2
assert resp.pyquery('.registrant--bar.check')[1].findall('time')[0].text == '12:30'
assert resp.pyquery('.registrant--bar.check')[1].findall('time')[1].text == '17:30'
resp = resp.click('Checked period', index=1)
assert 'Add second booking check' not in resp.text
resp.form['start_time'] = '11:30'
resp = resp.form.submit()
assert 'Booking check hours overlap existing check.' in resp.text
resp.form['start_time'] = '12:30'
resp.form['presence'] = ''
resp = resp.form.submit().follow()
assert len(resp.pyquery('.registrant--bar')) == 3
assert len(resp.pyquery('.registrant--bar.check')) == 1
assert len(resp.pyquery('.registrant--bar.computed')) == 1
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[0].text == '09:30'
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[1].text == '12:00'
@mock.patch('chrono.manager.forms.get_agenda_check_types')
def test_manager_partial_bookings_check_subscription(check_types, app, admin_user):
check_types.return_value = []