From 8c0b8f680d4451591e38b53df30595c1e50e205d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 2 Jun 2020 10:07:31 +0200
Subject: [PATCH 2/2] agendas: reduce querysets on settings view (#43516)

---
 chrono/agendas/models.py                      | 31 +++++++++++++------
 .../manager_meetings_agenda_settings.html     | 12 ++++---
 chrono/manager/views.py                       |  4 +++
 tests/test_manager.py                         |  2 +-
 4 files changed, 33 insertions(+), 16 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 9a879fb..e7823e4 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -929,23 +929,34 @@ class Desk(models.Model):
         return new_desk
 
     def get_exceptions_within_two_weeks(self):
+        # timeperiodexception_set.all() is prefetched, do not filter the querysets
         in_two_weeks = make_aware(datetime.datetime.today() + datetime.timedelta(days=14))
-        exceptions = self.timeperiodexception_set.filter(end_datetime__gte=now()).filter(
-            Q(end_datetime__lte=in_two_weeks) | Q(start_datetime__lt=now())
-        )
-        if exceptions.exists():
+        exceptions = []
+        for exception in self.timeperiodexception_set.all():
+            if exception.end_datetime < now():
+                # exception ends in the past, skip it
+                continue
+            if exception.end_datetime <= in_two_weeks:
+                # ends in less than 2 weeks
+                exceptions.append(exception)
+            elif exception.start_datetime < now():
+                # has already started
+                exceptions.append(exception)
+        if exceptions:
             return exceptions
         # if none found within the 2 coming weeks, return the next one
-        next_exception = (
-            self.timeperiodexception_set.filter(start_datetime__gte=now()).order_by('start_datetime').first()
-        )
-        if next_exception:
-            return [next_exception]
+        for exception in self.timeperiodexception_set.all():
+            if exception.start_datetime < now():
+                # exception starts in the past, skip it
+                continue
+            # returns the first exception found
+            return [exception]
         return []
 
     def are_all_exceptions_displayed(self):
+        # timeperiod_set.all() is prefetched, do not filter the queryset
         in_two_weeks = self.get_exceptions_within_two_weeks()
-        return self.timeperiodexception_set.count() == len(in_two_weeks)
+        return len(self.timeperiodexception_set.all()) == len(in_two_weeks)
 
     def import_timeperiod_exceptions_from_remote_ics(self, ics_url, source=None):
         try:
diff --git a/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html b/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html
index 84cbf81..de786a8 100644
--- a/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html
+++ b/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html
@@ -22,9 +22,10 @@
 <div class="section">
 <h3>{% trans 'Meeting Types' %}</h3>
 <div>
-{% if object.meetingtype_set.count %}
+{% with object.meetingtype_set.all as meetingtypes %}
+{% if meetingtypes %}
   <ul class="objects-list single-links">
-    {% for meeting_type in object.meetingtype_set.all %}
+    {% for meeting_type in meetingtypes %}
     <li><a rel="popup" href="{% url 'chrono-manager-meeting-type-edit' pk=meeting_type.id %}">
         {{meeting_type.label}}
         <span class="duration">({{meeting_type.duration}} {% trans "minutes" %})</span>
@@ -42,15 +43,16 @@
   {% endblocktrans %}
 </div>
 {% endif %}
+{% endwith %}
 </div>
 </div>
 
 <div class="section">
 <h3>{% trans 'Time Periods' %}</h3>
 <div>
-    {% if object.desk_set.count %}
+    {% if object.prefetched_desks %}
     <div class="timeperiods">
-        {% for desk in object.desk_set.all %}
+        {% for desk in object.prefetched_desks %}
         <div class="timeperiod">
             {% url 'chrono-manager-agenda-add-time-period' agenda_pk=object.pk pk=desk.pk as add_time_period_url %}
             <ul class="objects-list single-links">
@@ -93,7 +95,7 @@
     the top right of the page to add a first one.
     {% endblocktrans %}
     </div>
-{% endif %}
+    {% endif %}
 </div>
 </div>
 
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 1f538a4..22c320d 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -723,6 +723,10 @@ class AgendaSettings(ManagedAgendaMixin, DetailView):
     model = Agenda
 
     def get_object(self, *args, **kwargs):
+        if self.agenda.kind == 'meetings':
+            self.agenda.prefetched_desks = self.agenda.desk_set.all().prefetch_related(
+                'timeperiod_set', 'timeperiodexception_set',
+            )
         return self.agenda
 
     def get_context_data(self, **kwargs):
diff --git a/tests/test_manager.py b/tests/test_manager.py
index b2c5af7..367ce76 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -316,7 +316,7 @@ def test_options_meetings_agenda_num_queries(app, admin_user):
     app = login(app)
     with CaptureQueriesContext(connection) as ctx:
         app.get('/manage/agendas/%s/settings' % agenda.pk)
-        assert len(ctx.captured_queries) == 77
+        assert len(ctx.captured_queries) == 7
 
 
 def test_delete_agenda(app, admin_user):
-- 
2.20.1

