From ab60ad71aab4e4e9263c23fd8b84744705ea332f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 19 Nov 2020 17:21:00 +0100
Subject: [PATCH 2/2] manager: reduce querysets on agenda settings page
 (#48624)

---
 chrono/agendas/models.py | 13 +++++--------
 chrono/manager/views.py  | 18 +++++++++++++++++-
 tests/test_agendas.py    | 17 +++++++++++++++++
 tests/test_manager.py    |  2 +-
 4 files changed, 40 insertions(+), 10 deletions(-)

diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 911892d..4821ffc 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -1210,12 +1210,11 @@ class Desk(models.Model):
         return new_desk
 
     def get_exceptions_within_two_weeks(self):
-        # FIXME : optimize DB queries, on the settings view this is called once per desk
+        # prefetched_exceptions contains desks exceptions + unavailability_calendars exceptions
         # default ordering: start_datetime
         in_two_weeks = make_aware(datetime.datetime.today() + datetime.timedelta(days=14))
         exceptions = []
-        queryset = TimePeriodException.objects.filter(Q(desk=self) | Q(unavailability_calendar__desks=self))
-        for exception in queryset.all():
+        for exception in self.prefetched_exceptions:
             if exception.end_datetime < now():
                 # exception ends in the past, skip it
                 continue
@@ -1228,7 +1227,7 @@ class Desk(models.Model):
         if exceptions:
             return exceptions
         # if none found within the 2 coming weeks, return the next one
-        for exception in queryset.all():
+        for exception in self.prefetched_exceptions:
             if exception.start_datetime < now():
                 # exception starts in the past, skip it
                 continue
@@ -1238,9 +1237,7 @@ class Desk(models.Model):
 
     def are_all_exceptions_displayed(self):
         in_two_weeks = self.get_exceptions_within_two_weeks()
-        return TimePeriodException.objects.filter(
-            Q(desk=self) | Q(unavailability_calendar__desks=self)
-        ).count() == len(in_two_weeks)
+        return len(self.prefetched_exceptions) == len(in_two_weeks)
 
     def import_timeperiod_exceptions_from_remote_ics(self, ics_url, source=None):
         try:
@@ -1621,7 +1618,7 @@ class TimePeriodException(models.Model):
 
     @property
     def read_only(self):
-        return self.source and self.source.settings_slug or self.unavailability_calendar
+        return self.source and self.source.settings_slug or self.unavailability_calendar_id
 
     class Meta:
         ordering = ['start_datetime']
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 9350e0c..e146209 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -1346,11 +1346,27 @@ class ManagedTimePeriodExceptionMixin(object):
 class AgendaSettings(ManagedAgendaMixin, DetailView):
     model = Agenda
 
+    def set_agenda(self, **kwargs):
+        self.agenda = get_object_or_404(
+            Agenda.objects.select_related('edit_role', 'view_role'), pk=kwargs.get('pk')
+        )
+
     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',
+                'timeperiod_set', 'unavailability_calendars',
             )
+            all_desks_exceptions = TimePeriodException.objects.filter(
+                Q(desk__in=self.agenda.prefetched_desks)
+                | Q(unavailability_calendar__desks__in=self.agenda.prefetched_desks)
+            ).select_related('source')
+            for desk in self.agenda.prefetched_desks:
+                uc_ids = [uc.pk for uc in desk.unavailability_calendars.all()]
+                desk.prefetched_exceptions = [
+                    e
+                    for e in all_desks_exceptions
+                    if e.desk_id == desk.pk or e.unavailability_calendar_id in uc_ids
+                ]
         return self.agenda
 
     def get_context_data(self, **kwargs):
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index ea5adef..3428a6d 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -6,6 +6,7 @@ import requests
 import smtplib
 
 
+from django.db.models import Q
 from django.contrib.auth.models import Group, User
 from django.core.files.base import ContentFile
 from django.core.management import call_command
@@ -862,10 +863,16 @@ def test_agenda_get_effective_time_periods(db):
 
 
 def test_desk_exceptions_within_two_weeks():
+    def set_prefetched_exceptions(desk):
+        desk.prefetched_exceptions = TimePeriodException.objects.filter(
+            Q(desk=desk) | Q(unavailability_calendar__desks=desk)
+        )
+
     agenda = Agenda.objects.create(label='Agenda', kind='meetings')
     desk = Desk.objects.create(agenda=agenda, label='Desk')
 
     # no exception
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == []
 
     # exception ends in the past
@@ -874,6 +881,7 @@ def test_desk_exceptions_within_two_weeks():
         start_datetime=now() - datetime.timedelta(days=2),
         end_datetime=now() - datetime.timedelta(days=1),
     )
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == []
 
     # exception ends in the future - 14 days
@@ -881,6 +889,7 @@ def test_desk_exceptions_within_two_weeks():
     # but starts in the past
     exception.start_datetime = now() - datetime.timedelta(days=2)
     exception.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception]
 
     # exception ends in the future - 14 days
@@ -888,6 +897,7 @@ def test_desk_exceptions_within_two_weeks():
     # but starts in the future
     exception.start_datetime = now() + datetime.timedelta(days=2)
     exception.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception]
 
     # exception ends in the future + 14 days
@@ -895,6 +905,7 @@ def test_desk_exceptions_within_two_weeks():
     # but starts in the past
     exception.start_datetime = now() - datetime.timedelta(days=2)
     exception.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception]
 
     # create another one, very far way from now
@@ -909,6 +920,7 @@ def test_desk_exceptions_within_two_weeks():
     # but starts in the future - 14 days
     exception.start_datetime = now() + datetime.timedelta(days=2)
     exception.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception]
 
     # exception ends in the future + 14 days
@@ -916,12 +928,14 @@ def test_desk_exceptions_within_two_weeks():
     # but starts in the future + 14 days
     exception.start_datetime = now() + datetime.timedelta(days=21)
     exception.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception]
 
     # exception in the past
     exception.end_datetime = now() - datetime.timedelta(days=20)
     exception.start_datetime = now() - datetime.timedelta(days=21)
     exception.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception2]
 
     # check ordering of the queryset: exception is after exception2
@@ -931,6 +945,7 @@ def test_desk_exceptions_within_two_weeks():
     exception2.start_datetime = now() + datetime.timedelta(days=5)
     exception2.end_datetime = now() + datetime.timedelta(days=6)
     exception2.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception2, exception]
 
     # add an exception from unavailability calendar
@@ -941,12 +956,14 @@ def test_desk_exceptions_within_two_weeks():
         end_datetime=now() + datetime.timedelta(days=3),
     )
     unavailability_calendar.desks.add(desk)
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception3, exception2, exception]
 
     # change it
     exception3.start_datetime = now() + datetime.timedelta(days=7)
     exception3.end_datetime = now() + datetime.timedelta(days=8)
     exception3.save()
+    set_prefetched_exceptions(desk)
     assert list(desk.get_exceptions_within_two_weeks()) == [exception2, exception3, exception]
 
 
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 28e6307..64ab7f4 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -1042,7 +1042,7 @@ def test_options_meetings_agenda_num_queries(app, admin_user, managers_group):
     app = login(app)
     with CaptureQueriesContext(connection) as ctx:
         app.get('/manage/agendas/%s/settings' % agenda.pk)
-        assert len(ctx.captured_queries) == 63
+        assert len(ctx.captured_queries) == 12
 
 
 def test_agenda_resources(app, admin_user):
-- 
2.28.0

