From 38964940f7d8e570be40ef6f8b9719a2e35426a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 18 Feb 2022 15:30:10 +0100
Subject: [PATCH 5/5] manager: timesheet and date display (#61070)

---
 chrono/manager/forms.py                       |  34 +++
 chrono/manager/static/css/style.scss          |   4 +
 chrono/manager/static/css/timesheet.scss      |   5 +
 .../chrono/manager_events_timesheet.html      |  12 ++
 .../manager_events_timesheet_fragment.html    |   7 +-
 tests/manager/test_event.py                   | 203 ++++++++++++++++--
 6 files changed, 241 insertions(+), 24 deletions(-)

diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 092ace4..33a5c0a 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -376,6 +376,20 @@ class EventsTimesheetForm(forms.Form):
         required=False,
         help_text=_('Comma separated list of keys defined in extra_data.'),
     )
+    date_display = forms.ChoiceField(
+        label=_('Display'),
+        choices=[
+            ('all', _('All on the same page')),
+            ('month', _('1 month per page')),
+            ('week', _('1 week per page')),
+            ('custom', _('Custom')),
+        ],
+        initial='all',
+    )
+    custom_nb_dates_per_page = forms.IntegerField(
+        label=_('Number of dates per page'),
+        required=False,
+    )
     orientation = forms.ChoiceField(
         label=_('PDF orientation'),
         choices=[
@@ -421,6 +435,22 @@ class EventsTimesheetForm(forms.Form):
             dates_per_event_id[real_event.pk].append(date)
         dates = sorted(dates)
 
+        date_display = self.cleaned_data['date_display']
+        if date_display in ['month', 'week']:
+            grouper = defaultdict(list)
+            for date in dates:
+                if date_display == 'month':
+                    attr = date.month
+                else:
+                    attr = date.isocalendar().week
+                grouper[(date.year, attr)].append(date)
+            dates = [grouper[g] for g in sorted(grouper.keys())]
+        elif date_display == 'custom':
+            n = self.cleaned_data['custom_nb_dates_per_page']
+            dates = [dates[i : i + n] for i in range(0, len(dates), n)]
+        else:
+            dates = [dates]
+
         event_slots = []
         for event in events:
             event_slots.append(
@@ -493,6 +523,10 @@ class EventsTimesheetForm(forms.Form):
             elif (cleaned_data['date_start'] + relativedelta(months=3)) < cleaned_data['date_end']:
                 self.add_error('date_end', _('Please select an interval of no more than 3 months.'))
 
+        if cleaned_data.get('date_display') == 'custom':
+            if not cleaned_data.get('custom_nb_dates_per_page'):
+                self.add_error('custom_nb_dates_per_page', _('This field is required.'))
+
         return cleaned_data
 
 
diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss
index faf793e..3cf4044 100644
--- a/chrono/manager/static/css/style.scss
+++ b/chrono/manager/static/css/style.scss
@@ -503,3 +503,7 @@ table.timesheet {
 		}
 	}
 }
+
+.page_break {
+	height: 20px;
+}
diff --git a/chrono/manager/static/css/timesheet.scss b/chrono/manager/static/css/timesheet.scss
index 751f5e3..819c908 100644
--- a/chrono/manager/static/css/timesheet.scss
+++ b/chrono/manager/static/css/timesheet.scss
@@ -36,3 +36,8 @@ table.timesheet {
 		}
 	}
 }
+
+.page_break {
+	display: block;
+	page-break-before: always;
+}
diff --git a/chrono/manager/templates/chrono/manager_events_timesheet.html b/chrono/manager/templates/chrono/manager_events_timesheet.html
index cb7bcd5..08655b0 100644
--- a/chrono/manager/templates/chrono/manager_events_timesheet.html
+++ b/chrono/manager/templates/chrono/manager_events_timesheet.html
@@ -14,6 +14,18 @@
   <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');
+      });
+      </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>
diff --git a/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html b/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
index 986698a..dfe3d42 100644
--- a/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
+++ b/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
@@ -2,6 +2,7 @@
 
 {% with slots=form.get_slots %}
 {% with events_num=slots.events|length %}
+{% for dates in slots.dates %}
 <table class="main timesheet">
   <thead>
     <tr>
@@ -9,7 +10,7 @@
       <th>{% trans "Last name" %}</th>
       {% for k in slots.extra_data %}<th>{{ k }}</th>{% endfor %}
       {% if events_num > 1 %}<th>{% trans "Activity" %}</th>{% endif %}
-      {% for date in slots.dates %}<th class="date">{{ date|date:"D d/m" }}</th>{% endfor %}
+      {% for date in dates %}<th class="date">{{ date|date:"D d/m" }}</th>{% endfor %}
     </tr>
   </thead>
   <tbody>
