From 9ace4823eb4286de25bebb7fe25a6ef08af50ae7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 22 Apr 2021 11:39:42 +0200
Subject: [PATCH 2/2] managers: display filters on check page (#53238)

---
 chrono/manager/forms.py                       | 35 +++++++++
 chrono/manager/static/css/style.scss          |  5 ++
 .../templates/chrono/manager_event_check.html | 11 +++
 chrono/manager/views.py                       | 19 +++--
 tests/manager/test_event.py                   | 77 ++++++++++++++++++-
 5 files changed, 140 insertions(+), 7 deletions(-)

diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 565cf7a..78ff99e 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -16,9 +16,11 @@
 
 from __future__ import unicode_literals
 
+import collections
 import csv
 import datetime
 
+import django_filters
 from django import forms
 from django.conf import settings
 from django.contrib.auth.models import Group
@@ -225,6 +227,39 @@ class EventForm(forms.ModelForm):
         return self.instance
 
 
+class BookingCheckFilterSet(django_filters.FilterSet):
+    class Meta:
+        model = Booking
+        fields = []
+
+    def __init__(self, *args, **kwargs):
+        self.agenda = kwargs.pop('agenda')
+        super().__init__(*args, **kwargs)
+
+        # build filters from DB
+        agenda_filters = self.agenda.get_booking_check_filters()
+        filters = collections.defaultdict(set)
+        for extra_data in self.queryset.filter(extra_data__has_any_keys=agenda_filters).values_list(
+            'extra_data', flat=True
+        ):
+            for k, v in extra_data.items():
+                if k in agenda_filters:
+                    filters[k].add(v)
+        filters = sorted(filters.items())
+        filters = {k: sorted(list(v)) for k, v in filters}
+
+        # add filters to filterset
+        for key, values in filters.items():
+            self.filters[key] = django_filters.ChoiceFilter(
+                label=_('Filter by %s') % key,
+                field_name='extra_data__%s' % key,
+                lookup_expr='iexact',
+                choices=[(v, v) for v in values],
+                empty_label=_('all'),
+                widget=forms.RadioSelect,
+            )
+
+
 class BookingAbsenceReasonForm(forms.Form):
     reason = forms.ChoiceField(required=False)
 
diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss
index ba8c5b8..d36a56d 100644
--- a/chrono/manager/static/css/style.scss
+++ b/chrono/manager/static/css/style.scss
@@ -382,6 +382,11 @@ table.check-bookings {
 	}
 }
 
+form.check-bookings-filters li {
+	display: inline;
+	margin-right: 10px;
+}
+
 // booking colors
 $booking-colors: (
 	0: hsl(30, 100%, 46%),
diff --git a/chrono/manager/templates/chrono/manager_event_check.html b/chrono/manager/templates/chrono/manager_event_check.html
index c113bf5..0e3ba7e 100644
--- a/chrono/manager/templates/chrono/manager_event_check.html
+++ b/chrono/manager/templates/chrono/manager_event_check.html
@@ -14,6 +14,17 @@
     {% blocktrans with places=object.places booked_places=booked|length %}Bookings ({{ booked_places }}/{{ places }}){% endblocktrans %}
   </h3>
   <div>
+    <form class="check-bookings-filters">
+      {{ filterset.form.as_p }}
+      <script>
+      $(function() {
+          $('form.check-bookings-filters input').on('change',
+              function() {
+                $(this).parents('form').submit();
+          });
+      });
+      </script>
+    </form>
     <table class="main check-bookings">
       <tbody>
       {% for booking in booked %}
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 5f92c2e..2ece831 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -14,6 +14,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import collections
 import csv
 import datetime
 import itertools
@@ -89,6 +90,7 @@ from .forms import (
     AgendasImportForm,
     BookingAbsenceReasonForm,
     BookingCancelForm,
+    BookingCheckFilterSet,
     DeskForm,
     EventCancelForm,
     EventForm,
@@ -1946,17 +1948,22 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
+
         event = self.object
-        context['booked'] = event.booking_set.filter(
-            cancellation_datetime__isnull=True, in_waiting_list=False
-        ).order_by('user_name')
-        context['waiting'] = event.booking_set.filter(
-            cancellation_datetime__isnull=True, in_waiting_list=True
-        ).order_by('user_name')
+        booked_qs = event.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=False)
+        filterset = BookingCheckFilterSet(self.request.GET, queryset=booked_qs, agenda=self.agenda)
+        context['filterset'] = filterset
+
+        # build booking list
+        context['booked'] = filterset.qs.order_by('user_name')
         for booking in context['booked']:
             booking.form = BookingAbsenceReasonForm(
                 agenda=self.agenda, initial={'reason': booking.user_absence_reason}
             )
+        context['waiting'] = event.booking_set.filter(
+            cancellation_datetime__isnull=True, in_waiting_list=True
+        ).order_by('user_name')
+
         return context
 
 
diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index 248faf0..1ff4a70 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -1077,7 +1077,7 @@ def test_event_check(app, admin_user):
     assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) not in resp
     app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=404)
 
