manager: partial bookings, allow user check without booking (#80369)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Valentin Deniaud 2023-09-25 18:00:46 +02:00
parent 28c3641d50
commit ec86a9bbcc
6 changed files with 140 additions and 28 deletions

View File

@ -2964,6 +2964,12 @@ class Booking(models.Model):
# else take next_slot
return next_slot
def get_partial_bookings_check_url(self, agenda, event=None):
return reverse(
'chrono-manager-partial-booking-check',
kwargs={'pk': agenda.pk, 'booking_pk': self.pk},
)
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])
@ -3993,6 +3999,12 @@ class Subscription(models.Model):
except (VariableDoesNotExist, TemplateSyntaxError):
return
def get_partial_bookings_check_url(self, agenda, event):
return reverse(
'chrono-manager-partial-booking-subscription-check',
kwargs={'pk': agenda.pk, 'event_pk': event.pk, 'subscription_pk': self.pk},
)
class Person(models.Model):
user_external_id = models.CharField(max_length=250, unique=True)

View File

@ -609,8 +609,9 @@ class PartialBookingCheckForm(forms.ModelForm):
}
def __init__(self, *args, **kwargs):
agenda = kwargs.pop('agenda')
super().__init__(*args, **kwargs)
self.check_types = get_agenda_check_types(self.instance.event.agenda)
self.check_types = get_agenda_check_types(agenda)
absence_check_types = [(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'absence']
presence_check_types = [(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'presence']
@ -626,6 +627,12 @@ class PartialBookingCheckForm(forms.ModelForm):
else:
del self.fields['absence_check_type']
if not self.instance.start_time:
self.fields['user_check_start_time'].widget = widgets.TimeWidget(step=60)
self.fields['user_check_end_time'].widget = widgets.TimeWidget(step=60)
self.fields['user_was_present'].widget.choices = ((None, _('Not checked')), (True, _('Present')))
self.fields.pop('absence_check_type', None)
def clean(self):
if self.cleaned_data['user_was_present'] is not None:
kind = 'presence' if self.cleaned_data['user_was_present'] else 'absence'

View File

@ -50,10 +50,10 @@
<section class="partial-booking--registrant">
{% spaceless %}
<h3 class="registrant--name">
{% if user.bookings and allow_check %}
{% if allow_check %}
<a
rel="popup"
href="{% url 'chrono-manager-partial-booking-check' pk=agenda.pk booking_pk=user.bookings.0.pk %}"
href="{{ user.check_url }}"
>{{ user.name }}</a>
{% else %}
<span>{{ user.name }}</span>
@ -63,15 +63,17 @@
<div class="registrant--datas">
<div class="registrant--bar-container">
{% for booking in user.bookings %}
<p
class="registrant--bar clearfix booking"
title="{% trans "Booked period" %}"
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
>
<strong class="sr-only">{% trans "Booked period:" %}</strong>
<time class="start-time" datetime="{{ booking.start_time|time:"H:i" }}">{{ booking.start_time|time:"H:i" }}</time>
<time class="end-time" datetime="{{ booking.end_time|time:"H:i" }}">{{ booking.end_time|time:"H:i" }}</time>
</p>
{% if booking.start_time %}
<p
class="registrant--bar clearfix booking"
title="{% trans "Booked period" %}"
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
>
<strong class="sr-only">{% trans "Booked period:" %}</strong>
<time class="start-time" datetime="{{ booking.start_time|time:"H:i" }}">{{ booking.start_time|time:"H:i" }}</time>
<time class="end-time" datetime="{{ booking.end_time|time:"H:i" }}">{{ booking.end_time|time:"H:i" }}</time>
</p>
{% endif %}
{% endfor %}
</div>
{% if user.bookings %}

View File

@ -439,6 +439,11 @@ urlpatterns = [
views.partial_booking_check_view,
name='chrono-manager-partial-booking-check',
),
path(
'agendas/<int:pk>/subscriptions/<int:subscription_pk>/check/<int:event_pk>',
views.partial_booking_subscription_check_view,
name='chrono-manager-partial-booking-subscription-check',
),
path(
'agendas/<int:pk>/subscriptions/<int:subscription_pk>/extra-user-block',
views.subscription_extra_user_block,

View File

@ -1652,8 +1652,9 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
bookings = [x for x in context['results'] if x.kind == 'booking']
for booking in bookings:
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.start_time:
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'
@ -1672,7 +1673,12 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
users_info = {}
for result in context['results']:
user_info = users_info.setdefault(
result.user_external_id, {'name': result.get_user_block(), 'bookings': []}
result.user_external_id,
{
'name': result.get_user_block(),
'check_url': result.get_partial_bookings_check_url(self.agenda, event),
'bookings': [],
},
)
if result.kind != 'booking':
continue
@ -4486,20 +4492,12 @@ class SharedCustodySettingsView(UpdateView):
shared_custody_settings = SharedCustodySettingsView.as_view()
class PartialBookingCheckView(ViewableAgendaMixin, UpdateView):
class PartialBookingCheckMixin(ViewableAgendaMixin):
template_name = 'chrono/manager_partial_booking_form.html'
model = Booking
pk_url_kwarg = 'booking_pk'
form_class = PartialBookingCheckForm
def get_queryset(self, **kwargs):
qs = super().get_queryset()
return qs.filter(
Q(event__checked=False) | Q(event__agenda__disable_check_update=False),
Q(event__start_datetime__date__lte=localtime().date())
| Q(event__agenda__enable_check_for_future_events=True),
event__check_locked=False,
)
def get_object(self):
return self.get_booking(**self.kwargs)
def get_success_url(self):
date = self.object.event.start_datetime
@ -4508,10 +4506,30 @@ class PartialBookingCheckView(ViewableAgendaMixin, UpdateView):
kwargs={'pk': self.agenda.pk, 'year': date.year, 'month': date.month, 'day': date.day},
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['agenda'] = self.agenda
return kwargs
class PartialBookingCheckView(PartialBookingCheckMixin, BookingCheckMixin, UpdateView):
pass
partial_booking_check_view = PartialBookingCheckView.as_view()
class PartialBookingSubscriptionCheckView(PartialBookingCheckMixin, SubscriptionCheckMixin, UpdateView):
def get_object(self):
if self.request.method == 'POST':
return super().get_object()
else:
return Booking()
partial_booking_subscription_check_view = PartialBookingSubscriptionCheckView.as_view()
def menu_json(request):
if not request.user.is_staff:
homepage_view = HomepageView(request=request)

View File

@ -404,6 +404,75 @@ def test_manager_partial_bookings_check_future_events(app, admin_user, freezer):
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=200)
@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 = []
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,
recurrence_days=list(range(1, 8)),
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
)
event.create_all_recurrences()
Subscription.objects.create(
agenda=agenda,
user_external_id='xxx',
user_first_name='Jane',
user_last_name='Doe',
date_start=event.start_datetime,
date_end=event.start_datetime + datetime.timedelta(days=2),
)
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')) == 0
resp = resp.click('Jane Doe')
assert 'Fill with booking start time' not in resp.text
assert 'absence_check_type' not in resp.form.fields
assert resp.form['user_was_present'].options == [
('', True, None),
('True', False, None),
] # no 'False' option
assert not Booking.objects.exists()
resp.form['user_check_start_time'] = '10:00'
resp.form['user_check_end_time'] = '16:00'
resp.form['user_was_present'] = 'True'
resp = resp.form.submit().follow()
assert len(resp.pyquery('.registrant--bar')) == 2
assert len(resp.pyquery('.registrant--bar.check.present')) == 1
assert len(resp.pyquery('.registrant--bar.computed.present')) == 1
assert resp.pyquery('.registrant--bar.check time')[0].text == '10:00'
assert resp.pyquery('.registrant--bar.check time')[1].text == '16:00'
booking = Booking.objects.get()
assert booking.user_external_id == 'xxx'
assert booking.user_first_name == 'Jane'
assert booking.user_last_name == 'Doe'
resp = resp.click('Jane Doe')
assert 'Fill with booking start time' not in resp.text
assert 'absence_check_type' not in resp.form.fields
assert resp.form['user_was_present'].options == [
('', False, None),
('True', True, None),
] # no 'False' option
resp.form['user_was_present'] = ''
resp = resp.form.submit().follow()
assert len(resp.pyquery('.registrant--bar')) == 0
@mock.patch('chrono.manager.forms.get_agenda_check_types')
def test_manager_partial_bookings_check_filters(check_types, app, admin_user):
check_types.return_value = [
@ -481,9 +550,8 @@ def test_manager_partial_bookings_check_filters(check_types, app, admin_user):
'User Present Vegan',
]
# one registrant has not booked, no bar is shown and no booking check link
# one registrant has not booked, no bar is shown
assert len(resp.pyquery('.registrant--bar.booking')) == 3
assert len(resp.pyquery('.registrant--name a')) == 3
resp = app.get(url, params={'booking-status': 'booked'})
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == [