From 0c227f48ee7b10ffe86fdbc52253286a4d11b104 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 31 Aug 2020 14:37:43 +0200
Subject: [PATCH] api: return only agenda with open events (#44294)

---
 chrono/agendas/models.py | 41 ++++++++++++++-----------
 chrono/api/views.py      | 25 ++++++++++++++-
 tests/test_api.py        | 66 ++++++++++++++++++++++++++++++++++++++--
 3 files changed, 112 insertions(+), 20 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index b6d3ec3..66221c1 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -462,29 +462,36 @@ class Agenda(models.Model):
                 for weektime_interval in IntervalSet.simple(*time_period_interval) - closed_hours_by_days:
                     yield SharedTimePeriod.from_weektime_interval(weektime_interval, desks=desks)
 
-    def get_open_events(self):
+    def get_open_events(self, prefetched_queryset=False):
         assert self.kind == 'events'
 
-        entries = self.event_set.all()
-        entries = self.event_set.filter(cancelled=False)
-        # we never want to allow booking for past events.
-        entries = entries.filter(start_datetime__gte=localtime(now()))
-        # exclude non published events
-        entries = entries.filter(
-            Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date())
-        )
-        if self.minimal_booking_delay:
+        if prefetched_queryset:
+            entries = self.prefetched_events
+        else:
+            entries = self.event_set.filter(cancelled=False)
+            # we never want to allow booking for past events.
+            entries = entries.filter(start_datetime__gte=localtime(now()))
+            # exclude non published events
             entries = entries.filter(
-                start_datetime__gte=localtime(
-                    now() + datetime.timedelta(days=self.minimal_booking_delay)
-                ).replace(hour=0, minute=0)
+                Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date())
             )
+
+        if self.minimal_booking_delay:
+            min_start = localtime(now() + datetime.timedelta(days=self.minimal_booking_delay)).replace(
+                hour=0, minute=0
+            )
+            if prefetched_queryset:
+                entries = [e for e in entries if e.start_datetime >= min_start]
+            else:
+                entries = entries.filter(start_datetime__gte=min_start)
         if self.maximal_booking_delay:
-            entries = entries.filter(
-                start_datetime__lt=localtime(
-                    now() + datetime.timedelta(days=self.maximal_booking_delay)
-                ).replace(hour=0, minute=0)
+            max_start = localtime(now() + datetime.timedelta(days=self.maximal_booking_delay)).replace(
+                hour=0, minute=0
             )
+            if prefetched_queryset:
+                entries = [e for e in entries if e.start_datetime < max_start]
+            else:
+                entries = entries.filter(start_datetime__lt=max_start)
         return entries
 
 
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 607689e..345de45 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -21,6 +21,7 @@ import uuid
 
 
 from django.db import transaction
+from django.db.models import Prefetch, Q
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.urls import reverse
@@ -349,11 +350,33 @@ class Agendas(APIView):
 
     def get(self, request, format=None):
         agendas_queryset = Agenda.objects.all().prefetch_related('resources').order_by('label')
+
         if 'q' in request.GET:
             if not request.GET['q']:
                 return Response({'data': []})
             agendas_queryset = agendas_queryset.filter(slug__icontains=request.GET['q'])
-        agendas = [get_agenda_detail(request, agenda) for agenda in agendas_queryset]
+
+        with_open_events = request.GET.get('with_open_events') in ['1', 'true']
+        if with_open_events:
+            # return only events agenda
+            event_queryset = Event.objects.filter(
+                Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
+                cancelled=False,
+                start_datetime__gte=localtime(now()),
+            ).order_by()
+            agendas_queryset = agendas_queryset.filter(kind='events').prefetch_related(
+                Prefetch('event_set', queryset=event_queryset, to_attr='prefetched_events',)
+            )
+
+        agendas = []
+        for agenda in agendas_queryset:
+            if with_open_events and not any(
+                not e.full for e in agenda.get_open_events(prefetched_queryset=True)
+            ):
+                # exclude agendas without open events
+                continue
+            agendas.append(get_agenda_detail(request, agenda))
+
         return Response({'data': agendas})
 
 
diff --git a/tests/test_api.py b/tests/test_api.py
index a208030..280a847 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -129,8 +129,8 @@ def virtual_meetings_agenda(meetings_agenda):
 
 
 def test_agendas_api(app):
-    Agenda.objects.create(label='Foo bar')
-    Agenda.objects.create(label='Foo bar 2')
+    event_agenda = Agenda.objects.create(label='Foo bar')
+    event_agenda2 = Agenda.objects.create(label='Foo bar 2')
     meetings_agenda1 = Agenda.objects.create(label='Foo bar Meeting', kind='meetings')
     Agenda.objects.create(label='Foo bar Meeting 2', kind='meetings')
     resource1 = Resource.objects.create(label='Resource 1', description='Foo bar Resource 1')
@@ -228,6 +228,68 @@ def test_agendas_api(app):
         resp = app.get('/api/agenda/', params={'q': 'MEET'})
         assert len(ctx.captured_queries) == 2
 
+    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
+    assert len(resp.json['data']) == 0
+
+    first_date = localtime(now()).replace(hour=17, minute=0, second=0, microsecond=0)
+    first_date += datetime.timedelta(days=1)
+    event1 = Event.objects.create(
+        start_datetime=(now() + datetime.timedelta(days=5)).replace(hour=10, minute=0),
+        places=20,
+        agenda=event_agenda,
+    )
+    event2 = Event.objects.create(
+        start_datetime=(now() + datetime.timedelta(days=10)).replace(hour=10, minute=0),
+        places=20,
+        agenda=event_agenda,
+    )
+    event3 = Event.objects.create(
+        start_datetime=(now() + datetime.timedelta(days=15)).replace(hour=10, minute=0),
+        places=20,
+        agenda=event_agenda,
+    )
+
+    # all events are free
+    resp = app.get('/api/agenda/', params={'with_open_events': 'true'})
+    assert len(resp.json['data']) == 1
+
+    # one event is full
+    Event.objects.filter(pk=event1.pk).update(full=True)
+    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
+    assert len(resp.json['data']) == 1
+
+    # all events are full
+    Event.objects.update(full=True)
+    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
+    assert len(resp.json['data']) == 0
+
+    # event1 is not full but too soon
+    Event.objects.filter(pk=event1.pk).update(full=False)
+    event_agenda.minimal_booking_delay = 10
+    event_agenda.save()
+    assert list(event_agenda.get_open_events()) == [event2, event3]
+    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
+    assert len(resp.json['data']) == 0
+
+    # event3 is not full but too late
+    Event.objects.filter(pk=event3.pk).update(full=False)
+    event_agenda.maximal_booking_delay = 12
+    event_agenda.save()
+    assert list(event_agenda.get_open_events()) == [event2]
+    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
+    assert len(resp.json['data']) == 0
+
+    # events are not full but not published
+    Event.objects.update(full=False)
+    event_agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=20))
+    assert list(event_agenda.get_open_events()) == []
+    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
+    assert len(resp.json['data']) == 0
+
+    with CaptureQueriesContext(connection) as ctx:
+        resp = app.get('/api/agenda/', params={'with_open_events': '1'})
+        assert len(ctx.captured_queries) == 3
+
 
 def test_agendas_meetingtypes_api(app, some_data, meetings_agenda):
     resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug)
-- 
2.28.0

