manager: allow adding second check to partial booking (#80371)
gitea/chrono/pipeline/head This commit looks good
Details
gitea/chrono/pipeline/head This commit looks good
Details
This commit is contained in:
parent
2e22706c08
commit
2a56ba5432
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue