From f07d2bb8b22ccf0b53f86bce1ad61ecd614a21e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 4 Oct 2021 18:04:26 +0200
Subject: [PATCH 5/5] api: show_past_events for agendas/datetimes endpoint
 (#56615)

---
 chrono/agendas/models.py    | 36 ++++++++++++++------
 chrono/api/serializers.py   |  1 +
 chrono/api/views.py         | 24 +++++++------
 tests/api/test_datetimes.py | 67 ++++++++++++++++++++++++++++++++++---
 4 files changed, 104 insertions(+), 24 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index b51e182..3a0016e 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -654,6 +654,8 @@ class Agenda(models.Model):
 
         if prefetched_queryset:
             entries = self.prefetched_events
+            # we may have past events
+            entries = [e for e in entries if e.start_datetime >= localtime(now())]
         else:
             # recurring events are never opened
             entries = self.event_set.filter(recurrence_days__isnull=True)
@@ -702,23 +704,29 @@ class Agenda(models.Model):
 
     def get_past_events(
         self,
+        prefetched_queryset=False,
         min_start=None,
         max_start=None,
         user_external_id=None,
     ):
         assert self.kind == 'events'
 
-        # recurring events are never opened
-        entries = self.event_set.filter(recurrence_days__isnull=True)
-        # exclude canceled events except for event recurrences
-        entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False))
-        # we want only past events
-        entries = entries.filter(start_datetime__lt=localtime(now()))
+        if prefetched_queryset:
+            entries = self.prefetched_events
+            # we may have future events
+            entries = [e for e in entries if e.start_datetime < localtime(now())]
+        else:
+            # no recurring events
+            entries = self.event_set.filter(recurrence_days__isnull=True)
+            # exclude canceled events except for event recurrences
+            entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False))
+            # we want only past events
+            entries = entries.filter(start_datetime__lt=localtime(now()))
 
-        if min_start:
+        if min_start and not prefetched_queryset:
             entries = entries.filter(start_datetime__gte=min_start)
 
-        if max_start:
+        if max_start and not prefetched_queryset:
             entries = entries.filter(start_datetime__lt=max_start)
 
         if user_external_id:
@@ -729,6 +737,7 @@ class Agenda(models.Model):
                 entries,
                 min_start,
                 min(max_start or localtime(now()), localtime(now())),
+                prefetched_queryset=prefetched_queryset,
             )
 
         return entries
@@ -870,16 +879,23 @@ class Agenda(models.Model):
             ]
 
     @staticmethod
-    def prefetch_events_and_exceptions(qs, user_external_id=None):
+    def prefetch_events_and_exceptions(
+        qs, user_external_id=None, show_past_events=False, min_start=None, max_start=None
+    ):
         event_queryset = Event.objects.filter(
             Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
             recurrence_days__isnull=True,
             cancelled=False,
-            start_datetime__gte=localtime(now()),
         ).order_by()
 
         if user_external_id:
             event_queryset = Event.annotate_queryset_for_user(event_queryset, user_external_id)
+        if not show_past_events:
+            event_queryset = event_queryset.filter(start_datetime__gte=localtime(now()))
+        if min_start:
+            event_queryset = event_queryset.filter(start_datetime__gte=min_start)
+        if max_start:
+            event_queryset = event_queryset.filter(start_datetime__lt=max_start)
 
         recurring_event_queryset = Event.objects.filter(
             Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py
index 077eaa6..41c9f79 100644
--- a/chrono/api/serializers.py
+++ b/chrono/api/serializers.py
@@ -133,6 +133,7 @@ class MultipleAgendasDatetimesSerializer(DatetimesSerializer):
     agendas = CommaSeparatedStringField(
         required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
     )
+    show_past_events = serializers.BooleanField(default=False)
 
 
 class EventSerializer(serializers.ModelSerializer):
diff --git a/chrono/api/views.py b/chrono/api/views.py
index c45bd21..ed6992a 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -842,27 +842,31 @@ class MultipleAgendasDatetimes(APIView):
             )
         payload = serializer.validated_data
 
-        if 'events' in payload:
-            raise APIError(
-                _('events parameter is not supported'),
-                err_class='events parameter is not supported',
-                http_status=status.HTTP_400_BAD_REQUEST,
-            )
-
         agenda_slugs = payload['agendas']
         agendas = get_objects_from_slugs(agenda_slugs, qs=Agenda.objects.filter(kind='events'))
 
         user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
         disable_booked = bool(payload.get('exclude_user_external_id'))
-        agendas = Agenda.prefetch_events_and_exceptions(agendas, user_external_id=user_external_id)
+        show_past_events = bool(payload.get('show_past_events'))
+        agendas = Agenda.prefetch_events_and_exceptions(
+            agendas,
+            user_external_id=user_external_id,
+            show_past_events=show_past_events,
+            min_start=payload.get('date_start'),
+            max_start=payload.get('date_end'),
+        )
 
         entries = []
         for agenda in agendas:
+            if show_past_events:
+                entries.extend(
+                    agenda.get_past_events(
+                        prefetched_queryset=True,
+                    )
+                )
             entries.extend(
                 agenda.get_open_events(
                     prefetched_queryset=True,
-                    min_start=payload.get('date_start'),
-                    max_start=payload.get('date_end'),
                 )
             )
 
