manager: event check page (#51535)

This commit is contained in:
Lauréline Guérin 2021-03-04 15:16:31 +01:00
parent eb9b9f98ca
commit ca514f6d11
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
8 changed files with 402 additions and 19 deletions

View File

@ -1176,6 +1176,9 @@ class Event(models.Model):
return False
return True
def is_day_past(self):
return self.start_datetime.date() <= now().date()
@staticmethod
def annotate_queryset(qs):
if django.VERSION < (2, 0):

View File

@ -31,6 +31,7 @@ from django.utils.timezone import now, make_aware, make_naive
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import (
AbsenceReason,
Agenda,
Booking,
Event,
@ -224,6 +225,18 @@ class EventForm(forms.ModelForm):
return self.instance
class BookingAbsenceReasonForm(forms.Form):
reason = forms.ChoiceField(required=False)
def __init__(self, *args, **kwargs):
agenda = kwargs.pop('agenda')
super().__init__(*args, **kwargs)
if agenda.absence_reasons_group:
self.fields['reason'].choices = [('', '---------')] + [
(r.label, r.label) for r in agenda.absence_reasons_group.absence_reasons.all()
]
class AgendaResourceForm(forms.Form):
resource = forms.ModelChoiceField(label=_('Resource'), queryset=Resource.objects.none())

View File

@ -9,22 +9,24 @@ function autorefresh() {
}
$(function() {
/* refresh every 30 seconds (idle_id) after any user activity
* on inactivity for more than 5 minutes (longidle_id), stop refreshing (clear idle_id)
*/
var idle_id = null;
var longidle_id = null;
$(window).on('mousemove keydown mousedown touchstart', function() {
/* if refresh timer exists, clear it */
if (idle_id) window.clearInterval(idle_id);
/* if stop refreshing timer exists, clear it */
if (longidle_id) window.clearTimeout(longidle_id);
/* launch timer to refresh every 30 seconds */
idle_id = setInterval(autorefresh, 30000);
/* launch timer to stop refreshing after 5 minutes idle */
longidle_id = setTimeout(function () {
if (idle_id) idle_id = window.clearInterval(idle_id);
longidle_id = undefined;
}, 300 * 1000);
});
if ($('#event_details')) {
/* refresh every 30 seconds (idle_id) after any user activity
* on inactivity for more than 5 minutes (longidle_id), stop refreshing (clear idle_id)
*/
var idle_id = null;
var longidle_id = null;
$(window).on('mousemove keydown mousedown touchstart', function() {
/* if refresh timer exists, clear it */
if (idle_id) window.clearInterval(idle_id);
/* if stop refreshing timer exists, clear it */
if (longidle_id) window.clearTimeout(longidle_id);
/* launch timer to refresh every 30 seconds */
idle_id = setInterval(autorefresh, 30000);
/* launch timer to stop refreshing after 5 minutes idle */
longidle_id = setTimeout(function () {
if (idle_id) idle_id = window.clearInterval(idle_id);
longidle_id = undefined;
}, 300 * 1000);
});
}
});

View File

@ -0,0 +1,67 @@
{% extends "chrono/manager_event_detail.html" %}
{% load staticfiles i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-event-check' pk=agenda.pk event_pk=object.pk %}">{% trans "Check" %}</a>
{% endblock %}
{% block appbar_actions %}{% endblock %}
{% block content %}
<div class="section">
<h3>
{% blocktrans with places=object.places booked_places=booked|length %}Bookings ({{ booked_places }}/{{ places }}){% endblocktrans %}
</h3>
<div>
<table class="main">
<tbody>
{% for booking in booked %}
<tr>
<td>{{ booking.user_name|default:booking.label|default:_('Unknown') }}</td>
<td>
{% trans "Status:" %} {{ booking.user_was_present|yesno:_('Present,Absent,Unknown') }}
{% if booking.user_was_present is False %}
<br />
{{ booking.user_absence_reason }}
{% endif %}
</td>
<td>
{% if booking.user_was_present is not True %}
<form method="post" action="{% url 'chrono-manager-booking-presence' pk=agenda.pk booking_pk=booking.pk %}">
{% csrf_token %}
<button class="submit-button">{% trans "Presence" %}</button>
</form>
{% endif %}
</td>
<td>
{% if booking.user_was_present is not False or booking.form.reason.field.choices.1 %}
<form method="post" action="{% url 'chrono-manager-booking-absence' pk=agenda.pk booking_pk=booking.pk %}">
{% csrf_token %}
{% if booking.form.reason.field.choices.1 %}{{ booking.form.reason }}{% endif %}
<button class="submit-button">{% if booking.user_was_present is False %}{% trans "Set absence reason" %}{% else %}{% trans "Absence" %}{% endif %}</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if object.waiting_list_places %}
<div class="section">
<h3>
{% blocktrans with places=object.waiting_list_places booked_places=waiting|length %}Waiting List ({{ booked_places }}/{{ places }}){% endblocktrans %}
</h3>
<div>
<ul class="objects-list single-links">
{% for booking in waiting %}
<li><a>{{ booking.user_name|default:booking.label|default:_('Unknown') }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -27,6 +27,7 @@
({{ object.cancellation_status }})
{% endif %}
</h2>
{% block appbar_actions %}
<span class="actions">
{% if user_can_manage %}
{% if not object.primary_event %}
@ -35,6 +36,9 @@
{% if not event.cancellation_status %}
<a rel="popup" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
{% endif %}
{% if object.is_day_past and not object.cancelled %}
<a href="{% url 'chrono-manager-event-check' pk=agenda.pk event_pk=object.pk %}">{% trans "Check" %}</a>
{% endif %}
<a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=object.id %}">{% trans "Options" %}</a>
{% endif %}
{% if object.agenda.booking_form_url %}
@ -42,6 +46,7 @@
{% endif %}
</span>
{% endblock %}
{% endblock %}
{% block content %}
<div id="event_details">

View File

@ -206,6 +206,11 @@ urlpatterns = [
views.event_cancel,
name='chrono-manager-event-cancel',
),
url(
r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/check$',
views.event_check,
name='chrono-manager-event-check',
),
url(
r'^agendas/(?P<pk>\d+)/event_cancellation_report/(?P<report_pk>\d+)/$',
views.event_cancellation_report,
@ -331,6 +336,16 @@ urlpatterns = [
views.booking_cancel,
name='chrono-manager-booking-cancel',
),
url(
r'^agendas/(?P<pk>\d+)/bookings/(?P<booking_pk>\d+)/presence$',
views.booking_presence,
name='chrono-manager-booking-presence',
),
url(
r'^agendas/(?P<pk>\d+)/bookings/(?P<booking_pk>\d+)/absence$',
views.booking_absence,
name='chrono-manager-booking-absence',
),
url(
r'^agendas/events.csv$',
views.agenda_import_events_sample_csv,

View File

@ -106,6 +106,7 @@ from .forms import (
UnavailabilityCalendarAddForm,
UnavailabilityCalendarEditForm,
AgendasExportForm,
BookingAbsenceReasonForm,
)
from .utils import import_site, export_site
@ -1898,6 +1899,41 @@ class EventDeleteView(ManagedAgendaMixin, DeleteView):
event_delete = EventDeleteView.as_view()
class EventCheckView(ViewableAgendaMixin, DetailView):
template_name = 'chrono/manager_event_check.html'
model = Event
pk_url_kwarg = 'event_pk'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(
Agenda.objects.prefetch_related('absence_reasons_group__absence_reasons'),
pk=kwargs.get('pk'),
kind='events',
)
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(agenda=self.agenda, start_datetime__date__lte=now().date(), cancelled=False)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
event = self.object
context['booked'] = event.booking_set.filter(
cancellation_datetime__isnull=True, in_waiting_list=False
).order_by('user_name')
context['waiting'] = event.booking_set.filter(
cancellation_datetime__isnull=True, in_waiting_list=True
).order_by('user_name')
for booking in context['booked']:
booking.form = BookingAbsenceReasonForm(
agenda=self.agenda, initial={'reason': booking.user_absence_reason}
)
return context
event_check = EventCheckView.as_view()
class AgendaAddResourceView(ManagedAgendaMixin, FormView):
template_name = 'chrono/manager_agenda_resource_form.html'
form_class = AgendaResourceForm
@ -2560,6 +2596,65 @@ class BookingCancelView(ViewableAgendaMixin, UpdateView):
booking_cancel = BookingCancelView.as_view()
class BookingPresenceView(ViewableAgendaMixin, View):
def post(self, request, *args, **kwargs):
booking = get_object_or_404(
Booking,
pk=kwargs['booking_pk'],
event__agenda=self.agenda,
event__start_datetime__date__lte=now().date(),
event__cancelled=False,
cancellation_datetime__isnull=True,
in_waiting_list=False,
)
booking.user_absence_reason = ''
booking.user_was_present = True
booking.save()
return HttpResponseRedirect(
reverse(
'chrono-manager-event-check',
kwargs={'pk': booking.event.agenda_id, 'event_pk': booking.event.pk},
)
)
booking_presence = BookingPresenceView.as_view()
class BookingAbsenceView(ViewableAgendaMixin, FormView):
form_class = BookingAbsenceReasonForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['agenda'] = self.agenda
return kwargs
def post(self, request, *args, **kwargs):
booking = get_object_or_404(
Booking,
pk=kwargs['booking_pk'],
event__agenda=self.agenda,
event__start_datetime__lt=now(),
event__cancelled=False,
cancellation_datetime__isnull=True,
in_waiting_list=False,
)
form = self.get_form()
if form.is_valid():
booking.user_absence_reason = form.cleaned_data['reason']
booking.user_was_present = False
booking.save()
return HttpResponseRedirect(
reverse(
'chrono-manager-event-check',
kwargs={'pk': booking.event.agenda_id, 'event_pk': booking.event.pk},
)
)
booking_absence = BookingAbsenceView.as_view()
class EventCancelView(ViewableAgendaMixin, UpdateView):
template_name = 'chrono/manager_confirm_event_cancellation.html'
model = Event

View File

@ -15,7 +15,7 @@ from django.test.utils import CaptureQueriesContext
from django.utils.timezone import localtime, make_aware, now
from webtest import Upload
from chrono.agendas.models import Agenda, Booking, Event
from chrono.agendas.models import Agenda, Booking, Event, AbsenceReasonGroup, AbsenceReason
from .test_all import login
@ -1047,3 +1047,186 @@ def test_booking_cancellation_events_agenda(app, admin_user):
secondary.refresh_from_db()
assert primary.cancellation_datetime
assert secondary.cancellation_datetime
def test_event_check(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
agenda2 = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
label='xyz',
start_datetime=now() + datetime.timedelta(days=1),
places=10,
waiting_list_places=5,
agenda=agenda,
)
booking1 = Booking.objects.create(event=event, user_name='User 42')
Booking.objects.create(event=event, user_name='User 1')
Booking.objects.create(event=event, user_name='User 17')
Booking.objects.create(event=event, user_name='User 35')
Booking.objects.create(event=event, user_name='User 5')
booking6 = Booking.objects.create(event=event, user_name='User Cancelled')
booking6.cancel()
booking7 = Booking.objects.create(event=event, user_name='User Waiting', in_waiting_list=True)
login(app)
# event not in past
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) not in resp
app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=404)
event.start_datetime = localtime(now()).replace(hour=22, minute=0) # it'os ok all the day
event.save()
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) in resp
# unknown agenda
app.get('/manage/agendas/%s/events/%s/check' % (0, event.pk), status=404)
# wrong agenda
app.get('/manage/agendas/%s/events/%s/check' % (agenda2.pk, event.pk), status=404)
resp = resp.click('Check')
assert 'Bookings (5/10)' in resp
assert 'Waiting List (1/5)' in resp
assert (
resp.text.index('Bookings (5/10)')
< resp.text.index('User 1')
< resp.text.index('User 17')
< resp.text.index('User 35')
< resp.text.index('User 42')
< resp.text.index('User 5')
< resp.text.index('Waiting List (1/5)')
< resp.text.index('User Waiting')
) # user ordering is not optimal ...
assert 'User Cancelled' not in resp
# cancelled booking
token = resp.context['csrf_token']
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking6.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking6.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# booking in waiting list
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking7.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking7.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# unknown agenda
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (0, booking1.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (0, booking1.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# wrong agenda
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda2.pk, booking1.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda2.pk, booking1.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# cancelled event
event.cancel()
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) not in resp
app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=404)
event.cancellation_datetime = None
def test_event_check_booking(app, admin_user):
group = AbsenceReasonGroup.objects.create(label='Foo bar')
agenda = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
label='xyz',
start_datetime=now() - datetime.timedelta(days=1),
places=10,
waiting_list_places=5,
agenda=agenda,
)
booking = Booking.objects.create(event=event, user_name='User 42')
login(app)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert 'Status: Unknown' in resp
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
assert 'Presence' in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
assert 'Absence' in resp
# set as present
token = resp.context['csrf_token']
resp = app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
).follow()
assert 'Status: Present' in resp
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) not in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
assert 'Absence' in resp
booking.refresh_from_db()
assert booking.user_was_present is True
assert booking.user_absence_reason == ''
# set as absent without reason
resp = app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
).follow()
assert 'Status: Absent' in resp
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
assert 'Presence' in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) not in resp
booking.refresh_from_db()
assert booking.user_was_present is False
assert booking.user_absence_reason == ''
agenda.absence_reasons_group = group
agenda.save()
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) not in resp
AbsenceReason.objects.create(label='Foo reason', group=group)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
assert 'Set absence reason' in resp
# set as absent with reason
resp = app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token, 'reason': 'Foo reason'},
).follow()
assert 'Foo reason' in resp
booking.refresh_from_db()
assert booking.user_was_present is False
assert booking.user_absence_reason == 'Foo reason'
# set as present
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
)
booking.refresh_from_db()
assert booking.user_was_present is True
assert booking.user_absence_reason == ''