From ff2706b4f1975aebb37e7c636ec992c28a6c8cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= Date: Tue, 28 Jun 2022 14:43:28 +0200 Subject: [PATCH] manager: timesheet & activity display configuration (#66681) --- chrono/manager/forms.py | 25 +- chrono/manager/static/css/style.scss | 4 + chrono/manager/static/css/timesheet.scss | 4 + .../manager_events_timesheet_fragment.html | 51 ++- tests/manager/test_event_timesheet.py | 300 ++++++++++-------- 5 files changed, 234 insertions(+), 150 deletions(-) diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 5a7d36db..c6d9cff5 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -543,7 +543,7 @@ class EventsTimesheetForm(forms.Form): initial='lastname,firstname', ) date_display = forms.ChoiceField( - label=_('Display'), + label=_('Date display'), choices=[ ('all', _('All on the same page')), ('month', _('1 month per page')), @@ -556,6 +556,14 @@ class EventsTimesheetForm(forms.Form): label=_('Number of dates per page'), required=False, ) + activity_display = forms.ChoiceField( + label=_('Activity display'), + choices=[ + ('row', _('In line')), + ('col', _('In column')), + ], + initial='row', + ) orientation = forms.ChoiceField( label=_('PDF orientation'), choices=[ @@ -572,6 +580,9 @@ class EventsTimesheetForm(forms.Form): if self.event is not None: del self.fields['date_start'] del self.fields['date_end'] + del self.fields['date_display'] + del self.fields['custom_nb_dates_per_page'] + del self.fields['activity_display'] def get_slots(self): extra_data = self.cleaned_data['extra_data'].split(',') @@ -604,27 +615,27 @@ class EventsTimesheetForm(forms.Form): .select_related('primary_event') .order_by('start_datetime', 'label') ) - dates = set() + dates = defaultdict(list) events = [] dates_per_event_id = defaultdict(list) for event in all_events: date = localtime(event.start_datetime).date() - dates.add(date) real_event = event.primary_event or event + dates[date].append(real_event) if real_event not in events: events.append(real_event) dates_per_event_id[real_event.pk].append(date) - dates = sorted(dates) + dates = sorted(dates.items(), key=lambda a: a[0]) - date_display = self.cleaned_data['date_display'] + date_display = self.cleaned_data.get('date_display') or 'all' if date_display in ['month', 'week']: grouper = defaultdict(list) - for date in dates: + for date, event in dates: if date_display == 'month': attr = date.month else: attr = date.isocalendar().week - grouper[(date.year, attr)].append(date) + grouper[(date.year, attr)].append((date, event)) dates = [grouper[g] for g in sorted(grouper.keys())] elif date_display == 'custom': n = self.cleaned_data['custom_nb_dates_per_page'] diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss index b4f10d88..4250bebd 100644 --- a/chrono/manager/static/css/style.scss +++ b/chrono/manager/static/css/style.scss @@ -494,6 +494,10 @@ table.timesheet { width: 20px; text-align: center; } + &.date-event { + width: 60px; + text-align: center; + } } td { padding: 0.5em 0.5ex; diff --git a/chrono/manager/static/css/timesheet.scss b/chrono/manager/static/css/timesheet.scss index 819c9088..da6669fd 100644 --- a/chrono/manager/static/css/timesheet.scss +++ b/chrono/manager/static/css/timesheet.scss @@ -25,6 +25,10 @@ table.timesheet { width: 30px; text-align: center; } + &.date-event { + width: 60px; + text-align: center; + } } td { border: 0.5px solid black; diff --git a/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html b/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html index b1a2238a..da15bbd8 100644 --- a/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html +++ b/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html @@ -11,24 +11,47 @@ {% trans "First name" %} {% trans "Last name" %} {% for k in slots.extra_data %}{{ k }}{% endfor %} - {% if events_num > 1 %}{% trans "Activity" %}{% endif %} - {% for date in dates %}{{ date|date:"D d/m" }}{% endfor %} + {% if events_num > 1 and form.cleaned_data.activity_display != 'col' %}{% trans "Activity" %}{% endif %} + {% for date, events in dates %} + {% if form.cleaned_data.activity_display == 'col' %} + {% for event in events %} + {% blocktrans with date=date|date:"d/m" %}{{ event }} of {{ date }}{% endblocktrans %} + {% endfor %} + {% else %} + {{ date|date:"D d/m" }} + {% endif %} + {% endfor %} - {% for user in grouper.users %}{% for event in user.events %} - - {% if forloop.first %} - 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }} - 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_last_name }} - {% for k in slots.extra_data %} 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.extra_data|get:k }}{% endfor %} - {% endif %} - {% if events_num > 1 %}{{ event.event }}{% endif %} - {% for date in dates %} - {% with booked=event.dates|get:date %}{% if booked is True %}☐{% elif booked is None %}-{% endif %}{% endwith %} + {% if form.cleaned_data.activity_display == 'col' %} + {% for user in grouper.users %} + + {{ user.user_first_name }} + {{ user.user_last_name }} + {% for k in slots.extra_data %}{{ user.extra_data|get:k }}{% endfor %} + {% for date, events in dates %} + {% for event in events %} + {% for item in user.events %}{% if item.event == event %}{% with booked=item.dates|get:date %}{% if booked is True %}☐{% elif booked is None %}-{% endif %}{% endwith %}{% endif %}{% endfor %} + {% endfor %} + {% endfor %} + {% endfor %} - - {% endfor %}{% endfor %} + {% else %} + {% for user in grouper.users %}{% for item in user.events %} + + {% if forloop.first %} + 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }} + 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_last_name }} + {% for k in slots.extra_data %} 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.extra_data|get:k }}{% endfor %} + {% endif %} + {% if events_num > 1 %}{{ item.event }}{% endif %} + {% for date, events in dates %} + {% with booked=item.dates|get:date %}{% if booked is True %}☐{% elif booked is None %}-{% endif %}{% endwith %} + {% endfor %} + + {% endfor %}{% endfor %} + {% endif %} {% if form.cleaned_data.with_page_break %}{% if not forloop.last or not forloop.parentloop.last %}
{% endif %}{% endif %} diff --git a/tests/manager/test_event_timesheet.py b/tests/manager/test_event_timesheet.py index eb82737d..5e3daf2f 100644 --- a/tests/manager/test_event_timesheet.py +++ b/tests/manager/test_event_timesheet.py @@ -1,4 +1,5 @@ import datetime +import itertools import pytest from django.db import connection @@ -99,21 +100,20 @@ def test_events_timesheet_slots(app, admin_user): assert len(ctx.captured_queries) == 7 slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 2, 1), - datetime.date(2022, 2, 2), - datetime.date(2022, 2, 7), - datetime.date(2022, 2, 8), - datetime.date(2022, 2, 9), - datetime.date(2022, 2, 14), - datetime.date(2022, 2, 15), - datetime.date(2022, 2, 16), - datetime.date(2022, 2, 21), - datetime.date(2022, 2, 22), - datetime.date(2022, 2, 23), - datetime.date(2022, 2, 28), - ] + assert len(slots['dates']) == 1 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 2, 1), + datetime.date(2022, 2, 2), + datetime.date(2022, 2, 7), + datetime.date(2022, 2, 8), + datetime.date(2022, 2, 9), + datetime.date(2022, 2, 14), + datetime.date(2022, 2, 15), + datetime.date(2022, 2, 16), + datetime.date(2022, 2, 21), + datetime.date(2022, 2, 22), + datetime.date(2022, 2, 23), + datetime.date(2022, 2, 28), ] assert slots['events'] == [ event2, @@ -131,26 +131,32 @@ def test_events_timesheet_slots(app, admin_user): 'events': [ { 'event': event2, - 'dates': {date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 1)}, + 'dates': { + date: False for date, events in slots['dates'][0] if date == datetime.date(2022, 2, 1) + }, }, { 'event': recurring_event1, - 'dates': {date: False for date in slots['dates'][0] if date.weekday() in [0, 1]}, + 'dates': {date: False for date, events in slots['dates'][0] if date.weekday() in [0, 1]}, }, { 'event': recurring_event2, - 'dates': {date: False for date in slots['dates'][0] if date.weekday() in [1, 2]}, + 'dates': {date: False for date, events in slots['dates'][0] if date.weekday() in [1, 2]}, }, { 'event': event3, 'dates': { - date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 15) + date: False + for date, events in slots['dates'][0] + if date == datetime.date(2022, 2, 15) }, }, { 'event': event4, 'dates': { - date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 28) + date: False + for date, events in slots['dates'][0] + if date == datetime.date(2022, 2, 28) }, }, ], @@ -202,12 +208,11 @@ def test_events_timesheet_subscription_limits(app, admin_user): resp = resp.form.submit() slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 2, 1), - datetime.date(2022, 2, 15), - datetime.date(2022, 2, 28), - ] + assert len(slots['dates']) == 1 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 2, 1), + datetime.date(2022, 2, 15), + datetime.date(2022, 2, 28), ] assert slots['events'] == [ @@ -507,6 +512,45 @@ def test_events_timesheet_booked(app, admin_user): }, ] + # activity_display: row + assert len(resp.pyquery.find('th.activity')) == 1 + assert resp.pyquery.find('th.activity')[0].text == 'Activity' + assert len(resp.pyquery.find('th.date')) == 1 + assert len(resp.pyquery.find('th.date-event')) == 0 + assert resp.pyquery.find('th.date[data-id="15-02"]')[0].text == 'Tue 15/02' + assert len(resp.pyquery.find('td.activity')) == 5 + assert resp.pyquery.find('td.activity')[0].text == 'event 1' + assert resp.pyquery.find('td.activity')[1].text == 'event 2' + assert resp.pyquery.find('td.activity')[2].text == 'event 3' + assert resp.pyquery.find('td.activity')[3].text == 'recurring 1' + assert resp.pyquery.find('td.activity')[4].text == 'recurring 2' + assert len(resp.pyquery.find('td.date')) == 5 + assert resp.pyquery.find('td.date[data-id="15-02:event-1:user:1"]')[0].text == '☐' + assert resp.pyquery.find('td.date[data-id="15-02:event-2:user:1"]')[0].text is None + assert resp.pyquery.find('td.date[data-id="15-02:event-3:user:1"]')[0].text is None + assert resp.pyquery.find('td.date[data-id="15-02:recurring-1:user:1"]')[0].text == '☐' + assert resp.pyquery.find('td.date[data-id="15-02:recurring-2:user:1"]')[0].text is None + + # activity_display: col + resp.form['activity_display'] = 'col' + resp = resp.form.submit() + new_slots = resp.context['form'].get_slots() + assert new_slots == slots + assert len(resp.pyquery.find('th.activity')) == 0 + assert len(resp.pyquery.find('th.date')) == 0 + assert len(resp.pyquery.find('th.date-event')) == 5 + assert resp.pyquery.find('th.date-event[data-id="15-02:event-1"]')[0].text == 'event 1 of 15/02' + assert resp.pyquery.find('th.date-event[data-id="15-02:event-2"]')[0].text == 'event 2 of 15/02' + assert resp.pyquery.find('th.date-event[data-id="15-02:event-3"]')[0].text == 'event 3 of 15/02' + assert resp.pyquery.find('th.date-event[data-id="15-02:recurring-1"]')[0].text == 'recurring 1 of 15/02' + assert resp.pyquery.find('th.date-event[data-id="15-02:recurring-2"]')[0].text == 'recurring 2 of 15/02' + assert len(resp.pyquery.find('td.date')) == 5 + assert resp.pyquery.find('td.date[data-id="15-02:event-1:user:1"]')[0].text == '☐' + assert resp.pyquery.find('td.date[data-id="15-02:event-2:user:1"]')[0].text is None + assert resp.pyquery.find('td.date[data-id="15-02:event-3:user:1"]')[0].text is None + assert resp.pyquery.find('td.date[data-id="15-02:recurring-1:user:1"]')[0].text == '☐' + assert resp.pyquery.find('td.date[data-id="15-02:recurring-2:user:1"]')[0].text is None + def test_events_timesheet_extra_data(app, admin_user): agenda = Agenda.objects.create(label='Events', kind='events') @@ -732,48 +776,46 @@ def test_events_timesheet_date_display(app, admin_user): resp = resp.form.submit() slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 1, 3), - datetime.date(2022, 1, 10), - datetime.date(2022, 1, 17), - datetime.date(2022, 1, 24), - datetime.date(2022, 1, 31), - datetime.date(2022, 2, 7), - datetime.date(2022, 2, 14), - datetime.date(2022, 2, 21), - datetime.date(2022, 2, 28), - datetime.date(2022, 3, 7), - datetime.date(2022, 3, 14), - datetime.date(2022, 3, 21), - datetime.date(2022, 3, 28), - ] + assert len(slots['dates']) == 1 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 1, 3), + datetime.date(2022, 1, 10), + datetime.date(2022, 1, 17), + datetime.date(2022, 1, 24), + datetime.date(2022, 1, 31), + datetime.date(2022, 2, 7), + datetime.date(2022, 2, 14), + datetime.date(2022, 2, 21), + datetime.date(2022, 2, 28), + datetime.date(2022, 3, 7), + datetime.date(2022, 3, 14), + datetime.date(2022, 3, 21), + datetime.date(2022, 3, 28), ] resp.form['date_display'] = 'month' resp = resp.form.submit() slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 1, 3), - datetime.date(2022, 1, 10), - datetime.date(2022, 1, 17), - datetime.date(2022, 1, 24), - datetime.date(2022, 1, 31), - ], - [ - datetime.date(2022, 2, 7), - datetime.date(2022, 2, 14), - datetime.date(2022, 2, 21), - datetime.date(2022, 2, 28), - ], - [ - datetime.date(2022, 3, 7), - datetime.date(2022, 3, 14), - datetime.date(2022, 3, 21), - datetime.date(2022, 3, 28), - ], + assert len(slots['dates']) == 3 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 1, 3), + datetime.date(2022, 1, 10), + datetime.date(2022, 1, 17), + datetime.date(2022, 1, 24), + datetime.date(2022, 1, 31), + ] + assert [d[0] for d in slots['dates'][1]] == [ + datetime.date(2022, 2, 7), + datetime.date(2022, 2, 14), + datetime.date(2022, 2, 21), + datetime.date(2022, 2, 28), + ] + assert [d[0] for d in slots['dates'][2]] == [ + datetime.date(2022, 3, 7), + datetime.date(2022, 3, 14), + datetime.date(2022, 3, 21), + datetime.date(2022, 3, 28), ] resp.form['date_display'] = 'week' @@ -781,20 +823,21 @@ def test_events_timesheet_date_display(app, admin_user): slots = resp.context['form'].get_slots() assert resp.text.count('
') == 12 - assert slots['dates'] == [ - [datetime.date(2022, 1, 3)], - [datetime.date(2022, 1, 10)], - [datetime.date(2022, 1, 17)], - [datetime.date(2022, 1, 24)], - [datetime.date(2022, 1, 31)], - [datetime.date(2022, 2, 7)], - [datetime.date(2022, 2, 14)], - [datetime.date(2022, 2, 21)], - [datetime.date(2022, 2, 28)], - [datetime.date(2022, 3, 7)], - [datetime.date(2022, 3, 14)], - [datetime.date(2022, 3, 21)], - [datetime.date(2022, 3, 28)], + assert len(slots['dates']) == 13 + assert [d[0] for d in itertools.chain(*slots['dates'])] == [ + datetime.date(2022, 1, 3), + datetime.date(2022, 1, 10), + datetime.date(2022, 1, 17), + datetime.date(2022, 1, 24), + datetime.date(2022, 1, 31), + datetime.date(2022, 2, 7), + datetime.date(2022, 2, 14), + datetime.date(2022, 2, 21), + datetime.date(2022, 2, 28), + datetime.date(2022, 3, 7), + datetime.date(2022, 3, 14), + datetime.date(2022, 3, 21), + datetime.date(2022, 3, 28), ] resp.form['date_display'] = 'custom' @@ -805,54 +848,52 @@ def test_events_timesheet_date_display(app, admin_user): resp = resp.form.submit() slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 1, 3), - datetime.date(2022, 1, 10), - datetime.date(2022, 1, 17), - datetime.date(2022, 1, 24), - datetime.date(2022, 1, 31), - datetime.date(2022, 2, 7), - datetime.date(2022, 2, 14), - datetime.date(2022, 2, 21), - datetime.date(2022, 2, 28), - datetime.date(2022, 3, 7), - ], - [ - datetime.date(2022, 3, 14), - datetime.date(2022, 3, 21), - datetime.date(2022, 3, 28), - ], + assert len(slots['dates']) == 2 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 1, 3), + datetime.date(2022, 1, 10), + datetime.date(2022, 1, 17), + datetime.date(2022, 1, 24), + datetime.date(2022, 1, 31), + datetime.date(2022, 2, 7), + datetime.date(2022, 2, 14), + datetime.date(2022, 2, 21), + datetime.date(2022, 2, 28), + datetime.date(2022, 3, 7), + ] + assert [d[0] for d in slots['dates'][1]] == [ + datetime.date(2022, 3, 14), + datetime.date(2022, 3, 21), + datetime.date(2022, 3, 28), ] resp.form['custom_nb_dates_per_page'] = 3 resp = resp.form.submit() slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 1, 3), - datetime.date(2022, 1, 10), - datetime.date(2022, 1, 17), - ], - [ - datetime.date(2022, 1, 24), - datetime.date(2022, 1, 31), - datetime.date(2022, 2, 7), - ], - [ - datetime.date(2022, 2, 14), - datetime.date(2022, 2, 21), - datetime.date(2022, 2, 28), - ], - [ - datetime.date(2022, 3, 7), - datetime.date(2022, 3, 14), - datetime.date(2022, 3, 21), - ], - [ - datetime.date(2022, 3, 28), - ], + assert len(slots['dates']) == 5 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 1, 3), + datetime.date(2022, 1, 10), + datetime.date(2022, 1, 17), + ] + assert [d[0] for d in slots['dates'][1]] == [ + datetime.date(2022, 1, 24), + datetime.date(2022, 1, 31), + datetime.date(2022, 2, 7), + ] + assert [d[0] for d in slots['dates'][2]] == [ + datetime.date(2022, 2, 14), + datetime.date(2022, 2, 21), + datetime.date(2022, 2, 28), + ] + assert [d[0] for d in slots['dates'][3]] == [ + datetime.date(2022, 3, 7), + datetime.date(2022, 3, 14), + datetime.date(2022, 3, 21), + ] + assert [d[0] for d in slots['dates'][4]] == [ + datetime.date(2022, 3, 28), ] Subscription.objects.create( @@ -881,8 +922,8 @@ def test_events_timesheet_pdf(app, admin_user): login(app) resp = app.get( - '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname&date_display=all&orientation=portrait' - % agenda.pk + '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname' + '&date_display=all&activity_display=row&orientation=portrait' % agenda.pk ) assert resp.headers['Content-Type'] == 'application/pdf' assert ( @@ -892,7 +933,7 @@ def test_events_timesheet_pdf(app, admin_user): # form invalid resp = app.get( - '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname&date_display=all' + '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&sort=lastname,firstname&date_display=all&activity_display=row' % agenda.pk ) assert resp.context['form'].errors['orientation'] == ['This field is required.'] @@ -946,6 +987,9 @@ def test_event_timesheet_form(app, admin_user): 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 'date_display' not in resp.context['form'].fields + assert 'custom_nb_dates_per_page' not in resp.context['form'].fields + assert 'activity_display' not in resp.context['form'].fields assert resp.context['form'].errors == {} @@ -970,10 +1014,9 @@ def test_event_timesheet_slots(app, admin_user): assert len(ctx.captured_queries) == 7 slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 2, 15), - ] + assert len(slots['dates']) == 1 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 2, 15), ] assert slots['events'] == [event] assert slots['users'][0]['users'] == [ @@ -1027,10 +1070,9 @@ def test_event_timesheet_subscription_limits(app, admin_user): resp = resp.form.submit() slots = resp.context['form'].get_slots() - assert slots['dates'] == [ - [ - datetime.date(2022, 2, 15), - ] + assert len(slots['dates']) == 1 + assert [d[0] for d in slots['dates'][0]] == [ + datetime.date(2022, 2, 15), ] assert slots['events'] == [