manager: allow booking check in partial bookings agendas (#78081)
gitea/chrono/pipeline/head This commit looks good
Details
gitea/chrono/pipeline/head This commit looks good
Details
This commit is contained in:
parent
beb31a38ca
commit
46e60b37a1
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.2.18 on 2023-07-05 10:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0157_convert_week_days'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='user_check_end_time',
|
||||
field=models.TimeField(null=True, verbose_name='Departure'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='user_check_start_time',
|
||||
field=models.TimeField(null=True, verbose_name='Arrival'),
|
||||
),
|
||||
]
|
|
@ -2696,6 +2696,8 @@ class Booking(models.Model):
|
|||
user_was_present = models.BooleanField(null=True)
|
||||
user_check_type_slug = models.CharField(max_length=160, blank=True, null=True)
|
||||
user_check_type_label = models.CharField(max_length=150, blank=True, null=True)
|
||||
user_check_start_time = models.TimeField(_('Arrival'), null=True)
|
||||
user_check_end_time = models.TimeField(_('Departure'), null=True)
|
||||
out_of_min_delay = models.BooleanField(default=False)
|
||||
|
||||
extra_emails = ArrayField(models.EmailField(), default=list)
|
||||
|
|
|
@ -576,6 +576,54 @@ class BookingCheckPresenceForm(forms.Form):
|
|||
]
|
||||
|
||||
|
||||
class PartialBookingCheckForm(forms.ModelForm):
|
||||
user_was_present = forms.NullBooleanField(
|
||||
label=_('Status'),
|
||||
widget=forms.RadioSelect(
|
||||
choices=(
|
||||
(None, _('Not checked')),
|
||||
(True, _('Present')),
|
||||
(False, _('Absent')),
|
||||
)
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
presence_check_type = forms.ChoiceField(label=_('Type'), required=False)
|
||||
absence_check_type = forms.ChoiceField(label=_('Type'), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Booking
|
||||
fields = ['user_check_start_time', 'user_check_end_time', 'user_was_present']
|
||||
widgets = {
|
||||
'user_check_start_time': widgets.TimeWidget(),
|
||||
'user_check_end_time': widgets.TimeWidget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.check_types = get_agenda_check_types(self.instance.event.agenda)
|
||||
self.fields['presence_check_type'].choices = [(None, '---------')] + [
|
||||
(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'presence'
|
||||
]
|
||||
self.fields['presence_check_type'].initial = self.instance.user_check_type_slug
|
||||
self.fields['absence_check_type'].choices = [(None, '---------')] + [
|
||||
(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'absence'
|
||||
]
|
||||
self.fields['absence_check_type'].initial = self.instance.user_check_type_slug
|
||||
|
||||
def clean(self):
|
||||
if 'user_was_present' in self.cleaned_data:
|
||||
kind = 'presence' if self.cleaned_data['user_was_present'] else 'absence'
|
||||
self.check_type_slug = self.cleaned_data[f'{kind}_check_type']
|
||||
self.check_type_label = dict(self.fields[f'{kind}_check_type'].choices).get(self.check_type_slug)
|
||||
|
||||
def save(self):
|
||||
if 'user_was_present' in self.cleaned_data:
|
||||
self.instance.user_check_type_slug = self.check_type_slug
|
||||
self.instance.user_check_type_label = self.check_type_label
|
||||
return super().save()
|
||||
|
||||
|
||||
class EventsTimesheetForm(forms.Form):
|
||||
date_start = forms.DateField(
|
||||
label=_('Start date'),
|
||||
|
|
|
@ -676,7 +676,6 @@ div#appbar a.active {
|
|||
padding: .33rem 0;
|
||||
}
|
||||
&--bar {
|
||||
--background: #1066bc;
|
||||
--color: white;
|
||||
box-sizing: border-box;
|
||||
margin: 0.33rem 0;
|
||||
|
@ -684,6 +683,15 @@ div#appbar a.active {
|
|||
padding: 0.33em 0.66em;
|
||||
background-color: var(--background);
|
||||
color: var(--color);
|
||||
&.booking {
|
||||
--background: #1066bc;
|
||||
}
|
||||
&.check.present {
|
||||
--background: hsl(120, 57%, 35%);
|
||||
}
|
||||
&.check.absent {
|
||||
--background: hsl(355, 80%, 45%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "chrono/manager_agenda_view.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="">{% trans "Check booking" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Check booking" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button class="submit-button">{% trans "Save" %}</button>
|
||||
<a class="cancel" href="{{ agenda.get_absolute_url }}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
presence_check_type_select = $('.widget[id=id_presence_check_type_p]');
|
||||
absence_check_type_select = $('.widget[id=id_absence_check_type_p]');
|
||||
$('input[type=radio][name=user_was_present]').change(function() {
|
||||
if (!this.checked)
|
||||
return;
|
||||
if (this.value == 'True') {
|
||||
presence_check_type_select.show();
|
||||
absence_check_type_select.hide();
|
||||
} else if (this.value == 'False') {
|
||||
absence_check_type_select.show();
|
||||
presence_check_type_select.hide();
|
||||
} else {
|
||||
presence_check_type_select.hide();
|
||||
absence_check_type_select.hide();
|
||||
}
|
||||
}).change();
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -18,7 +18,12 @@
|
|||
<div class="partial-booking--registrant-items">
|
||||
{% for booking in bookings %}
|
||||
<section class="partial-booking--registrant">
|
||||
<h3 class="registrant--name">{{ booking.get_user_block }}</h3>
|
||||
<h3 class="registrant--name">
|
||||
<a
|
||||
rel="popup"
|
||||
href="{% url 'chrono-manager-partial-booking-check' pk=agenda.pk booking_pk=booking.pk %}"
|
||||
>{{ booking.get_user_block }}</a>
|
||||
</h3>
|
||||
<div class="registrant--datas">
|
||||
<div class="registrant--bar-container">
|
||||
<p
|
||||
|
@ -32,6 +37,21 @@
|
|||
<time datetime="{{ booking.end_time|time:"H:i" }}">{{ booking.end_time|time:"H:i" }}</time>
|
||||
</p>
|
||||
</div>
|
||||
{% if booking.user_was_present is not None %}
|
||||
<div class="registrant--bar-container">
|
||||
<p
|
||||
class="registrant--bar check {{ booking.check_css_class }}"
|
||||
title="{% trans "Checked period:" %}"
|
||||
style="left: {{ booking.check_css_left }}%; width: {{ booking.check_css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Checked period:" %}</strong>
|
||||
<time datetime="{{ booking.user_check_start_time|time:"H:i" }}">{{ booking.user_check_start_time|time:"H:i" }}</time>
|
||||
–
|
||||
<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 %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
|
|
@ -429,6 +429,11 @@ urlpatterns = [
|
|||
views.booking_extra_user_block,
|
||||
name='chrono-manager-booking-extra-user-block',
|
||||
),
|
||||
path(
|
||||
'agendas/<int:pk>/bookings/<int:booking_pk>/check',
|
||||
views.partial_booking_check_view,
|
||||
name='chrono-manager-partial-booking-check',
|
||||
),
|
||||
path(
|
||||
'agendas/<int:pk>/subscriptions/<int:subscription_pk>/extra-user-block',
|
||||
views.subscription_extra_user_block,
|
||||
|
|
|
@ -124,6 +124,7 @@ from .forms import (
|
|||
NewEventForm,
|
||||
NewMeetingTypeForm,
|
||||
NewTimePeriodExceptionForm,
|
||||
PartialBookingCheckForm,
|
||||
SharedCustodyHolidayRuleForm,
|
||||
SharedCustodyPeriodForm,
|
||||
SharedCustodyRuleForm,
|
||||
|
@ -1523,6 +1524,13 @@ class AgendaDayView(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_was_present is not None:
|
||||
booking.check_css_class = 'present' if booking.user_was_present 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
|
||||
)
|
||||
|
||||
|
||||
agenda_day_view = AgendaDayView.as_view()
|
||||
|
||||
|
@ -4346,6 +4354,23 @@ class SharedCustodySettingsView(UpdateView):
|
|||
shared_custody_settings = SharedCustodySettingsView.as_view()
|
||||
|
||||
|
||||
class PartialBookingCheckView(ViewableAgendaMixin, UpdateView):
|
||||
template_name = 'chrono/manager_partial_booking_form.html'
|
||||
model = Booking
|
||||
pk_url_kwarg = 'booking_pk'
|
||||
form_class = PartialBookingCheckForm
|
||||
|
||||
def get_success_url(self):
|
||||
date = self.object.event.start_datetime
|
||||
return reverse(
|
||||
'chrono-manager-agenda-day-view',
|
||||
kwargs={'pk': self.agenda.pk, 'year': date.year, 'month': date.month, 'day': date.day},
|
||||
)
|
||||
|
||||
|
||||
partial_booking_check_view = PartialBookingCheckView.as_view()
|
||||
|
||||
|
||||
def menu_json(request):
|
||||
if not request.user.is_staff:
|
||||
homepage_view = HomepageView(request=request)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from chrono.agendas.models import Agenda, Booking, Event
|
||||
from chrono.utils.lingo import CheckType
|
||||
from chrono.utils.timezone import make_aware
|
||||
from tests.utils import login
|
||||
|
||||
|
@ -111,9 +113,9 @@ def test_manager_partial_bookings_day_view(app, admin_user, freezer):
|
|||
)
|
||||
|
||||
assert len(resp.pyquery('.partial-booking--registrant')) == 3
|
||||
assert resp.pyquery('.registrant--name')[0].text == 'Jane Doe'
|
||||
assert resp.pyquery('.registrant--name')[1].text == 'Jon Doe'
|
||||
assert resp.pyquery('.registrant--name')[2].text == 'Bruce Doe'
|
||||
assert resp.pyquery('.registrant--name a')[0].text == 'Jane Doe'
|
||||
assert resp.pyquery('.registrant--name a')[1].text == 'Jon Doe'
|
||||
assert resp.pyquery('.registrant--name a')[2].text == 'Bruce Doe'
|
||||
|
||||
assert resp.pyquery('.registrant--bar')[0].findall('time')[0].text == '11:00'
|
||||
assert resp.pyquery('.registrant--bar')[0].findall('time')[1].text == '13:30'
|
||||
|
@ -144,3 +146,77 @@ def test_manager_partial_bookings_day_view_24_hours(app, admin_user, freezer):
|
|||
assert [int(x.text.replace('\u202fh', '')) for x in resp.pyquery('.partial-booking--hour')] == list(
|
||||
range(0, 24)
|
||||
)
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
def test_manager_partial_bookings_check(check_types, app, admin_user):
|
||||
check_types.return_value = [
|
||||
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
|
||||
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
|
||||
CheckType(slug='baz-reason', label='Baz reason', kind='presence'),
|
||||
]
|
||||
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(11, 00),
|
||||
end_time=datetime.time(13, 30),
|
||||
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))
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 1
|
||||
assert resp.pyquery('.registrant--bar time')[0].text == '11:00'
|
||||
assert resp.pyquery('.registrant--bar time')[1].text == '13:30'
|
||||
assert resp.pyquery('.registrant--bar')[0].attrib['style'] == 'left: 30.77%; width: 19.23%;'
|
||||
|
||||
resp = resp.click('Jane Doe')
|
||||
assert resp.form['presence_check_type'].options == [
|
||||
('', True, '---------'),
|
||||
('bar-reason', False, 'Bar reason'),
|
||||
('baz-reason', False, 'Baz reason'),
|
||||
]
|
||||
assert resp.form['absence_check_type'].options == [
|
||||
('', True, '---------'),
|
||||
('foo-reason', False, 'Foo reason'),
|
||||
]
|
||||
|
||||
resp.form['user_check_start_time'] = '11:00'
|
||||
resp.form['user_check_end_time'] = '13:15'
|
||||
resp.form['user_was_present'] = 'True'
|
||||
resp.form['presence_check_type'] = 'bar-reason'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 2
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.check.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check time')[0].text == '11:00'
|
||||
assert resp.pyquery('.registrant--bar.check time')[1].text == '13:15'
|
||||
assert resp.pyquery('.registrant--bar.check')[0].attrib['style'] == 'left: 30.77%; width: 17.31%;'
|
||||
assert 'Bar reason' in resp.text
|
||||
|
||||
resp = resp.click('Jane Doe')
|
||||
assert resp.form['presence_check_type'].value == 'bar-reason'
|
||||
|
||||
resp.form['user_was_present'] = 'False'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 2
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.check.absent')) == 1
|
||||
assert 'Bar reason' not in resp.text
|
||||
|
||||
resp = resp.click('Jane Doe')
|
||||
resp.form['user_was_present'] = ''
|
||||
resp = resp.form.submit().follow()
|
||||
assert len(resp.pyquery('.registrant--bar')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 1
|
||||
assert 'Bar reason' not in resp.text
|
||||
|
|
Loading…
Reference in New Issue