From a576c194470a4deefdb625ba84a3270a2d1954e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 27 Jan 2022 16:31:02 +0100
Subject: [PATCH] manager: list subscriptions on event check page (#61069)

---
 chrono/agendas/models.py                      |  20 ++
 chrono/manager/forms.py                       |  21 +-
 .../templates/chrono/manager_event_check.html |  14 +-
 chrono/manager/views.py                       |  79 +++++-
 tests/manager/test_event.py                   | 224 ++++++++++++++++--
 5 files changed, 314 insertions(+), 44 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 9a50d69..7292410 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -2977,3 +2977,23 @@ class Subscription(models.Model):
     extra_data = JSONField(null=True)
     date_start = models.DateField()
     date_end = models.DateField()
+
+    @property
+    def user_name(self):
+        return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
+
+    @property
+    def label(self):
+        return _('Subscription')
+
+    def get_user_block(self):
+        template_vars = Context(settings.TEMPLATE_VARS)
+        template_vars.update(
+            {
+                'booking': self,
+            }
+        )
+        try:
+            return Template(self.agenda.get_booking_user_block_template()).render(template_vars)
+        except (VariableDoesNotExist, TemplateSyntaxError):
+            return
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 22318b3..916d3b4 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -15,7 +15,6 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-import collections
 import csv
 import datetime
 
@@ -43,6 +42,7 @@ from chrono.agendas.models import (
     Event,
     MeetingType,
     Resource,
+    Subscription,
     TimePeriod,
     TimePeriodException,
     TimePeriodExceptionSource,
@@ -324,20 +324,9 @@ class BookingCheckFilterSet(django_filters.FilterSet):
 
     def __init__(self, *args, **kwargs):
         self.agenda = kwargs.pop('agenda')
+        filters = kwargs.pop('filters')
         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(
@@ -350,6 +339,12 @@ class BookingCheckFilterSet(django_filters.FilterSet):
             )
 
 
+class SubscriptionCheckFilterSet(BookingCheckFilterSet):
+    class Meta:
+        model = Subscription
+        fields = []
+
+
 class BookingAbsenceReasonForm(forms.Form):
     reason = forms.ChoiceField(required=False)
 
diff --git a/chrono/manager/templates/chrono/manager_event_check.html b/chrono/manager/templates/chrono/manager_event_check.html
index 6050e3c..4627e47 100644
--- a/chrono/manager/templates/chrono/manager_event_check.html
+++ b/chrono/manager/templates/chrono/manager_event_check.html
@@ -27,7 +27,7 @@
     </form>
     <table class="main check-bookings">
       <tbody>
-      {% if booked and not event.checked %}
+      {% if results and not event.checked %}
         <tr class="booking">
           <td class="booking-actions">
             <form method="post" action="{% url 'chrono-manager-event-checked' pk=agenda.pk event_pk=object.pk %}">
@@ -63,9 +63,17 @@
         </tr>
         {% endif %}
       {% endif %}
-      {% for booking in booked %}
+      {% for result in results %}
         <tr class="booking">
-        {% include "chrono/manager_event_check_booking_fragment.html" %}
+        {% if result.kind == 'booking' %}
+          {% with result as booking %}{% include "chrono/manager_event_check_booking_fragment.html" %}{% endwith %}
+        {% elif result.kind == 'subscription' %}
+          <td class="booking-username main-list">{{ result.get_user_block }}</td>
+          <td class="booking-status">({% trans "Not booked" %})</td>
+          {% if not event.checked or not agenda.disable_check_update %}
+          <td class="booking-actions"></td>
+          {% endif %}
+        {% endif %}
         </tr>
       {% endfor %}
       </tbody>
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 6461624..497783d 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 copy
 import csv
 import datetime
@@ -21,6 +22,7 @@ import itertools
 import json
 import math
 import uuid
+from operator import attrgetter
 
 import requests
 from django.contrib import messages
@@ -102,6 +104,7 @@ from .forms import (
     NewEventForm,
     NewMeetingTypeForm,
     NewTimePeriodExceptionForm,
+    SubscriptionCheckFilterSet,
     TimePeriodAddForm,
     TimePeriodExceptionForm,
     TimePeriodExceptionSourceReplaceForm,
@@ -2049,10 +2052,29 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
         queryset = super().get_queryset()
         return queryset.filter(agenda=self.agenda, start_datetime__date__lte=now().date(), cancelled=False)
 
+    def get_filters(self, booked_queryset, subscription_queryset):
+        agenda_filters = self.agenda.get_booking_check_filters()
+        filters = collections.defaultdict(set)
+        extra_data_from_booked = booked_queryset.filter(extra_data__has_any_keys=agenda_filters).values_list(
+            'extra_data', flat=True
+        )
+        extra_data_from_subscriptions = subscription_queryset.filter(
+            extra_data__has_any_keys=agenda_filters
+        ).values_list('extra_data', flat=True)
+        for extra_data in list(extra_data_from_booked) + list(extra_data_from_subscriptions):
+            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}
+        return filters
+
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
         event = self.object
+
+        # booking base queryset
         booked_qs = event.booking_set.filter(
             cancellation_datetime__isnull=True, in_waiting_list=False, primary_booking__isnull=True
         )
@@ -2060,18 +2082,8 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
             places_count=Value(1)
             + Count('secondary_booking_set', filter=Q(cancellation_datetime__isnull=True))
         )
-        filterset = BookingCheckFilterSet(self.request.GET, queryset=booked_qs, agenda=self.agenda)
-        context['filterset'] = filterset
 
-        # build booking list
-        context['booked'] = filterset.qs.order_by('user_last_name', 'user_first_name')
-        context['booked_without_status'] = any(e.user_was_present is None for e in context['booked'])
-        if context['booked_without_status']:
-            context['absence_form'] = BookingAbsenceReasonForm(agenda=self.agenda)
-        for booking in context['booked']:
-            booking.form = BookingAbsenceReasonForm(
-                agenda=self.agenda, initial={'reason': booking.user_absence_reason}
-            )
+        # waiting list queryset
         waiting_qs = event.booking_set.filter(
             cancellation_datetime__isnull=True, in_waiting_list=True, primary_booking__isnull=True
         ).order_by('user_last_name', 'user_first_name')
@@ -2079,6 +2091,51 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
             places_count=Value(1)
             + Count('secondary_booking_set', filter=Q(cancellation_datetime__isnull=True))
         )
