0002-manager-timesheet-generation-61070.patch
chrono/manager/forms.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 | |
18 |
import copy |
|
18 | 19 |
import csv |
19 | 20 |
import datetime |
21 |
from collections import defaultdict |
|
22 |
from operator import itemgetter |
|
20 | 23 | |
21 | 24 |
import django_filters |
25 |
from dateutil.relativedelta import relativedelta |
|
22 | 26 |
from django import forms |
23 | 27 |
from django.conf import settings |
24 | 28 |
from django.contrib.auth.models import Group |
... | ... | |
27 | 31 |
from django.forms import ValidationError |
28 | 32 |
from django.utils.encoding import force_text |
29 | 33 |
from django.utils.six import StringIO |
30 |
from django.utils.timezone import make_aware, now |
|
34 |
from django.utils.timezone import localtime, make_aware, now
|
|
31 | 35 |
from django.utils.translation import ugettext_lazy as _ |
32 | 36 | |
33 | 37 |
from chrono.agendas.models import ( |
... | ... | |
357 | 361 |
] |
358 | 362 | |
359 | 363 | |
364 |
class EventsTimesheetForm(forms.Form): |
|
365 |
date_start = forms.DateField( |
|
366 |
label=_('Start date'), |
|
367 |
widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
|
368 |
) |
|
369 |
date_end = forms.DateField( |
|
370 |
label=_('End date'), |
|
371 |
widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
|
372 |
) |
|
373 | ||
374 |
def __init__(self, *args, **kwargs): |
|
375 |
self.agenda = kwargs.pop('agenda') |
|
376 |
super().__init__(*args, **kwargs) |
|
377 | ||
378 |
def get_slots(self): |
|
379 |
min_start = make_aware( |
|
380 |
datetime.datetime.combine(self.cleaned_data['date_start'], datetime.time(0, 0)) |
|
381 |
) |
|
382 |
max_start = make_aware(datetime.datetime.combine(self.cleaned_data['date_end'], datetime.time(0, 0))) |
|
383 |
max_start = max_start + datetime.timedelta(days=1) |
|
384 | ||
385 |
# fetch all events in this range |
|
386 |
events_qs = ( |
|
387 |
self.agenda.event_set.filter( |
|
388 |
recurrence_days__isnull=True, |
|
389 |
start_datetime__gte=min_start, |
|
390 |
start_datetime__lt=max_start, |
|
391 |
) |
|
392 |
.select_related('primary_event') |
|
393 |
.order_by('start_datetime', 'label') |
|
394 |
) |
|
395 |
all_events = self.agenda.add_event_recurrences(events_qs, min_start, max_start) |
|
396 |
dates = set() |
|
397 |
events = [] |
|
398 |
dates_per_event_id = defaultdict(list) |
|
399 |
for event in all_events: |
|
400 |
date = localtime(event.start_datetime).date() |
|
401 |
dates.add(date) |
|
402 |
real_event = event.primary_event or event |
|
403 |
if real_event not in events: |
|
404 |
events.append(real_event) |
|
405 |
dates_per_event_id[real_event.pk].append(date) |
|
406 |
dates = sorted(dates) |
|
407 | ||
408 |
event_slots = [] |
|
409 |
for event in events: |
|
410 |
event_slots.append( |
|
411 |
{'event': event, 'dates': {date: False for date in dates_per_event_id[event.pk]}} |
|
412 |
) |
|
413 | ||
414 |
users = {} |
|
415 |
subscriptions = self.agenda.subscriptions.filter(date_start__lt=max_start, date_end__gt=min_start) |
|
416 |
for subscription in subscriptions: |
|
417 |
if subscription.user_external_id in users: |
|
418 |
continue |
|
419 |
users[subscription.user_external_id] = { |
|
420 |
'user_id': subscription.user_external_id, |
|
421 |
'user_first_name': subscription.user_first_name, |
|
422 |
'user_last_name': subscription.user_last_name, |
|
423 |
'events': copy.deepcopy(event_slots), |
|
424 |
} |
|
425 | ||
426 |
booking_qs_kwargs = {} |
|
427 |
if not self.agenda.subscriptions.exists(): |
|
428 |
booking_qs_kwargs = {'cancellation_datetime__isnull': True} |
|
429 |
booked_qs = ( |
|
430 |
Booking.objects.filter( |
|
431 |
event__in=all_events, |
|
432 |
in_waiting_list=False, |
|
433 |
primary_booking__isnull=True, |
|
434 |
**booking_qs_kwargs, |
|
435 |
) |
|
436 |
.exclude(user_external_id='') |
|
437 |
.select_related('event') |
|
438 |
.order_by('event__start_datetime') |
|
439 |
) |
|
440 |
for booking in booked_qs: |
|
441 |
user_id = booking.user_external_id |
|
442 |
if user_id not in users: |
|
443 |
users[user_id] = { |
|
444 |
'user_id': user_id, |
|
445 |
'user_first_name': booking.user_first_name, |
|
446 |
'user_last_name': booking.user_last_name, |
|
447 |
'events': copy.deepcopy(event_slots), |
|
448 |
} |
|
449 |
if booking.cancellation_datetime is not None: |
|
450 |
continue |
|
451 |
# mark the slot as booked |
|
452 |
date = localtime(booking.event.start_datetime).date() |
|
453 |
for event in users[user_id]['events']: |
|
454 |
if event['event'].pk != (booking.event.primary_event_id or booking.event_id): |
|
455 |
continue |
|
456 |
if date in event['dates']: |
|
457 |
event['dates'][date] = True |
|
458 |
break |
|
459 | ||
460 |
users = sorted(users.values(), key=itemgetter('user_last_name', 'user_first_name', 'user_id')) |
|
461 | ||
462 |
return { |
|
463 |
'dates': dates, |
|
464 |
'events': events, |
|
465 |
'users': users, |
|
466 |
} |
|
467 | ||
468 |
def clean(self): |
|
469 |
cleaned_data = super().clean() |
|
470 | ||
471 |
if 'date_start' in cleaned_data and 'date_end' in cleaned_data: |
|
472 |
if cleaned_data['date_end'] < cleaned_data['date_start']: |
|
473 |
self.add_error('date_end', _('End date must be greater than start date.')) |
|
474 |
elif (cleaned_data['date_start'] + relativedelta(months=3)) < cleaned_data['date_end']: |
|
475 |
self.add_error('date_end', _('Please select an interval of no more than 3 months.')) |
|
476 | ||
477 |
return cleaned_data |
|
478 | ||
479 | ||
360 | 480 |
class AgendaResourceForm(forms.Form): |
361 | 481 |
resource = forms.ModelChoiceField(label=_('Resource'), queryset=Resource.objects.none()) |
362 | 482 |
chrono/manager/static/css/style.scss | ||
---|---|---|
481 | 481 |
div.ui-dialog div.widget .datetime input { |
482 | 482 |
width: auto; |
483 | 483 |
} |
484 | ||
485 |
table.timesheet { |
|
486 |
width: auto; |
|
487 |
th { |
|
488 |
padding: 0.5em 0.5ex; |
|
489 |
background: none; |
|
490 |
border: 1px solid #f3f3f3; |
|
491 |
&.date { |
|
492 |
width: 20px; |
|
493 |
text-align: center; |
|
494 |
} |
|
495 |
} |
|
496 |
td { |
|
497 |
padding: 0.5em 0.5ex; |
|
498 |
border: 1px solid #f3f3f3; |
|
499 |
&.date { |
|
500 |
text-align: center; |
|
501 |
padding: 0px; |
|
502 |
max-width: 2em; |
|
503 |
} |
|
504 |
} |
|
505 |
} |
chrono/manager/templates/chrono/manager_agenda_open_events.html | ||
---|---|---|
9 | 9 |
{% endblock %} |
10 | 10 | |
11 | 11 |
{% block actions %} |
12 |
<a class="extra-actions-menu-opener"></a> |
|
13 |
<ul class="extra-actions-menu"> |
|
14 |
<li><a href="{% url 'chrono-manager-events-timesheet' pk=agenda.pk %}">{% trans 'Timesheet' %}</a></li> |
|
15 |
</ul> |
|
12 | 16 |
{{ block.super }} |
13 | 17 |
<a href="{% url 'chrono-manager-agenda-month-redirect-view' pk=agenda.pk %}">{% trans 'Month view' %}</a> |
14 | 18 |
{% endblock %} |
chrono/manager/templates/chrono/manager_events_agenda_day_view.html | ||
---|---|---|
2 | 2 |
{% load i18n %} |
3 | 3 | |
4 | 4 |
{% block actions %} |
5 |
<a class="extra-actions-menu-opener"></a> |
|
6 |
<ul class="extra-actions-menu"> |
|
7 |
<li><a href="{% url 'chrono-manager-events-timesheet' pk=agenda.pk %}">{% trans 'Timesheet' %}</a></li> |
|
8 |
</ul> |
|
5 | 9 |
{{ block.super }} |
6 | 10 |
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a> |
7 | 11 |
<a href="{% url 'chrono-manager-agenda-month-view' pk=agenda.pk year=view.date|date:"Y" month=view.date|date:"n" %}">{% trans 'Month view' %}</a> |
chrono/manager/templates/chrono/manager_events_agenda_month_view.html | ||
---|---|---|
3 | 3 | |
4 | 4 |
{% block actions %} |
5 | 5 |
<a class="extra-actions-menu-opener"></a> |
6 |
{{ block.super }} |
|
7 |
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a> |
|
8 |
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y" month=view.date|date:"m" day=view.date|date:"d" %}">{% trans 'Day view' %}</a> |
|
9 | 6 |
<ul class="extra-actions-menu"> |
10 | 7 |
<li><a href="{% url 'chrono-manager-event-cancellation-report-list' pk=agenda.pk %}">{% trans 'Cancellation error reports' %}</a></li> |
8 |
<li><a href="{% url 'chrono-manager-events-timesheet' pk=agenda.pk %}">{% trans 'Timesheet' %}</a></li> |
|
11 | 9 |
</ul> |
10 |
{{ block.super }} |
|
11 |
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a> |
|
12 |
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y" month=view.date|date:"m" day=view.date|date:"d" %}">{% trans 'Day view' %}</a> |
|
12 | 13 |
{% endblock %} |
13 | 14 | |
14 | 15 |
{% block content %} |
chrono/manager/templates/chrono/manager_events_timesheet.html | ||
---|---|---|
1 |
{% extends "chrono/manager_agenda_view.html" %} |
|
2 |
{% load i18n chrono %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="{% url 'chrono-manager-events-timesheet' pk=agenda.pk %}">{% trans "Timesheet" %}</a> |
|
7 |
{% endblock %} |
|
8 | ||
9 |
{% block appbar_actions %}{% endblock %} |
|
10 | ||
11 |
{% block content %} |
|
12 |
<div class="section"> |
|
13 |
<h3>{% trans "Timesheet configuration" %}</h3> |
|
14 |
<div> |
|
15 |
<form> |
|
16 |
{{ form.as_p }} |
|
17 |
<button class="submit-button">{% trans "See timesheet" %}</button> |
|
18 |
</form> |
|
19 | ||
20 |
{% if request.GET and form.is_valid %} |
|
21 |
<h4>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</h4> |
|
22 | ||
23 |
{% with slots=form.get_slots %} |
|
24 |
{% with events_num=slots.events|length %} |
|
25 |
<table class="main timesheet"> |
|
26 |
<thead> |
|
27 |
<tr> |
|
28 |
<th>{% trans "First name" %}</th> |
|
29 |
<th>{% trans "Last name" %}</th> |
|
30 |
{% if events_num > 1 %}<th>{% trans "Activity" %}</th>{% endif %} |
|
31 |
{% for date in slots.dates %}<th class="date">{{ date|date:"D d/m" }}</th>{% endfor %} |
|
32 |
</tr> |
|
33 |
</thead> |
|
34 |
<tbody> |
|
35 |
{% for user in slots.users %}{% for event in user.events %} |
|
36 |
<tr> |
|
37 |
{% if forloop.first %} |
|
38 |
<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }}</td> |
|
39 |
<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_last_name }}</td> |
|
40 |
{% endif %} |
|
41 |
{% if events_num > 1 %}<td>{{ event.event }}</td>{% endif %} |
|
42 |
{% for date in slots.dates %} |
|
43 |
{% with booked=event.dates|get:date %}<td class="date">{% if booked is True %}☐{% elif booked is None %}-{% endif %}</td>{% endwith %} |
|
44 |
{% endfor %} |
|
45 |
</tr> |
|
46 |
{% endfor %}{% endfor %} |
|
47 |
</tbody> |
|
48 |
</table> |
|
49 |
{% endwith %} |
|
50 |
{% endwith %} |
|
51 |
{% endif %} |
|
52 |
</div> |
|
53 |
</div> |
|
54 |
{% endblock %} |
chrono/manager/templatetags/chrono.py | ||
---|---|---|
1 |
# chrono - agendas system |
|
2 |
# Copyright (C) 2022 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 | ||
18 |
from django import template |
|
19 | ||
20 |
register = template.Library() |
|
21 | ||
22 | ||
23 |
@register.filter(name='get') |
|
24 |
def get(obj, key): |
|
25 |
try: |
|
26 |
return obj.get(key) |
|
27 |
except AttributeError: |
|
28 |
try: |
|
29 |
return obj[key] |
|
30 |
except (IndexError, KeyError, TypeError): |
|
31 |
return None |
chrono/manager/urls.py | ||
---|---|---|
191 | 191 |
views.agenda_reminder_preview, |
192 | 192 |
name='chrono-manager-agenda-reminder-preview', |
193 | 193 |
), |
194 |
url( |
|
195 |
r'^agendas/(?P<pk>\d+)/events/timesheet$', |
|
196 |
views.events_timesheet, |
|
197 |
name='chrono-manager-events-timesheet', |
|
198 |
), |
|
194 | 199 |
url( |
195 | 200 |
r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/$', |
196 | 201 |
views.event_view, |
chrono/manager/views.py | ||
---|---|---|
97 | 97 |
DeskForm, |
98 | 98 |
EventCancelForm, |
99 | 99 |
EventForm, |
100 |
EventsTimesheetForm, |
|
100 | 101 |
ExceptionsImportForm, |
101 | 102 |
ImportEventsForm, |
102 | 103 |
MeetingTypeForm, |
... | ... | |
1953 | 1954 |
agenda_reminder_preview = AgendaReminderPreviewView.as_view() |
1954 | 1955 | |
1955 | 1956 | |
1957 |
class EventsTimesheetView(ViewableAgendaMixin, DetailView): |
|
1958 |
model = Agenda |
|
1959 |
template_name = 'chrono/manager_events_timesheet.html' |
|
1960 | ||
1961 |
def set_agenda(self, **kwargs): |
|
1962 |
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events') |
|
1963 | ||
1964 |
def get_object(self, **kwargs): |
|
1965 |
return self.agenda |
|
1966 | ||
1967 |
def get_context_data(self, **kwargs): |
|
1968 |
context = super().get_context_data(**kwargs) |
|
1969 |
form = EventsTimesheetForm(agenda=self.agenda, data=self.request.GET or None) |
|
1970 |
if self.request.GET: |
|
1971 |
form.is_valid() |
|
1972 |
context['form'] = form |
|
1973 |
return context |
|
1974 | ||
1975 | ||
1976 |
events_timesheet = EventsTimesheetView.as_view() |
|
1977 | ||
1978 | ||
1956 | 1979 |
class EventDetailView(ViewableAgendaMixin, DetailView): |
1957 | 1980 |
model = Event |
1958 | 1981 |
pk_url_kwarg = 'event_pk' |
tests/manager/test_all.py | ||
---|---|---|
1448 | 1448 |
) |
1449 | 1449 |
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2020, 11)) |
1450 | 1450 |
assert len(resp.pyquery.find('.event-info')) == 4 |
1451 |
assert 'abc' in resp.pyquery.find('li')[4].text_content()
|
|
1452 |
assert 'Exception: 11/10/2020' in resp.pyquery.find('li')[5].text_content()
|
|
1453 |
assert 'xyz' in resp.pyquery.find('li')[6].text_content()
|
|
1451 |
assert 'abc' in resp.pyquery.find('li')[5].text_content()
|
|
1452 |
assert 'Exception: 11/10/2020' in resp.pyquery.find('li')[6].text_content()
|
|
1453 |
assert 'xyz' in resp.pyquery.find('li')[7].text_content()
|
|
1454 | 1454 | |
1455 | 1455 |
# 12/2020 has 5 Wednesday |
1456 | 1456 |
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2020, 12)) |
tests/manager/test_event.py | ||
---|---|---|
2007 | 2007 |
user_bookings = resp.pyquery.find('td.booking-username.waiting') |
2008 | 2008 |
assert len(user_bookings) == 1 |
2009 | 2009 |
assert user_bookings[0].text == 'Jane Doe (2 places)' |
2010 | ||
2011 | ||
2012 |
def test_events_timesheet_wrong_kind(app, admin_user): |
|
2013 |
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') |
|
2014 | ||
2015 |
app = login(app) |
|
2016 |
app.get('/manage/agendas/%s/events/timesheet' % agenda.id, status=404) |
|
2017 |
agenda.kind = 'virtual' |
|
2018 |
agenda.save() |
|
2019 |
app.get('/manage/agendas/%s/events/timesheet' % agenda.id, status=404) |
|
2020 | ||
2021 | ||
2022 |
def test_events_timesheet_form(app, admin_user): |
|
2023 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
2024 | ||
2025 |
login(app) |
|
2026 |
resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk) |
|
2027 |
resp.form['date_start'] = '2022-01-01' |
|
2028 |
resp.form['date_end'] = '2021-12-31' |
|
2029 |
resp = resp.form.submit() |
|
2030 |
assert resp.context['form'].errors['date_end'] == ['End date must be greater than start date.'] |
|
2031 | ||
2032 |
resp.form['date_end'] = '2022-04-02' |
|
2033 |
resp = resp.form.submit() |
|
2034 |
assert resp.context['form'].errors['date_end'] == ['Please select an interval of no more than 3 months.'] |
|
2035 | ||
2036 |
resp.form['date_end'] = '2022-04-01' |
|
2037 |
resp = resp.form.submit() |
|
2038 |
assert resp.context['form'].errors == {} |
|
2039 | ||
2040 | ||
2041 |
@pytest.mark.freeze_time('2022-02-15') |
|
2042 |
def test_events_timesheet_slots(app, admin_user): |
|
2043 |
start, end = ( |
|
2044 |
now() - datetime.timedelta(days=15), |
|
2045 |
now() + datetime.timedelta(days=14), |
|
2046 |
) # 2022-02-31, 2022-03-01 |
|
2047 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
2048 |
Event.objects.create(label='event 1', start_datetime=start, places=10, agenda=agenda) |
|
2049 |
event2 = Event.objects.create( |
|
2050 |
label='event 2', start_datetime=start + datetime.timedelta(days=1), places=10, agenda=agenda |
|
2051 |
) |
|
2052 |
event3 = Event.objects.create(label='event 3', start_datetime=now(), places=10, agenda=agenda) |
|
2053 |
Event.objects.create( |
|
2054 |
label='event cancelled', |
|
2055 |
start_datetime=now() + datetime.timedelta(days=4), |
|
2056 |
places=10, |
|
2057 |
agenda=agenda, |
|
2058 |
cancelled=True, |
|
2059 |
) |
|
2060 |
event4 = Event.objects.create( |
|
2061 |
label='event 4', start_datetime=end - datetime.timedelta(days=1), places=10, agenda=agenda |
|
2062 |
) |
|
2063 |
Event.objects.create(label='event 5', start_datetime=end, places=10, agenda=agenda) |
|
2064 |
Subscription.objects.create( |
|
2065 |
agenda=agenda, |
|
2066 |
user_external_id='user:1', |
|
2067 |
user_first_name='Subscription', |
|
2068 |
user_last_name='42', |
|
2069 |
date_start=start, |
|
2070 |
date_end=end + datetime.timedelta(days=1), |
|
2071 |
) |
|
2072 |
recurring_event1 = Event.objects.create( |
|
2073 |
label='recurring 1', |
|
2074 |
start_datetime=start, |
|
2075 |
places=10, |
|
2076 |
agenda=agenda, |
|
2077 |
recurrence_days=[0, 1], |
|
2078 |
recurrence_end_date=end, |
|
2079 |
) |
|
2080 |
recurring_event1.create_all_recurrences() |
|
2081 |
recurring_event2 = Event.objects.create( |
|
2082 |
label='recurring 2', |
|
2083 |
start_datetime=start, |
|
2084 |
places=10, |
|
2085 |
agenda=agenda, |
|
2086 |
recurrence_days=[1, 2], |
|
2087 |
recurrence_end_date=end, |
|
2088 |
) |
|
2089 |
recurring_event2.create_all_recurrences() |
|
2090 | ||
2091 |
login(app) |
|
2092 |
resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk) |
|
2093 |
resp.form['date_start'] = '2022-02-01' |
|
2094 |
resp.form['date_end'] = '2022-02-28' |
|
2095 |
with CaptureQueriesContext(connection) as ctx: |
|
2096 |
resp = resp.form.submit() |
|
2097 |
assert len(ctx.captured_queries) == 9 |
|
2098 | ||
2099 |
slots = resp.context['form'].get_slots() |
|
2100 |
assert slots['dates'] == [ |
|
2101 |
datetime.date(2022, 2, 1), |
|
2102 |
datetime.date(2022, 2, 2), |
|
2103 |
datetime.date(2022, 2, 7), |
|
2104 |
datetime.date(2022, 2, 8), |
|
2105 |
datetime.date(2022, 2, 9), |
|
2106 |
datetime.date(2022, 2, 14), |
|
2107 |
datetime.date(2022, 2, 15), |
|
2108 |
datetime.date(2022, 2, 16), |
|
2109 |
datetime.date(2022, 2, 21), |
|
2110 |
datetime.date(2022, 2, 22), |
|
2111 |
datetime.date(2022, 2, 23), |
|
2112 |
datetime.date(2022, 2, 28), |
|
2113 |
] |
|
2114 |
assert slots['events'] == [ |
|
2115 |
event2, |
|
2116 |
recurring_event1, |
|
2117 |
recurring_event2, |
|
2118 |
event3, |
|
2119 |
event4, |
|
2120 |
] |
|
2121 |
assert slots['users'] == [ |
|
2122 |
{ |
|
2123 |
'user_id': 'user:1', |
|
2124 |
'user_first_name': 'Subscription', |
|
2125 |
'user_last_name': '42', |
|
2126 |
'events': [ |
|
2127 |
{ |
|
2128 |
'event': event2, |
|
2129 |
'dates': {date: False for date in slots['dates'] if date == datetime.date(2022, 2, 1)}, |
|
2130 |
}, |
|
2131 |
{ |
|
2132 |
'event': recurring_event1, |
|
2133 |
'dates': {date: False for date in slots['dates'] if date.weekday() in [0, 1]}, |
|
2134 |
}, |
|
2135 |
{ |
|
2136 |
'event': recurring_event2, |
|
2137 |
'dates': {date: False for date in slots['dates'] if date.weekday() in [1, 2]}, |
|
2138 |
}, |
|
2139 |
{ |
|
2140 |
'event': event3, |
|
2141 |
'dates': {date: False for date in slots['dates'] if date == datetime.date(2022, 2, 15)}, |
|
2142 |
}, |
|
2143 |
{ |
|
2144 |
'event': event4, |
|
2145 |
'dates': {date: False for date in slots['dates'] if date == datetime.date(2022, 2, 28)}, |
|
2146 |
}, |
|
2147 |
], |
|
2148 |
}, |
|
2149 |
] |
|
2150 | ||
2151 | ||
2152 |
@pytest.mark.freeze_time('2022-02-15') |
|
2153 |
def test_events_timesheet_subscription_limits(app, admin_user): |
|
2154 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
2155 |
event1 = Event.objects.create( |
|
2156 |
start_datetime=make_aware(datetime.datetime(2022, 2, 1, 17, 0)), places=10, agenda=agenda |
|
2157 |
) |
|
2158 |
event2 = Event.objects.create( |
|
2159 |
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda |
|
2160 |
) |
|
2161 |
event3 = Event.objects.create( |
|
2162 |
start_datetime=make_aware(datetime.datetime(2022, 2, 28, 17, 0)), places=10, agenda=agenda |
|
2163 |
) |
|
2164 | ||
2165 |
dates = [ |
|
2166 |
('2022-01-31', '2022-02-01'), |
|
2167 |
('2022-02-01', '2022-02-02'), |
|
2168 |
('2022-02-01', '2022-02-15'), |
|
2169 |
('2022-02-01', '2022-02-16'), |
|
2170 |
('2022-02-15', '2022-02-28'), |
|
2171 |
('2022-02-15', '2022-03-01'), |
|
2172 |
('2022-02-16', '2022-03-01'), |
|
2173 |
('2022-02-01', '2022-03-01'), |
|
2174 |
('2022-02-28', '2022-03-01'), |
|
2175 |
('2022-03-01', '2022-03-02'), |
|
2176 |
] |
|
2177 | ||
2178 |
for start, end in dates: |
|
2179 |
Subscription.objects.create( |
|
2180 |
agenda=agenda, |
|
2181 |
user_external_id='user:%s-%s' % (start, end), |
|
2182 |
user_first_name='Subscription', |
|
2183 |
user_last_name='%s - %s' % (start, end), |
|
2184 |
date_start=datetime.datetime.strptime(start, '%Y-%m-%d'), |
|
2185 |
date_end=datetime.datetime.strptime(end, '%Y-%m-%d'), |
|
2186 |
) |
|
2187 | ||
2188 |
login(app) |
|
2189 |
resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk) |
|
2190 |
resp.form['date_start'] = '2022-02-01' |
|
2191 |
resp.form['date_end'] = '2022-02-28' |
|
2192 |
resp = resp.form.submit() |
|
2193 | ||
2194 |
slots = resp.context['form'].get_slots() |
|
2195 |
assert slots['dates'] == [ |
|
2196 |
datetime.date(2022, 2, 1), |
|
2197 |
datetime.date(2022, 2, 15), |
|
2198 |
datetime.date(2022, 2, 28), |
|
2199 |
] |
|
2200 | ||
2201 |
assert slots['events'] == [ |
|
2202 |
event1, |
|
2203 |
event2, |
|
2204 |
event3, |
|
2205 |
] |
|
2206 |
assert len(slots['users']) == 8 |
|
2207 |
assert slots['users'][0]['user_id'] == 'user:2022-02-01-2022-02-02' |
|
2208 |
assert slots['users'][1]['user_id'] == 'user:2022-02-01-2022-02-15' |
|
2209 |
assert slots['users'][2]['user_id'] == 'user:2022-02-01-2022-02-16' |
|
2210 |
assert slots['users'][3]['user_id'] == 'user:2022-02-01-2022-03-01' |
|
2211 |
assert slots['users'][4]['user_id'] == 'user:2022-02-15-2022-02-28' |
|
2212 |
assert slots['users'][5]['user_id'] == 'user:2022-02-15-2022-03-01' |
|
2213 |
assert slots['users'][6]['user_id'] == 'user:2022-02-16-2022-03-01' |
|
2214 |
assert slots['users'][7]['user_id'] == 'user:2022-02-28-2022-03-01' |
|
2215 | ||
2216 | ||
2217 |
def test_events_timesheet_users(app, admin_user): |
|
2218 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
2219 |
event = Event.objects.create( |
|
2220 |
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda |
|
2221 |
) |
|
2222 | ||
2223 |
booking1 = Booking.objects.create( |
|
2224 |
event=event, user_external_id='user:1', user_first_name='User', user_last_name='42' |
|
2225 |
) |
|
2226 |
Booking.objects.create( |
|
2227 |
event=event, user_external_id='user:2', user_first_name='User', user_last_name='01' |
|
2228 |
) |
|
2229 |
Booking.objects.create( |
|
2230 |
event=event, user_external_id='user:3', user_first_name='User', user_last_name='17' |
|
2231 |
) |
|
2232 |
Booking.objects.create( |
|
2233 |
event=event, user_external_id='user:4', user_first_name='User', user_last_name='35' |
|
2234 |
) |
|
2235 |
Booking.objects.create( |
|
2236 |
event=event, user_external_id='user:5', user_first_name='User', user_last_name='05' |
|
2237 |
) |
|
2238 |
booking6 = Booking.objects.create( |
|
2239 |
event=event, user_external_id='user:6', user_first_name='User', user_last_name='12 Cancelled' |
|
2240 |
) |
|
2241 |
booking6.cancel() |
|
2242 |
Booking.objects.create( |
|
2243 |
event=event, |
|
2244 |
user_external_id='user:7', |
|
2245 |
user_first_name='User', |
|
2246 |
user_last_name='Waiting', |
|
2247 |
in_waiting_list=True, |
|
2248 |
) |
|
2249 |
booking8 = Booking.objects.create( |
|
2250 |
event=event, |
|
2251 |
user_external_id='user:8', |
|
2252 |
user_first_name='User', |
|
2253 |
user_last_name='Waiting and Cancelled', |
|
2254 |
in_waiting_list=True, |
|
2255 |
) |
|
2256 |
booking8.cancel() |
|
2257 |
Booking.objects.create( |
|
2258 |
event=event, |
|
2259 |
user_external_id='user:1', |
|
2260 |
user_first_name='User', |
|
2261 |
user_last_name='Secondary', |
|
2262 |
primary_booking=booking1, |
|
2263 |
) |
|
2264 | ||
2265 |
login(app) |
|
2266 |
resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk) |
|
2267 |
resp.form['date_start'] = '2022-02-01' |
|
2268 |
resp.form['date_end'] = '2022-02-28' |
|
2269 |
resp = resp.form.submit() |
|
2270 |
slots = resp.context['form'].get_slots() |
|
2271 |
assert [u['user_id'] for u in slots['users']] == [ |
|
2272 |
'user:2', |
|
2273 |
'user:5', |
|
2274 |
'user:3', |
|
2275 |
'user:4', |
|
2276 |
'user:1', |
|
2277 |
] |
|
2278 | ||
2279 |
start = datetime.date(2022, 2, 1) |
|
2280 |
end = datetime.date(2022, 3, 1) |
|
2281 |
Subscription.objects.create( |
|
2282 |
agenda=agenda, |
|
2283 |
user_external_id='user:1', |
|
2284 |
user_first_name='Subscription', |
|
2285 |
user_last_name='42', |
|
2286 |
date_start=start, |
|
2287 |
date_end=end, |
|
2288 |
) |
|
2289 |
Subscription.objects.create( |
|
2290 |
agenda=agenda, |
|
2291 |
user_external_id='user:9', |
|
2292 |
user_first_name='Subscription', |
|
2293 |
user_last_name='43', |
|
2294 |
date_start=start, |
|
2295 |
date_end=end, |
|
2296 |
) |
|
2297 |
Subscription.objects.create( |
|
2298 |
agenda=agenda, |
|
2299 |
user_external_id='user:10', |
|
2300 |
user_first_name='Subscription', |
|
2301 |
user_last_name='14', |
|
2302 |
date_start=start, |
|
2303 |
date_end=end, |
|
2304 |
) |
|
2305 |
Subscription.objects.create( |
|
2306 |
agenda=agenda, |
|
2307 |
user_external_id='user:7', |
|
2308 |
user_first_name='Subscription', |
|
2309 |
user_last_name='Waiting', |
|
2310 |
date_start=start, |
|
2311 |
date_end=end, |
|
2312 |
) |
|
2313 |
Subscription.objects.create( |
|
2314 |
agenda=agenda, |
|
2315 |
user_external_id='user:42', |
|
2316 |
user_first_name='Subscription', |
|
2317 |
user_last_name='Too soon', |
|
2318 |
date_start=start - datetime.timedelta(days=1), |
|
2319 |
date_end=start, |
|
2320 |
) |
|
2321 |
Subscription.objects.create( |
|
2322 |
agenda=agenda, |
|
2323 |
user_external_id='user:43', |
|
2324 |
user_first_name='Subscription', |
|
2325 |
user_last_name='Too late', |
|
2326 |
date_start=end + datetime.timedelta(days=1), |
|
2327 |
date_end=end + datetime.timedelta(days=2), |
|
2328 |
) |
|
2329 | ||
2330 |
resp = resp.form.submit() |
|
2331 |
slots = resp.context['form'].get_slots() |
|
2332 |
assert [u['user_id'] for u in slots['users']] == [ |
|
2333 |
'user:2', |
|
2334 |
'user:5', |
|
2335 |
'user:6', |
|
2336 |
'user:10', |
|
2337 |
'user:3', |
|
2338 |
'user:4', |
|
2339 |
'user:1', |
|
2340 |
'user:9', |
|
2341 |
'user:7', |
|
2342 |
] |
|
2343 | ||
2344 | ||
2345 |
def test_events_timesheet_user_ids(app, admin_user): |
|
2346 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
2347 |
event = Event.objects.create( |
|
2348 |
start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda |
|
2349 |
) |
|
2350 |
booking = Booking.objects.create(event=event, user_first_name='User', user_last_name='42') |
|
2351 | ||
2352 |
login(app) |
|
2353 |
resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk) |
|
2354 |
resp.form['date_start'] = '2022-02-01' |
|
2355 |
resp.form['date_end'] = '2022-02-28' |
|
2356 |
resp = resp.form.submit() |
|
2357 |
slots = resp.context['form'].get_slots() |
|
2358 |
# no user_id found |
|
2359 |
assert [u['user_id'] for u in slots['users']] == [] |
|
2360 |
assert [u['user_first_name'] for u in slots['users']] == [] |
|
2361 |
assert [u['user_last_name'] for u in slots['users']] == [] |
|
2362 | ||
2363 |
booking.user_external_id = 'user:1' |
|
2364 |
booking.save() |
|
2365 | ||
2366 |
resp = resp.form.submit() |
|
2367 |
slots = resp.context['form'].get_slots() |
|
2368 |
assert [u['user_id'] for u in slots['users']] == [ |
|
2369 |
'user:1', |
|
2370 |
] |
|
2371 |
assert [u['user_first_name'] for u in slots['users']] == ['User'] |
|
2372 |
assert [u['user_last_name'] for u in slots['users']] == ['42'] |
|
2373 | ||
2374 |
Subscription.objects.create( |
|
2375 |
agenda=agenda, |
|
2376 |
user_external_id='user:1', |
|
2377 |
user_first_name='Subscription', |
|
2378 |
user_last_name='41', |
|
2379 |
date_start=datetime.date(2022, 2, 1), |
|
2380 |
date_end=datetime.date(2022, 3, 1), |
|
2381 |
) |
|
2382 | ||
2383 |
resp = resp.form.submit() |
|
2384 |
slots = resp.context['form'].get_slots() |
|
2385 |
assert [u['user_id'] for u in slots['users']] == [ |
|
2386 |
'user:1', |
|
2387 |
] |
|
2388 |
assert [u['user_first_name'] for u in slots['users']] == [ |
|
2389 |
'Subscription', |
|
2390 |
] |
|
2391 |
assert [u['user_last_name'] for u in slots['users']] == [ |
|
2392 |
'41', |
|
2393 |
] |
|
2394 | ||
2395 | ||
2396 |
@pytest.mark.freeze_time('2022-02-01') |
|
2397 |
def test_events_timesheet_booked(app, admin_user): |
|
2398 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
2399 |
event_date = make_aware(datetime.datetime(2022, 2, 15, 17, 0)) |
|
2400 |
event1 = Event.objects.create(label='event 1', start_datetime=event_date, places=10, agenda=agenda) |
|
2401 |
event2 = Event.objects.create(label='event 2', start_datetime=event_date, places=10, agenda=agenda) |
|
2402 |
event3 = Event.objects.create(label='event 3', start_datetime=event_date, places=10, agenda=agenda) |
|
2403 |
recurring_event1 = Event.objects.create( |
|
2404 |
label='recurring 1', |
|
2405 |
start_datetime=event_date, |
|
2406 |
places=10, |
|
2407 |
agenda=agenda, |
|
2408 |
recurrence_days=[1], |
|
2409 |
recurrence_end_date=event_date + datetime.timedelta(days=1), |
|
2410 |
) |
|
2411 |
recurring_event1.create_all_recurrences() |
|
2412 |
recurring_event1_occurence = recurring_event1.recurrences.first() |
|
2413 |
recurring_event2 = Event.objects.create( |
|
2414 |
label='recurring 2', |
|
2415 |
start_datetime=event_date, |
|
2416 |
places=10, |
|
2417 |
agenda=agenda, |
|
2418 |
recurrence_days=[1], |
|
2419 |
recurrence_end_date=event_date + datetime.timedelta(days=1), |
|
2420 |
) |
|
2421 |
recurring_event2.create_all_recurrences() |
|
2422 |
recurring_event2_occurence = recurring_event2.recurrences.first() |
|
2423 | ||
2424 |
Subscription.objects.create( |
|
2425 |
agenda=agenda, |
|
2426 |
user_external_id='user:1', |
|
2427 |
user_first_name='Subscription', |
|
2428 |
user_last_name='42', |
|
2429 |
date_start=datetime.date(2022, 2, 1), |
|
2430 |
date_end=datetime.date(2022, 3, 1), |
|
2431 |
) |
|
2432 |
Booking.objects.create( |
|
2433 |
event=event1, |
|
2434 |
user_external_id='user:1', |
|
2435 |
user_first_name='User', |
|
2436 |
user_last_name='42', |
|
2437 |
) |
|
2438 |
Booking.objects.create( |
|
2439 |
event=event2, |
|
2440 |
user_external_id='user:1', |
|
2441 |
user_first_name='User', |
|
2442 |
user_last_name='42', |
|
2443 |
cancellation_datetime=now(), |
|
2444 |
) |
|
2445 |
Booking.objects.create( |
|
2446 |
event=recurring_event1_occurence, |
|
2447 |
user_external_id='user:1', |
|
2448 |
user_first_name='User', |
|
2449 |
user_last_name='42', |
|
2450 |
) |
|
2451 |
Booking.objects.create( |
|
2452 |
event=recurring_event2_occurence, |
|
2453 |
user_external_id='user:1', |
|
2454 |
user_first_name='User', |
|
2455 |
user_last_name='42', |
|
2456 |
cancellation_datetime=now(), |
|
2457 |
) |
|
2458 | ||
2459 |
login(app) |
|
2460 |
resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk) |
|
2461 |
resp.form['date_start'] = '2022-02-01' |
|
2462 |
resp.form['date_end'] = '2022-02-28' |
|
2463 |
resp = resp.form.submit() |
|
2464 |
slots = resp.context['form'].get_slots() |
|
2465 | ||
2466 |
assert slots['events'] == [ |
|
2467 |
event1, |
|
2468 |
event2, |
|
2469 |
event3, |
|
2470 |
recurring_event1, |
|
2471 |
recurring_event2, |
|
2472 |
] |
|
2473 |
assert len(slots['users']) == 1 |
|
2474 |
assert slots['users'][0]['events'] == [ |
|
2475 |
{ |
|
2476 |
'event': event1, |
|
2477 |
'dates': {datetime.date(2022, 2, 15): True}, |
|
2478 |
}, |
|
2479 |
{ |
|
2480 |
'event': event2, |
|
2481 |
'dates': {datetime.date(2022, 2, 15): False}, |
|
2482 |
}, |
|
2483 |
{ |
|
2484 |
'event': event3, |
|
2485 |
'dates': {datetime.date(2022, 2, 15): False}, |
|
2486 |
}, |
|
2487 |
{ |
|
2488 |
'event': recurring_event1, |
|
2489 |
'dates': {datetime.date(2022, 2, 15): True}, |
|
2490 |
}, |
|
2491 |
{ |
|
2492 |
'event': recurring_event2, |
|
2493 |
'dates': {datetime.date(2022, 2, 15): False}, |
|
2494 |
}, |
|
2495 |
] |
|
2010 |
- |