manager: timesheet for one event (#66358)

This commit is contained in:
Lauréline Guérin 2022-06-17 15:19:20 +02:00
parent d700cbb8bc
commit 4f03dfb2c3
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
8 changed files with 305 additions and 75 deletions

View File

@ -567,7 +567,11 @@ class EventsTimesheetForm(forms.Form):
def __init__(self, *args, **kwargs):
self.agenda = kwargs.pop('agenda')
self.event = kwargs.pop('event', None)
super().__init__(*args, **kwargs)
if self.event is not None:
del self.fields['date_start']
del self.fields['date_end']
def get_slots(self):
extra_data = self.cleaned_data['extra_data'].split(',')
@ -576,23 +580,30 @@ class EventsTimesheetForm(forms.Form):
all_extra_data = extra_data[:]
if group_by:
all_extra_data += [group_by]
min_start = make_aware(
datetime.datetime.combine(self.cleaned_data['date_start'], datetime.time(0, 0))
)
max_start = make_aware(datetime.datetime.combine(self.cleaned_data['date_end'], datetime.time(0, 0)))
max_start = max_start + datetime.timedelta(days=1)
# fetch all events in this range
all_events = (
self.agenda.event_set.filter(
recurrence_days__isnull=True,
start_datetime__gte=min_start,
start_datetime__lt=max_start,
cancelled=False,
if self.event is not None:
all_events = [self.event]
min_start = self.event.start_datetime
max_start = min_start + datetime.timedelta(days=1)
else:
min_start = make_aware(
datetime.datetime.combine(self.cleaned_data['date_start'], datetime.time(0, 0))
)
max_start = make_aware(
datetime.datetime.combine(self.cleaned_data['date_end'], datetime.time(0, 0))
)
max_start = max_start + datetime.timedelta(days=1)
# fetch all events in this range
all_events = (
self.agenda.event_set.filter(
recurrence_days__isnull=True,
start_datetime__gte=min_start,
start_datetime__lt=max_start,
cancelled=False,
)
.select_related('primary_event')
.order_by('start_datetime', 'label')
)
.select_related('primary_event')
.order_by('start_datetime', 'label')
)
dates = set()
events = []
dates_per_event_id = defaultdict(list)

View File

@ -7,47 +7,50 @@
{% endblock %}
{% block page-title-extra-label %}
- {% firstof agenda.label object.label %}
- {% firstof agenda.label event.label %}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-agenda-view' pk=agenda.id %}">{{agenda.label}}</a>
<a href="{% url 'chrono-manager-event-view' pk=agenda.id event_pk=object.id %}">{{object}}</a>
<a href="{% url 'chrono-manager-event-view' pk=agenda.id event_pk=event.id %}">{{event}}</a>
{% endblock %}
{% block appbar %}
<h2>
{% if object.label %}
{{ object.label }} — {{object.start_datetime|date:"DATETIME_FORMAT"}}
{% if event.label %}
{{ event.label }} — {{event.start_datetime|date:"DATETIME_FORMAT"}}
{% else %}
{{ object.start_datetime|date:"DATETIME_FORMAT"}}
{{ event.start_datetime|date:"DATETIME_FORMAT"}}
{% endif %}
{% if object.cancellation_status %}<span class="tag">{{ event.cancellation_status }}</span>{% endif %}
{% if event.cancellation_status %}<span class="tag">{{ event.cancellation_status }}</span>{% endif %}
{% if event.main_list_full %}<span class="tag">{% trans "Full" %}</span>{% endif %}
{% if event.checked %}<span class="tag">{% trans "Checked" %}</span>{% endif %}
</h2>
{% block appbar_actions %}
<span class="actions">
{% if user_can_manage or object.agenda.booking_form_url %}
{% if user_can_manage or event.agenda.booking_form_url %}
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
{% if user_can_manage %}
{% if not object.primary_event %}
<li><a rel="popup" href="{% url 'chrono-manager-event-delete' pk=object.agenda.id event_pk=object.id %}">{% trans 'Delete' %}</a></li>
{% if not event.primary_event %}
<li><a rel="popup" href="{% url 'chrono-manager-event-delete' pk=event.agenda.id event_pk=event.id %}">{% trans 'Delete' %}</a></li>
{% endif %}
{% if not event.cancellation_status %}
<li><a rel="popup" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a></li>
{% endif %}
<li><a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=object.id %}">{% trans "Options" %}</a></li>
<li><a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=event.id %}">{% trans "Options" %}</a></li>
{% endif %}
{% if object.agenda.booking_form_url %}
<li><a href="{{ object.get_booking_form_url }}&ReturnURL={{ request.build_absolute_uri }}">{% trans "Booking form" %}</a></li>
{% if event.agenda.booking_form_url %}
<li><a href="{{ event.get_booking_form_url }}&ReturnURL={{ request.build_absolute_uri }}">{% trans "Booking form" %}</a></li>
{% endif %}
{% if not event.cancelled %}
<li><a href="{% url 'chrono-manager-event-timesheet' pk=agenda.pk event_pk=event.pk %}">{% trans "Timesheet" %}</a></li>
{% endif %}
</ul>
{% 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>
{% if event.is_day_past and not event.cancelled %}
<a href="{% url 'chrono-manager-event-check' pk=agenda.pk event_pk=event.pk %}">{% trans "Check" %}</a>
{% endif %}
</span>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "chrono/manager_event_detail.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-event-timesheet' pk=agenda.pk event_pk=event.pk %}">{% trans "Timesheet" %}</a>
{% endblock %}
{% block appbar_actions %}{% endblock %}
{% block content %}
{% include 'chrono/manager_events_timesheet_form_fragment.html' %}
{% endblock %}

View File

@ -9,42 +9,5 @@
{% block appbar_actions %}{% endblock %}
{% block content %}
<div class="section">
<h3>{% trans "Timesheet configuration" %}</h3>
<div>
<form id="timesheet">
{{ form.as_p }}
<script>
$(function() {
$('#id_date_display').on('change', function() {
if ($(this).val() == 'custom') {
$('#id_custom_nb_dates_per_page').parent().show();
} else {
$('#id_custom_nb_dates_per_page').parent().hide();
}
});
$('#id_date_display').trigger('change');
$('#id_group_by').on('change', function() {
if ($(this).val()) {
$('#id_with_page_break').parent().show();
} else {
$('#id_with_page_break').parent().hide();
}
});
$('#id_group_by').trigger('change');
});
</script>
<button class="submit-button">{% trans "See timesheet" %}</button>
{% if request.GET and form.is_valid %}
<button class="submit-button" name="pdf">{% trans "Get PDF file" %}</button>
{% endif %}
</form>
{% if request.GET and form.is_valid %}
<h4>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</h4>
{% include 'chrono/manager_events_timesheet_fragment.html' %}
{% endif %}
</div>
</div>
{% include 'chrono/manager_events_timesheet_form_fragment.html' %}
{% endblock %}

View File

@ -0,0 +1,44 @@
{% load i18n %}
<div class="section">
<h3>{% trans "Timesheet configuration" %}</h3>
<div>
<form id="timesheet">
{{ form.as_p }}
<script>
$(function() {
$('#id_date_display').on('change', function() {
if ($(this).val() == 'custom') {
$('#id_custom_nb_dates_per_page').parent().show();
} else {
$('#id_custom_nb_dates_per_page').parent().hide();
}
});
$('#id_date_display').trigger('change');
$('#id_group_by').on('change', function() {
if ($(this).val()) {
$('#id_with_page_break').parent().show();
} else {
$('#id_with_page_break').parent().hide();
}
});
$('#id_group_by').trigger('change');
});
</script>
<button class="submit-button">{% trans "See timesheet" %}</button>
{% if request.GET and form.is_valid %}
<button class="submit-button" name="pdf">{% trans "Get PDF file" %}</button>
{% endif %}
</form>
{% if request.GET and form.is_valid %}
{% if event %}
<h4>{% blocktrans %}Timesheet{% endblocktrans %}</h4>
{% else %}
<h4>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</h4>
{% endif %}
{% include 'chrono/manager_events_timesheet_fragment.html' %}
{% endif %}
</div>
</div>

View File

@ -229,6 +229,11 @@ urlpatterns = [
views.event_checked,
name='chrono-manager-event-checked',
),
url(
r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/timesheet$',
views.events_timesheet,
name='chrono-manager-event-timesheet',
),
url(
r'^agendas/(?P<pk>\d+)/event_cancellation_report/(?P<report_pk>\d+)/$',
views.event_cancellation_report,

View File

@ -2030,22 +2030,36 @@ agenda_reminder_preview = AgendaReminderPreviewView.as_view()
class EventsTimesheetView(ViewableAgendaMixin, DetailView):
model = Agenda
template_name = 'chrono/manager_events_timesheet.html'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
self.event = None
if 'event_pk' in kwargs:
self.event = get_object_or_404(
Event,
pk=kwargs.get('event_pk'),
agenda=self.agenda,
recurrence_days__isnull=True,
cancelled=False,
)
def get_object(self, **kwargs):
return self.agenda
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = EventsTimesheetForm(agenda=self.agenda, data=self.request.GET or None)
form = EventsTimesheetForm(agenda=self.agenda, event=self.event, data=self.request.GET or None)
if self.request.GET:
form.is_valid()
context['form'] = form
context['event'] = self.event
return context
def get_template_names(self):
if self.event is not None:
return ['chrono/manager_event_timesheet.html']
return ['chrono/manager_events_timesheet.html']
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
@ -2060,11 +2074,17 @@ class EventsTimesheetView(ViewableAgendaMixin, DetailView):
)
pdf = html.write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}_{}.pdf"'.format(
self.agenda.slug,
context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'),
context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'),
)
if self.event is not None:
response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}.pdf"'.format(
self.agenda.slug,
self.event.slug,
)
else:
response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}_{}.pdf"'.format(
self.agenda.slug,
context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'),
context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'),
)
return response

