From 5f0720c048d4c1f0b4974595eea109d47fa58dae 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 16:54:41 +0100
Subject: [PATCH 6/6] manager: timesheet grouper (#61920)

---
 chrono/manager/forms.py                       |  42 +++++-
 .../chrono/manager_events_timesheet.html      |   8 ++
 .../manager_events_timesheet_fragment.html    |   8 +-
 tests/manager/test_event.py                   | 136 +++++++++++++-----
 4 files changed, 155 insertions(+), 39 deletions(-)

diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 33a5c0a..9dc7fe3 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -376,6 +376,16 @@ class EventsTimesheetForm(forms.Form):
         required=False,
         help_text=_('Comma separated list of keys defined in extra_data.'),
     )
+    group_by = forms.CharField(
+        label=_('Group by'),
+        max_length=50,
+        required=False,
+        help_text=_('Key defined in extra_data.'),
+    )
+    with_page_break = forms.BooleanField(
+        label=_('Add a page break before earch grouper'),
+        required=False,
+    )
     date_display = forms.ChoiceField(
         label=_('Display'),
         choices=[
@@ -406,6 +416,10 @@ class EventsTimesheetForm(forms.Form):
     def get_slots(self):
         extra_data = self.cleaned_data['extra_data'].split(',')
         extra_data = [d.strip() for d in extra_data if d.strip()]
+        group_by = self.cleaned_data['group_by'].strip()
+        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))
         )
@@ -466,7 +480,7 @@ class EventsTimesheetForm(forms.Form):
                 'user_id': subscription.user_external_id,
                 'user_first_name': subscription.user_first_name,
                 'user_last_name': subscription.user_last_name,
-                'extra_data': {k: (subscription.extra_data or {}).get(k) or '' for k in extra_data},
+                'extra_data': {k: (subscription.extra_data or {}).get(k) or '' for k in all_extra_data},
                 'events': copy.deepcopy(event_slots),
             }
 
@@ -491,7 +505,7 @@ class EventsTimesheetForm(forms.Form):
                     'user_id': user_id,
                     'user_first_name': booking.user_first_name,
                     'user_last_name': booking.user_last_name,
-                    'extra_data': {k: (booking.extra_data or {}).get(k) or '' for k in extra_data},
+                    'extra_data': {k: (booking.extra_data or {}).get(k) or '' for k in all_extra_data},
                     'events': copy.deepcopy(event_slots),
                 }
             if booking.cancellation_datetime is not None:
@@ -505,7 +519,29 @@ class EventsTimesheetForm(forms.Form):
                     event['dates'][date] = True
                 break
 
-        users = sorted(users.values(), key=itemgetter('user_last_name', 'user_first_name', 'user_id'))
+        if group_by:
+            groupers = defaultdict(list)
+            for user in users.values():
+                groupers[user['extra_data'].get(group_by) or ''].append(user)
+            users = [
+                {
+                    'grouper': g,
+                    'users': sorted(u, key=itemgetter('user_last_name', 'user_first_name', 'user_id')),
+                }
+                for g, u in groupers.items()
+            ]
+            users = sorted(users, key=itemgetter('grouper'))
+            if users and users[0]['grouper'] == '':
+                users = users[1:] + users[:1]
+        else:
+            users = [
+                {
+                    'grouper': '',
+                    'users': sorted(
+                        users.values(), key=itemgetter('user_last_name', 'user_first_name', 'user_id')
+                    ),
+                }
+            ]
 
         return {
             'dates': dates,
diff --git a/chrono/manager/templates/chrono/manager_events_timesheet.html b/chrono/manager/templates/chrono/manager_events_timesheet.html
index 08655b0..6a16e15 100644
--- a/chrono/manager/templates/chrono/manager_events_timesheet.html
+++ b/chrono/manager/templates/chrono/manager_events_timesheet.html
@@ -24,6 +24,14 @@
           }
         });
         $('#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>
diff --git a/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html b/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
index dfe3d42..1a5764e 100644
--- a/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
+++ b/chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
@@ -3,6 +3,8 @@
 {% with slots=form.get_slots %}
 {% with events_num=slots.events|length %}
 {% for dates in slots.dates %}
+{% for grouper in slots.users %}
+{% if form.cleaned_data.group_by %}<h5>{{ form.cleaned_data.group_by }}: {{ grouper.grouper }}</h5>{% endif %}
 <table class="main timesheet">
   <thead>
     <tr>
@@ -14,7 +16,7 @@
     </tr>
   </thead>
   <tbody>
-    {% for user in slots.users %}{% for event in user.events %}
+    {% for user in grouper.users %}{% for event in user.events %}
     <tr>
       {% if forloop.first %}
       <td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }}</td>
@@ -29,7 +31,9 @@
     {% endfor %}{% endfor %}
   </tbody>
 </table>