@@ -21,12 +22,14 @@
       {% for k in slots.extra_data %}<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.extra_data|get:k }}</td>{% endfor %}
       {% endif %}
       {% if events_num > 1 %}<td>{{ event.event }}</td>{% endif %}
-      {% for date in slots.dates %}
+      {% for date in dates %}
       {% with booked=event.dates|get:date %}<td class="date">{% if booked is True %}☐{% elif booked is None %}-{% endif %}</td>{% endwith %}
       {% endfor %}
     </tr>
     {% endfor %}{% endfor %}
   </tbody>
 </table>
+{% if not forloop.last %}<div class="page_break"></div>{% endif %}
+{% endfor %}
 {% endwith %}
 {% endwith %}
diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index d880b2b..1f86969 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -2098,18 +2098,20 @@ def test_events_timesheet_slots(app, admin_user):
 
     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),
+        [
+            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,
@@ -2127,23 +2129,27 @@ def test_events_timesheet_slots(app, admin_user):
             'events': [
                 {
                     'event': event2,
-                    'dates': {date: False for date in slots['dates'] if date == datetime.date(2022, 2, 1)},
+                    'dates': {date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 1)},
                 },
                 {
                     'event': recurring_event1,
-                    'dates': {date: False for date in slots['dates'] if date.weekday() in [0, 1]},
+                    'dates': {date: False for date in slots['dates'][0] if date.weekday() in [0, 1]},
                 },
                 {
                     'event': recurring_event2,
-                    'dates': {date: False for date in slots['dates'] if date.weekday() in [1, 2]},
+                    'dates': {date: False for date in slots['dates'][0] if date.weekday() in [1, 2]},
                 },
                 {
                     'event': event3,
-                    'dates': {date: False for date in slots['dates'] if date == datetime.date(2022, 2, 15)},
+                    'dates': {
+                        date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 15)
+                    },
                 },
                 {
                     'event': event4,
-                    'dates': {date: False for date in slots['dates'] if date == datetime.date(2022, 2, 28)},
+                    'dates': {
+                        date: False for date in slots['dates'][0] if date == datetime.date(2022, 2, 28)
+                    },
                 },
             ],
         },
@@ -2195,9 +2201,11 @@ def test_events_timesheet_subscription_limits(app, admin_user):
 
     slots = resp.context['form'].get_slots()
     assert slots['dates'] == [
-        datetime.date(2022, 2, 1),
-        datetime.date(2022, 2, 15),
-        datetime.date(2022, 2, 28),
+        [
+            datetime.date(2022, 2, 1),
+            datetime.date(2022, 2, 15),
+            datetime.date(2022, 2, 28),
+        ]
     ]
 
     assert slots['events'] == [
@@ -2567,12 +2575,163 @@ def test_events_timesheet_extra_data(app, admin_user):
     assert slots['users'][0]['extra_data']['baz'] == ''
 
 
+@pytest.mark.freeze_time('2022-04-01')
+def test_events_timesheet_date_display(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events')
+    recurring_event = Event.objects.create(
+        label='recurring 1',
+        start_datetime=make_aware(datetime.datetime(2022, 1, 1, 12, 0)),
+        places=10,
+        agenda=agenda,
+        recurrence_days=[0],
+        recurrence_end_date=datetime.date(2022, 4, 1),
+    )
+    recurring_event.create_all_recurrences()
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='42',
+        date_start=datetime.date(2022, 1, 1),
+        date_end=datetime.date(2022, 4, 1),
+    )
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/events/timesheet' % agenda.pk)
+    resp.form['date_start'] = '2022-01-01'
+    resp.form['date_end'] = '2022-03-31'
+    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),
+        ]
+    ]
+
+    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),
+        ],
+    ]
+
+    resp.form['date_display'] = 'week'
+    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)],
+    ]
+
+    resp.form['date_display'] = 'custom'
+    resp = resp.form.submit()
+    assert resp.context['form'].errors['custom_nb_dates_per_page'] == ['This field is required.']
+
+    resp.form['custom_nb_dates_per_page'] = 10
+    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),
+        ],
+    ]
+
+    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),
+        ],
+    ]
+
+
 def test_events_timesheet_pdf(app, admin_user):
     agenda = Agenda.objects.create(label='Events', kind='events')
 
     login(app)
     resp = app.get(
-        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&extra_data=&orientation=portrait'
+        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&extra_data=&date_display=all&orientation=portrait'
         % agenda.pk
     )
     assert resp.headers['Content-Type'] == 'application/pdf'
@@ -2583,7 +2742,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&extra_data='
+        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&extra_data=&date_display=all'
         % agenda.pk
     )
     assert resp.context['form'].errors['orientation'] == ['This field is required.']
-- 
2.34.1