View File

@ -896,3 +896,174 @@ def test_events_timesheet_pdf(app, admin_user):
% agenda.pk
)
assert resp.context['form'].errors['orientation'] == ['This field is required.']
def test_event_timesheet_wrong_kind(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
)
app = login(app)
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
agenda.kind = 'virtual'
agenda.save()
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
def test_event_timesheet_wrong_event(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
agenda2 = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)),
places=10,
agenda=agenda,
cancelled=True,
)
app = login(app)
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
event.cancelled = False
event.recurrence_days = [1]
event.save()
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
event.recurrence_days = None
event.save()
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=200)
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda2.pk, event.pk), status=404)
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, 0), status=404)
def test_event_timesheet_form(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
)
app = login(app)
resp = app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk))
assert 'date_start' not in resp.context['form'].fields
assert 'date_end' not in resp.context['form'].fields
assert resp.context['form'].errors == {}
def test_event_timesheet_slots(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:1',
user_first_name='Subscription',
user_last_name='42',
date_start=datetime.date(2022, 2, 15),
date_end=datetime.date(2022, 2, 16),
)
login(app)
resp = app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk))
with CaptureQueriesContext(connection) as ctx:
resp = resp.form.submit()
assert len(ctx.captured_queries) == 7
slots = resp.context['form'].get_slots()
assert slots['dates'] == [
[
datetime.date(2022, 2, 15),
]
]
assert slots['events'] == [event]
assert slots['users'][0]['users'] == [
{
'user_id': 'user:1',
'user_first_name': 'Subscription',
'user_last_name': '42',
'extra_data': {},
'events': [
{
'event': event,
'dates': {datetime.date(2022, 2, 15): False},
},
],
},
]
assert slots['extra_data'] == []
def test_event_timesheet_subscription_limits(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
)
dates = [
('2022-01-31', '2022-02-01'),
('2022-02-01', '2022-02-02'),
('2022-02-01', '2022-02-15'),
('2022-02-01', '2022-02-16'),
('2022-02-15', '2022-02-28'),
('2022-02-15', '2022-03-01'),
('2022-02-16', '2022-03-01'),
('2022-02-01', '2022-03-01'),
('2022-02-28', '2022-03-01'),
('2022-03-01', '2022-03-02'),
]
for start, end in dates:
Subscription.objects.create(
agenda=agenda,
user_external_id='user:%s-%s' % (start, end),
user_first_name='Subscription',
user_last_name='%s - %s' % (start, end),
date_start=datetime.datetime.strptime(start, '%Y-%m-%d'),
date_end=datetime.datetime.strptime(end, '%Y-%m-%d'),
)
login(app)
resp = app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk))
resp = resp.form.submit()
slots = resp.context['form'].get_slots()
assert slots['dates'] == [
[
datetime.date(2022, 2, 15),
]
]
assert slots['events'] == [
event,
]
users = slots['users'][0]['users']
assert len(users) == 4
assert users[0]['user_id'] == 'user:2022-02-01-2022-02-16'
assert users[1]['user_id'] == 'user:2022-02-01-2022-03-01'
assert users[2]['user_id'] == 'user:2022-02-15-2022-02-28'
assert users[3]['user_id'] == 'user:2022-02-15-2022-03-01'
def test_event_timesheet_pdf(app, admin_user):
agenda = Agenda.objects.create(label='Foo', kind='events')
event = Event.objects.create(
label='Bar',
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)),
places=10,
agenda=agenda,
)
login(app)
resp = app.get(
'/manage/agendas/%s/events/%s/timesheet?pdf=&sort=lastname,firstname&date_display=all&orientation=portrait'
% (agenda.pk, event.pk)
)
assert resp.headers['Content-Type'] == 'application/pdf'
assert resp.headers['Content-Disposition'] == 'attachment; filename="timesheet_foo_bar.pdf"'
# form invalid
resp = app.get(
'/manage/agendas/%s/events/%s/timesheet?pdf=&sort=lastname,firstname&date_display=all'
% (agenda.pk, event.pk)
)
assert resp.context['form'].errors['orientation'] == ['This field is required.']