-    event.start_datetime = localtime(now()).replace(hour=22, minute=0)  # it'os ok all the day
+    event.start_datetime = localtime(now()).replace(hour=22, minute=0)  # it's ok all the day
     event.save()
     resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
     assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) in resp
@@ -1164,6 +1164,81 @@ def test_event_check(app, admin_user):
     event.cancellation_datetime = None
 
 
+def test_event_check_filters(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events', booking_check_filters='foo,bar')
+    event = Event.objects.create(
+        label='xyz',
+        start_datetime=now() - datetime.timedelta(days=1),
+        places=10,
+        agenda=agenda,
+    )
+    Booking.objects.create(event=event, user_name='User none')
+    Booking.objects.create(event=event, user_name='User empty', extra_data={})
+    Booking.objects.create(event=event, user_name='User foo-val1 bar-none', extra_data={'foo': 'val1'})
+    Booking.objects.create(
+        event=event, user_name='User foo-val2 bar-val1', extra_data={'foo': 'val2', 'bar': 'val1'}
+    )
+    Booking.objects.create(
+        event=event, user_name='User foo-val1 bar-val2', extra_data={'foo': 'val1', 'bar': 'val2'}
+    )
+    Booking.objects.create(event=event, user_name='User foo-none bar-val2', extra_data={'bar': 'val2'})
+    login(app)
+
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    assert 'User none' in resp
+    assert 'User empty' in resp
+    assert 'User foo-val1' in resp
+    assert 'User foo-val2 bar-val1' in resp
+    assert 'User foo-val1 bar-val2' in resp
+    assert 'User foo-none bar-val2' in resp
+
+    resp = app.get(
+        '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'unknown': 'unknown'}
+    )
+    assert 'User none' in resp
+    assert 'User empty' in resp
+    assert 'User foo-val1 bar-none' in resp
+    assert 'User foo-val2 bar-val1' in resp
+    assert 'User foo-val1 bar-val2' in resp
+    assert 'User foo-none bar-val2' in resp
+
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'foo': 'unknown'})
+    assert 'User none' in resp
+    assert 'User empty' in resp
+    assert 'User foo-val1 bar-none' in resp
+    assert 'User foo-val2 bar-val1' in resp
+    assert 'User foo-val1 bar-val2' in resp
+    assert 'User foo-none bar-val2' in resp
+
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'foo': 'val1'})
+    assert 'User none' not in resp
+    assert 'User empty' not in resp
+    assert 'User foo-val1 bar-none' in resp
+    assert 'User foo-val2 bar-val1' not in resp
+    assert 'User foo-val1 bar-val2' in resp
+    assert 'User foo-none bar-val2' not in resp
+
+    resp = app.get(
+        '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'foo': 'val1', 'bar': 'val2'}
+    )
+    assert 'User none' not in resp
+    assert 'User empty' not in resp
+    assert 'User foo-val1 bar-none' not in resp
+    assert 'User foo-val2 bar-val1' not in resp
+    assert 'User foo-val1 bar-val2' in resp
+    assert 'User foo-none bar-val2' not in resp
+
+    resp = app.get(
+        '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'foo': 'val2', 'bar': 'val2'}
+    )
+    assert 'User none' not in resp
+    assert 'User empty' not in resp
+    assert 'User foo-val1 bar-none' not in resp
+    assert 'User foo-val2 bar-val1' not in resp
+    assert 'User foo-val1 bar-val2' not in resp
+    assert 'User foo-none bar-val2' not in resp
+
+
 def test_event_check_booking(app, admin_user):
     group = AbsenceReasonGroup.objects.create(label='Foo bar')
     agenda = Agenda.objects.create(label='Events', kind='events')
-- 
2.30.2