+
+        # subscription base queryset
+        subscription_qs = (
+            self.agenda.subscriptions.filter(
+                date_start__lte=event.start_datetime, date_end__gte=event.start_datetime
+            )
+            # exclude user_external_id from booked_qs and waiting_qs
+            .exclude(user_external_id__in=booked_qs.values('user_external_id')).exclude(
+                user_external_id__in=waiting_qs.values('user_external_id')
+            )
+        )
+
+        # build filters from booked_qs and subscription_qs
+        filters = self.get_filters(booked_queryset=booked_qs, subscription_queryset=subscription_qs)
+        # and filter booked and subscriptions
+        booked_filterset = BookingCheckFilterSet(
+            self.request.GET, queryset=booked_qs, agenda=self.agenda, filters=filters
+        )
+        subscription_filterset = SubscriptionCheckFilterSet(
+            self.request.GET, queryset=subscription_qs, agenda=self.agenda, filters=filters
+        )
+
+        # build results from mixed booked and subscriptions
+        results = []
+        booked_without_status = False
+        for booking in booked_filterset.qs:
+            if booking.user_was_present is None:
+                booked_without_status = True
+            booking.form = BookingAbsenceReasonForm(
+                agenda=self.agenda, initial={'reason': booking.user_absence_reason}
+            )
+            booking.kind = 'booking'
+            results.append(booking)
+        for subscription in subscription_filterset.qs:
+            subscription.kind = 'subscription'
+            results.append(subscription)
+        # sort results
+        results = sorted(results, key=attrgetter('user_last_name', 'user_first_name'))
+
+        # set context
+        context['booked_without_status'] = booked_without_status
+        if context['booked_without_status']:
+            context['absence_form'] = BookingAbsenceReasonForm(agenda=self.agenda)
+        context['filterset'] = booked_filterset
+        context['results'] = results
         context['waiting'] = waiting_qs
 
         return context
diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index 825eac3..594f35c 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -10,7 +10,15 @@ from django.test.utils import CaptureQueriesContext
 from django.utils.timezone import localtime, make_aware, now
 from webtest import Upload
 
-from chrono.agendas.models import AbsenceReason, AbsenceReasonGroup, Agenda, Booking, Desk, Event
+from chrono.agendas.models import (
+    AbsenceReason,
+    AbsenceReasonGroup,
+    Agenda,
+    Booking,
+    Desk,
+    Event,
+    Subscription,
+)
 from tests.utils import login
 
 pytestmark = pytest.mark.django_db
@@ -1244,19 +1252,88 @@ def test_event_check(app, admin_user):
         waiting_list_places=5,
         agenda=agenda,
     )