diff --git a/tests/api/test_datetimes.py b/tests/api/test_datetimes.py
index 789b76f..6d38386 100644
--- a/tests/api/test_datetimes.py
+++ b/tests/api/test_datetimes.py
@@ -1462,8 +1462,53 @@ def test_datetimes_multiple_agendas(app):
     resp = app.get('/api/agendas/datetimes/', params={'agendas': 'first-agenda,xxx,yyy'}, status=400)
     assert resp.json['err_desc'] == 'invalid slugs: xxx, yyy'
 
-    # no support for past events
-    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'events': 'past'}, status=400)
+    # it's possible to show past events
+    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
+    assert len(resp.json['data']) == 2
+    assert resp.json['data'][0]['id'] == 'first-agenda@event'
+    assert resp.json['data'][1]['id'] == 'second-agenda@event'
+
+    Event.objects.create(
+        slug='event-in-past',
+        start_datetime=now() - datetime.timedelta(days=5),
+        places=5,
+        agenda=first_agenda,
+    )
+    Event.objects.create(  # not visible in datetimes api
+        slug='recurring-in-past',
+        start_datetime=now() - datetime.timedelta(days=5),
+        recurrence_days=[localtime().weekday()],
+        recurrence_end_date=now() - datetime.timedelta(days=5),
+        places=5,
+        agenda=first_agenda,
+    )
+
+    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
+    assert len(resp.json['data']) == 3
+    assert resp.json['data'][0]['id'] == 'first-agenda@event-in-past'
+    assert resp.json['data'][0]['disabled'] is True
+    assert resp.json['data'][1]['id'] == 'first-agenda@event'
+    assert resp.json['data'][1]['disabled'] is False
+    assert resp.json['data'][2]['id'] == 'second-agenda@event'
+    assert resp.json['data'][2]['disabled'] is False
+
+    date_start = localtime() - datetime.timedelta(days=4)
+    resp = app.get(
+        '/api/agendas/datetimes/',
+        params={'agendas': agenda_slugs, 'date_start': date_start, 'show_past_events': True},
+    )
+    assert len(resp.json['data']) == 2
+    assert resp.json['data'][0]['id'] == 'first-agenda@event'
+    assert resp.json['data'][1]['id'] == 'second-agenda@event'
+
+    date_end = localtime() + datetime.timedelta(days=5, hours=1)
+    resp = app.get(
+        '/api/agendas/datetimes/',
+        params={'agendas': agenda_slugs, 'date_end': date_end, 'show_past_events': True},
+    )
+    assert len(resp.json['data']) == 2
+    assert resp.json['data'][0]['id'] == 'first-agenda@event-in-past'
+    assert resp.json['data'][1]['id'] == 'first-agenda@event'
 
 
 @pytest.mark.freeze_time('2021-05-06 14:00')
@@ -1474,9 +1519,11 @@ def test_datetimes_multiple_agendas_sort(app):
     second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
     Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
     Event.objects.create(label='09-05', start_datetime=now().replace(day=9), places=5, agenda=second_agenda)
+    Event.objects.create(label='04-05', start_datetime=now().replace(day=4), places=5, agenda=second_agenda)
     third_agenda = Agenda.objects.create(label='Third agenda', kind='events')
     Desk.objects.create(agenda=third_agenda, slug='_exceptions_holder')
     Event.objects.create(label='09-05', start_datetime=now().replace(day=9), places=5, agenda=third_agenda)
+    Event.objects.create(label='04-05', start_datetime=now().replace(day=4), places=5, agenda=third_agenda)
 
     # check events are ordered by start_datetime and then by agenda order in querystring
     agenda_slugs = ','.join((first_agenda.slug, third_agenda.slug, second_agenda.slug))
@@ -1486,16 +1533,28 @@ def test_datetimes_multiple_agendas_sort(app):
     assert resp.json['data'][1]['id'] == 'second-agenda@09-05'
     assert resp.json['data'][2]['id'] == 'first-agenda@10-05'
 
+    resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'show_past_events': True})
+    assert len(resp.json['data']) == 5
+    assert resp.json['data'][0]['id'] == 'third-agenda@04-05'
+    assert resp.json['data'][1]['id'] == 'second-agenda@04-05'
+    assert resp.json['data'][2]['id'] == 'third-agenda@09-05'
+    assert resp.json['data'][3]['id'] == 'second-agenda@09-05'
+    assert resp.json['data'][4]['id'] == 'first-agenda@10-05'
+
 
 @pytest.mark.freeze_time('2021-05-06 14:00')
 def test_datetimes_multiple_agendas_queries(app):
     for i in range(10):
         agenda = Agenda.objects.create(label=str(i), kind='events')
         Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
+        Event.objects.create(start_datetime=now() - datetime.timedelta(days=5), places=5, agenda=agenda)
         Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
         Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
 
     with CaptureQueriesContext(connection) as ctx:
-        resp = app.get('/api/agendas/datetimes/', params={'agendas': ','.join(str(i) for i in range(10))})
-        assert len(resp.json['data']) == 20
+        resp = app.get(
+            '/api/agendas/datetimes/',
+            params={'agendas': ','.join(str(i) for i in range(10)), 'show_past_events': True},
+        )
+        assert len(resp.json['data']) == 30
         assert len(ctx.captured_queries) == 7
-- 
2.33.0

