manager: timesheet for one event (#66358)
This commit is contained in:
parent
d700cbb8bc
commit
4f03dfb2c3
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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.']
|
||||
|
|
Loading…
Reference in New Issue