-{% if not forloop.last %}<div class="page_break"></div>{% endif %}
+{% if form.cleaned_data.with_page_break %}<div class="page_break"></div>{% endif %}
+{% endfor %}
+{% if not form.cleaned_data.with_page_break %}<div class="page_break"></div>{% endif %}
 {% endfor %}
 {% endwith %}
 {% endwith %}
diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index 71a63c3..bde5a6a 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -2120,7 +2120,7 @@ def test_events_timesheet_slots(app, admin_user):
         event3,
         event4,
     ]
-    assert slots['users'] == [
+    assert slots['users'][0]['users'] == [
         {
             'user_id': 'user:1',
             'user_first_name': 'Subscription',
@@ -2213,15 +2213,16 @@ def test_events_timesheet_subscription_limits(app, admin_user):
         event2,
         event3,
     ]
-    assert len(slots['users']) == 8
-    assert slots['users'][0]['user_id'] == 'user:2022-02-01-2022-02-02'
-    assert slots['users'][1]['user_id'] == 'user:2022-02-01-2022-02-15'
-    assert slots['users'][2]['user_id'] == 'user:2022-02-01-2022-02-16'
-    assert slots['users'][3]['user_id'] == 'user:2022-02-01-2022-03-01'
-    assert slots['users'][4]['user_id'] == 'user:2022-02-15-2022-02-28'
-    assert slots['users'][5]['user_id'] == 'user:2022-02-15-2022-03-01'
-    assert slots['users'][6]['user_id'] == 'user:2022-02-16-2022-03-01'
-    assert slots['users'][7]['user_id'] == 'user:2022-02-28-2022-03-01'
+    users = slots['users'][0]['users']
+    assert len(users) == 8
+    assert users[0]['user_id'] == 'user:2022-02-01-2022-02-02'
+    assert users[1]['user_id'] == 'user:2022-02-01-2022-02-15'
+    assert users[2]['user_id'] == 'user:2022-02-01-2022-02-16'
+    assert users[3]['user_id'] == 'user:2022-02-01-2022-03-01'
+    assert users[4]['user_id'] == 'user:2022-02-15-2022-02-28'
+    assert users[5]['user_id'] == 'user:2022-02-15-2022-03-01'
+    assert users[6]['user_id'] == 'user:2022-02-16-2022-03-01'
+    assert users[7]['user_id'] == 'user:2022-02-28-2022-03-01'
 
 
 def test_events_timesheet_users(app, admin_user):
@@ -2278,7 +2279,7 @@ def test_events_timesheet_users(app, admin_user):
     resp.form['date_end'] = '2022-02-28'
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users']] == [
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
         'user:2',
         'user:5',
         'user:3',
@@ -2339,7 +2340,7 @@ def test_events_timesheet_users(app, admin_user):
 
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users']] == [
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
         'user:2',
         'user:5',
         'user:6',
@@ -2366,20 +2367,20 @@ def test_events_timesheet_user_ids(app, admin_user):
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
     # no user_id found
-    assert [u['user_id'] for u in slots['users']] == []
-    assert [u['user_first_name'] for u in slots['users']] == []
-    assert [u['user_last_name'] for u in slots['users']] == []
+    assert [u['user_id'] for u in slots['users'][0]['users']] == []
+    assert [u['user_first_name'] for u in slots['users'][0]['users']] == []
+    assert [u['user_last_name'] for u in slots['users'][0]['users']] == []
 
     booking.user_external_id = 'user:1'
     booking.save()
 
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users']] == [
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
         'user:1',
     ]
-    assert [u['user_first_name'] for u in slots['users']] == ['User']
-    assert [u['user_last_name'] for u in slots['users']] == ['42']
+    assert [u['user_first_name'] for u in slots['users'][0]['users']] == ['User']
+    assert [u['user_last_name'] for u in slots['users'][0]['users']] == ['42']
 
     Subscription.objects.create(
         agenda=agenda,
@@ -2392,13 +2393,13 @@ def test_events_timesheet_user_ids(app, admin_user):
 
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
-    assert [u['user_id'] for u in slots['users']] == [
+    assert [u['user_id'] for u in slots['users'][0]['users']] == [
         'user:1',
     ]
-    assert [u['user_first_name'] for u in slots['users']] == [
+    assert [u['user_first_name'] for u in slots['users'][0]['users']] == [
         'Subscription',
     ]
-    assert [u['user_last_name'] for u in slots['users']] == [
+    assert [u['user_last_name'] for u in slots['users'][0]['users']] == [
         '41',
     ]
 
@@ -2480,8 +2481,8 @@ def test_events_timesheet_booked(app, admin_user):
         recurring_event1,
         recurring_event2,
     ]
-    assert len(slots['users']) == 1
-    assert slots['users'][0]['events'] == [
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['users'][0]['users'][0]['events'] == [
         {
             'event': event1,
             'dates': {datetime.date(2022, 2, 15): True},
@@ -2527,25 +2528,27 @@ def test_events_timesheet_extra_data(app, admin_user):
     slots = resp.context['form'].get_slots()
 
     assert len(slots['users']) == 1
+    assert slots['users'][0]['grouper'] == ''
+    assert len(slots['users'][0]['users']) == 1
     assert slots['extra_data'] == ['foo']
-    assert slots['users'][0]['extra_data']['foo'] == 'bar'
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'bar'
 
     resp.form['extra_data'] = ' foo ,baz,,'
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
 
-    assert len(slots['users']) == 1
+    assert len(slots['users'][0]['users']) == 1
     assert slots['extra_data'] == ['foo', 'baz']
-    assert slots['users'][0]['extra_data']['foo'] == 'bar'
-    assert slots['users'][0]['extra_data']['baz'] == 'blah'
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'bar'
+    assert slots['users'][0]['users'][0]['extra_data']['baz'] == 'blah'
 
     resp.form['extra_data'] = 'unknown'
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
 
-    assert len(slots['users']) == 1
+    assert len(slots['users'][0]['users']) == 1
     assert slots['extra_data'] == ['unknown']
-    assert slots['users'][0]['extra_data']['unknown'] == ''
+    assert slots['users'][0]['users'][0]['extra_data']['unknown'] == ''
 
     Subscription.objects.create(
         agenda=agenda,
@@ -2561,18 +2564,83 @@ def test_events_timesheet_extra_data(app, admin_user):
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
 
-    assert len(slots['users']) == 1
+    assert len(slots['users'][0]['users']) == 1
     assert slots['extra_data'] == ['foo']
-    assert slots['users'][0]['extra_data']['foo'] == 'baz'
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'baz'
 
     resp.form['extra_data'] = ' foo ,baz,,'
     resp = resp.form.submit()
     slots = resp.context['form'].get_slots()
 
-    assert len(slots['users']) == 1
+    assert len(slots['users'][0]['users']) == 1
     assert slots['extra_data'] == ['foo', 'baz']
-    assert slots['users'][0]['extra_data']['foo'] == 'baz'
-    assert slots['users'][0]['extra_data']['baz'] == ''
+    assert slots['users'][0]['users'][0]['extra_data']['foo'] == 'baz'
+    assert slots['users'][0]['users'][0]['extra_data']['baz'] == ''
+
+    Booking.objects.create(
+        event=event,
+        user_first_name='User',
+        user_last_name='43',
+        user_external_id='user:2',
+        extra_data={'foo': 'bar', 'baz': 'aa'},
+    )
+    Booking.objects.create(
+        event=event,
+        user_first_name='User',
+        user_last_name='44',
+        user_external_id='user:3',
+        extra_data={'foo': 'bar2', 'baz': 'aa'},
+    )
+
+    resp.form['extra_data'] = ''
+    resp.form['group_by'] = 'foo'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users']) == 3
+    assert slots['users'][0]['grouper'] == 'bar'
+    assert len(slots['users'][0]['users']) == 1
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
+    assert slots['users'][1]['grouper'] == 'bar2'
+    assert len(slots['users'][1]['users']) == 1
+    assert slots['users'][1]['users'][0]['user_id'] == 'user:3'
+    assert slots['users'][2]['grouper'] == 'baz'
+    assert len(slots['users'][2]['users']) == 1
+    assert slots['users'][2]['users'][0]['user_id'] == 'user:1'
+
+    resp.form['group_by'] = 'baz'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users']) == 2
+    assert slots['users'][0]['grouper'] == 'aa'
+    assert len(slots['users'][0]['users']) == 2
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
+    assert slots['users'][0]['users'][1]['user_id'] == 'user:3'
+    assert slots['users'][1]['grouper'] == ''
+    assert len(slots['users'][1]['users']) == 1
+    assert slots['users'][1]['users'][0]['user_id'] == 'user:1'
+
+    Subscription.objects.update(extra_data={'foo': 'baz', 'baz': 'blah'})
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users']) == 2
+    assert slots['users'][0]['grouper'] == 'aa'
+    assert len(slots['users'][0]['users']) == 2
+    assert slots['users'][0]['users'][0]['user_id'] == 'user:2'
+    assert slots['users'][0]['users'][1]['user_id'] == 'user:3'
+    assert slots['users'][1]['grouper'] == 'blah'
+    assert len(slots['users'][1]['users']) == 1
+    assert slots['users'][1]['users'][0]['user_id'] == 'user:1'
+
+    resp.form['group_by'] = 'unknown'
+    resp = resp.form.submit()
+    slots = resp.context['form'].get_slots()
+
+    assert len(slots['users']) == 1
+    assert slots['users'][0]['grouper'] == ''
+    assert len(slots['users'][0]['users']) == 3
 
 
 @pytest.mark.freeze_time('2022-04-01')
-- 
2.34.1