-    booking1 = Booking.objects.create(event=event, user_first_name='User', user_last_name='42')
-    Booking.objects.create(event=event, user_first_name='User', user_last_name='01')
-    Booking.objects.create(event=event, user_first_name='User', user_last_name='17')
-    Booking.objects.create(event=event, user_first_name='User', user_last_name='35')
-    Booking.objects.create(event=event, user_first_name='User', user_last_name='05')
-    booking6 = Booking.objects.create(event=event, user_first_name='User', user_last_name='Cancelled')
+    booking1 = Booking.objects.create(
+        event=event, user_external_id='user:1', user_first_name='User', user_last_name='42'
+    )
+    Booking.objects.create(
+        event=event, user_external_id='user:2', user_first_name='User', user_last_name='01'
+    )
+    Booking.objects.create(
+        event=event, user_external_id='user:3', user_first_name='User', user_last_name='17'
+    )
+    Booking.objects.create(
+        event=event, user_external_id='user:4', user_first_name='User', user_last_name='35'
+    )
+    Booking.objects.create(
+        event=event, user_external_id='user:5', user_first_name='User', user_last_name='05'
+    )
+    booking6 = Booking.objects.create(
+        event=event, user_external_id='user:6', user_first_name='User', user_last_name='Cancelled'
+    )
     booking6.cancel()
     booking7 = Booking.objects.create(
-        event=event, user_first_name='User', user_last_name='Waiting', in_waiting_list=True
+        event=event,
+        user_external_id='user:7',
+        user_first_name='User',
+        user_last_name='Waiting',
+        in_waiting_list=True,
     )
     booking8 = Booking.objects.create(
-        event=event, user_first_name='User', user_last_name='Secondary', primary_booking=booking1
+        event=event,
+        user_external_id='user:1',
+        user_first_name='User',
+        user_last_name='Secondary',
+        primary_booking=booking1,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:1',
+        user_first_name='Subscription',
+        user_last_name='42',
+        date_start=now(),
+        date_end=now(),
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:8',
+        user_first_name='Subscription',
+        user_last_name='43',
+        date_start=now(),
+        date_end=now(),
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:9',
+        user_first_name='Subscription',
+        user_last_name='14',
+        date_start=now(),
+        date_end=now(),
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:7',
+        user_first_name='Subscription',
+        user_last_name='Waiting',
+        date_start=now(),
+        date_end=now(),
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:42',
+        user_first_name='Subscription',
+        user_last_name='Too soon',
+        date_start=now() - datetime.timedelta(days=1),
+        date_end=now() - datetime.timedelta(days=1),
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='user:42',
+        user_first_name='Subscription',
+        user_last_name='Too late',
+        date_start=now() + datetime.timedelta(days=1),
+        date_end=now() + datetime.timedelta(days=1),
     )
+
     login(app)
 
     # event not in past
@@ -1275,24 +1352,29 @@ def test_event_check(app, admin_user):
     app.get('/manage/agendas/%s/events/%s/check' % (agenda2.pk, event.pk), status=404)
 
     resp = resp.click('Check')
-    assert 'Bookings (6/10)' in resp
-    assert 'Waiting List (1/5)' in resp
     assert (
         resp.text.index('Bookings (6/10)')
         < resp.text.index('User 01')
         < resp.text.index('User 05')
+        < resp.text.index('Subscription 14')
         < resp.text.index('User 17')
         < resp.text.index('User 35')
         < resp.text.index('User 42')
+        < resp.text.index('Subscription 43')
         < resp.text.index('Waiting List (1/5)')
         < resp.text.index('User Waiting')
-    )  # user ordering is not optimal ...
+    )
     assert 'User Cancelled' not in resp
+    assert 'Subscription Waiting' not in resp
+    assert 'Subscription 42' not in resp
+    assert 'Subscription too soon' not in resp
+    assert 'Subscription too late' not in resp
 
     agenda.booking_user_block_template = '{{ booking.user_name }} Foo Bar'
     agenda.save()
     resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
     assert 'User 01 Foo Bar' in resp
+    assert 'Subscription 14 Foo Bar' in resp
     assert 'User Waiting Foo Bar' in resp
 
     # cancelled booking
@@ -1412,35 +1494,113 @@ def test_event_check_filters(app, admin_user):
         places=10,
         agenda=agenda,
     )
-    Booking.objects.create(event=event, user_first_name='User', user_last_name='none')
-    Booking.objects.create(event=event, user_first_name='User', user_last_name='empty', extra_data={})
     Booking.objects.create(
-        event=event, user_first_name='User', user_last_name='foo-val1 bar-none', extra_data={'foo': 'val1'}
+        event=event, user_external_id='user:none', user_first_name='User', user_last_name='none'
+    )
+    Booking.objects.create(
+        event=event,
+        user_external_id='user:empty',
+        user_first_name='User',
+        user_last_name='empty',
+        extra_data={},
+    )
+    Booking.objects.create(
+        event=event,
+        user_external_id='user:1',
+        user_first_name='User',
+        user_last_name='foo-val1 bar-none',
+        extra_data={'foo': 'val1'},
     )
     Booking.objects.create(
         event=event,
+        user_external_id='user:2',
         user_first_name='User',
         user_last_name='foo-val2 bar-val1',
         extra_data={'foo': 'val2', 'bar': 'val1'},
     )
     Booking.objects.create(
         event=event,
+        user_external_id='user:3',
         user_first_name='User',
         user_last_name='foo-val1 bar-val2',
         extra_data={'foo': 'val1', 'bar': 'val2'},
     )
     Booking.objects.create(
-        event=event, user_first_name='User', user_last_name='foo-none bar-val2', extra_data={'bar': 'val2'}
+        event=event,
+        user_external_id='user:4',
+        user_first_name='User',
+        user_last_name='foo-none bar-val2',
+        extra_data={'bar': 'val2'},
+    )
+
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='subscription:none',
+        user_first_name='Subscription',
+        user_last_name='none',
+        date_start=event.start_datetime,
+        date_end=event.start_datetime,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='subscription:empty',
+        user_first_name='Subscription',
+        user_last_name='empty',
+        extra_data={},
+        date_start=event.start_datetime,
+        date_end=event.start_datetime,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='subscription:1',
+        user_first_name='Subscription',
+        user_last_name='foo-val1 bar-none',
+        extra_data={'foo': 'val1'},
+        date_start=event.start_datetime,
+        date_end=event.start_datetime,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='subscription:2',
+        user_first_name='Subscription',
+        user_last_name='foo-val2 bar-val1',
+        extra_data={'foo': 'val2', 'bar': 'val1'},
+        date_start=event.start_datetime,
+        date_end=event.start_datetime,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='subscription:3',
+        user_first_name='Subscription',
+        user_last_name='foo-val1 bar-val2',
+        extra_data={'foo': 'val1', 'bar': 'val2'},
+        date_start=event.start_datetime,
+        date_end=event.start_datetime,
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='subscription:4',
+        user_first_name='Subscription',
+        user_last_name='foo-none bar-val2',
+        extra_data={'bar': 'val2'},
+        date_start=event.start_datetime,
+        date_end=event.start_datetime,
     )
     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-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
+    assert 'Subscription none' in resp
+    assert 'Subscription empty' in resp
+    assert 'Subscription foo-val1 bar-none' in resp
+    assert 'Subscription foo-val2 bar-val1' in resp
+    assert 'Subscription foo-val1 bar-val2' in resp
+    assert 'Subscription foo-none bar-val2' in resp
 
     resp = app.get(
         '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'unknown': 'unknown'}
@@ -1451,6 +1611,12 @@ def test_event_check_filters(app, admin_user):
     assert 'User foo-val2 bar-val1' in resp
     assert 'User foo-val1 bar-val2' in resp
     assert 'User foo-none bar-val2' in resp
+    assert 'Subscription none' in resp
+    assert 'Subscription empty' in resp
+    assert 'Subscription foo-val1 bar-none' in resp
+    assert 'Subscription foo-val2 bar-val1' in resp
+    assert 'Subscription foo-val1 bar-val2' in resp
+    assert 'Subscription 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
@@ -1459,6 +1625,12 @@ def test_event_check_filters(app, admin_user):
     assert 'User foo-val2 bar-val1' in resp
     assert 'User foo-val1 bar-val2' in resp
     assert 'User foo-none bar-val2' in resp
+    assert 'Subscription none' in resp
+    assert 'Subscription empty' in resp
+    assert 'Subscription foo-val1 bar-none' in resp
+    assert 'Subscription foo-val2 bar-val1' in resp
+    assert 'Subscription foo-val1 bar-val2' in resp
+    assert 'Subscription 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
@@ -1467,6 +1639,12 @@ def test_event_check_filters(app, admin_user):
     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
+    assert 'Subscription none' not in resp
+    assert 'Subscription empty' not in resp
+    assert 'Subscription foo-val1 bar-none' in resp
+    assert 'Subscription foo-val2 bar-val1' not in resp
+    assert 'Subscription foo-val1 bar-val2' in resp
+    assert 'Subscription 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'}
@@ -1477,6 +1655,12 @@ def test_event_check_filters(app, admin_user):
     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
+    assert 'Subscription none' not in resp
+    assert 'Subscription empty' not in resp
+    assert 'Subscription foo-val1 bar-none' not in resp
+    assert 'Subscription foo-val2 bar-val1' not in resp
+    assert 'Subscription foo-val1 bar-val2' in resp
+    assert 'Subscription 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'}
@@ -1487,6 +1671,12 @@ def test_event_check_filters(app, admin_user):
     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
+    assert 'Subscription none' not in resp
+    assert 'Subscription empty' not in resp
+    assert 'Subscription foo-val1 bar-none' not in resp
+    assert 'Subscription foo-val2 bar-val1' not in resp
+    assert 'Subscription foo-val1 bar-val2' not in resp
+    assert 'Subscription foo-none bar-val2' not in resp
 
 
 def test_event_check_booking(app, admin_user):
-- 
2.34.1

