From 1618796ee609418180db7f336b478fb6d08caa06 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Fri, 14 May 2021 23:16:08 +0200
Subject: [PATCH] Use custom queries to speed up permission filtering of
 timetables in overview

---
 aleksis/apps/chronos/rules.py                |  9 +--
 aleksis/apps/chronos/util/chronos_helpers.py | 84 ++++++++++++++++++++
 aleksis/apps/chronos/util/predicates.py      |  8 ++
 aleksis/apps/chronos/views.py                | 47 +++--------
 4 files changed, 108 insertions(+), 40 deletions(-)

diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py
index a31b3ca1..aec25bf3 100644
--- a/aleksis/apps/chronos/rules.py
+++ b/aleksis/apps/chronos/rules.py
@@ -1,6 +1,5 @@
 from rules import add_perm
 
-from aleksis.core.models import Group, Person
 from aleksis.core.util.predicates import (
     has_any_object,
     has_global_perm,
@@ -8,14 +7,12 @@ from aleksis.core.util.predicates import (
     has_person,
 )
 
-from .models import LessonSubstitution, Room
-from .util.predicates import has_timetable_perm
+from .models import LessonSubstitution
+from .util.predicates import has_any_timetable_object, has_timetable_perm
 
 # View timetable overview
 view_timetable_overview_predicate = has_person & (
-    has_any_object("chronos.view_timetable", Person)
-    | has_any_object("chronos.view_timetable", Group)
-    | has_any_object("chronos.view_timetable", Room)
+    has_any_timetable_object | has_global_perm("chronos.view_timetable_overview")
 )
 add_perm("chronos.view_timetable_overview", view_timetable_overview_predicate)
 
diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py
index 7f929ca8..0f229be4 100644
--- a/aleksis/apps/chronos/util/chronos_helpers.py
+++ b/aleksis/apps/chronos/util/chronos_helpers.py
@@ -1,9 +1,14 @@
 from typing import Optional
 
+from django.contrib.auth.models import User
+from django.db.models import Count, Q
 from django.http import HttpRequest, HttpResponseNotFound
 from django.shortcuts import get_object_or_404
 
+from guardian.core import ObjectPermissionChecker
+
 from aleksis.core.models import Group, Person
+from aleksis.core.util.predicates import check_global_permission
 
 from ..managers import TimetableType
 from ..models import LessonPeriod, LessonSubstitution, Room
@@ -37,3 +42,82 @@ def get_substitution_by_id(request: HttpRequest, id_: int, week: int):
     return LessonSubstitution.objects.filter(
         week=wanted_week.week, year=wanted_week.year, lesson_period=lesson_period
     ).first()
+
+
+def get_teachers(user: User):
+    checker = ObjectPermissionChecker(user)
+
+    teachers = (
+        Person.objects.annotate(lessons_count=Count("lessons_as_teacher"))
+        .filter(lessons_count__gt=0)
+        .order_by("short_name", "last_name")
+    )
+
+    if not check_global_permission(user, "chronos.view_all_person_timetables"):
+        checker.prefetch_perms(teachers)
+
+        wanted_teachers = set()
+
+        for teacher in teachers:
+            if checker.has_perm("core.view_person_timetable", teacher):
+                wanted_teachers.add(teacher.pk)
+
+        teachers = teachers.filter(Q(pk=user.person.pk) | Q(pk__in=wanted_teachers))
+
+    return teachers
+
+
+def get_classes(user: User):
+    checker = ObjectPermissionChecker(user)
+
+    classes = (
+        Group.objects.for_current_school_term_or_all()
+        .annotate(
+            lessons_count=Count("lessons"), child_lessons_count=Count("child_groups__lessons"),
+        )
+        .filter(
+            Q(lessons_count__gt=0, parent_groups=None)
+            | Q(child_lessons_count__gt=0, parent_groups=None)
+        )
+        .order_by("short_name", "name")
+    )
+
+    if not check_global_permission(user, "chronos.view_all_group_timetables"):
+        checker.prefetch_perms(classes)
+
+        wanted_classes = set()
+
+        for _class in classes:
+            if checker.has_perm("core.view_group_timetable", _class):
+                wanted_classes.add(_class.pk)
+
+        classes = classes.filter(
+            Q(pk__in=wanted_classes) | Q(members=user.person) | Q(pk=user.person.primary_group.pk)
+            if user.person.primary_group
+            else Q() | Q(owners=user.person)
+        )
+
+    return classes
+
+
+def get_rooms(user: User):
+    checker = ObjectPermissionChecker(user)
+
+    rooms = (
+        Room.objects.annotate(lessons_count=Count("lesson_periods"))
+        .filter(lessons_count__gt=0)
+        .order_by("short_name", "name")
+    )
+
+    if not check_global_permission(user, "chronos.view_all_room_timetables"):
+        checker.prefetch_perms(rooms)
+
+        wanted_rooms = set()
+
+        for room in rooms:
+            if checker.has_perm("chronos.view_room_timetable", room):
+                wanted_rooms.add(room.pk)
+
+        rooms = rooms.filter(Q(pk__in=wanted_rooms))
+
+    return rooms
diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py
index e7b3a31d..c07047dd 100644
--- a/aleksis/apps/chronos/util/predicates.py
+++ b/aleksis/apps/chronos/util/predicates.py
@@ -7,6 +7,8 @@ from aleksis.apps.chronos.models import Room
 from aleksis.core.models import Group, Person
 from aleksis.core.util.predicates import has_global_perm, has_object_perm
 
+from .chronos_helpers import get_classes, get_rooms, get_teachers
+
 
 @predicate
 def has_timetable_perm(user: User, obj: Model) -> bool:
@@ -31,3 +33,9 @@ def has_timetable_perm(user: User, obj: Model) -> bool:
         )(user, obj)
     else:
         return False
+
+
+@predicate
+def has_any_timetable_object(user: User) -> bool:
+    """Predicate which checks whether there exists a timetable that the user is allowed to access."""
+    return get_classes(user).exists() or get_rooms(user).exists() or get_teachers(user).exists()
diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py
index 31b0a8f6..42841d13 100644
--- a/aleksis/apps/chronos/views.py
+++ b/aleksis/apps/chronos/views.py
@@ -1,7 +1,7 @@
 from datetime import datetime, timedelta
 from typing import Optional
 
-from django.db.models import Count, Q
+from django.db.models import Q
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
@@ -12,16 +12,22 @@ from django.views.decorators.cache import never_cache
 from django_tables2 import RequestConfig
 from rules.contrib.views import permission_required
 
-from aleksis.core.models import Announcement, Group, Person
+from aleksis.core.models import Announcement, Group
 from aleksis.core.util import messages
-from aleksis.core.util.core_helpers import get_site_preferences, has_person, queryset_rules_filter
+from aleksis.core.util.core_helpers import get_site_preferences, has_person
 
 from .forms import LessonSubstitutionForm
 from .managers import TimetableType
-from .models import Absence, Holiday, LessonPeriod, LessonSubstitution, Room, TimePeriod
+from .models import Absence, Holiday, LessonPeriod, LessonSubstitution, TimePeriod
 from .tables import LessonsTable
 from .util.build import build_substitutions_list, build_timetable, build_weekdays
-from .util.chronos_helpers import get_el_by_pk, get_substitution_by_id
+from .util.chronos_helpers import (
+    get_classes,
+    get_el_by_pk,
+    get_rooms,
+    get_substitution_by_id,
+    get_teachers,
+)
 from .util.date import CalendarWeek, get_weeks_for_year
 from .util.js import date_unix
 
@@ -31,35 +37,8 @@ def all_timetables(request: HttpRequest) -> HttpResponse:
     """View all timetables for persons, groups and rooms."""
     context = {}
 
-    teachers = queryset_rules_filter(
-        request,
-        (
-            Person.objects.annotate(lessons_count=Count("lessons_as_teacher"))
-            .filter(lessons_count__gt=0)
-            .order_by("short_name", "last_name")
-        ),
-        "chronos.view_timetable",
-    )
-    groups = Group.objects.for_current_school_term_or_all().annotate(
-        lessons_count=Count("lessons"), child_lessons_count=Count("child_groups__lessons"),
-    )
-    classes = queryset_rules_filter(
-        request,
-        groups.filter(lessons_count__gt=0, parent_groups=None)
-        | groups.filter(child_lessons_count__gt=0, parent_groups=None).order_by(
-            "short_name", "name"
-        ),
-        "chronos.view_timetable",
-    )
-    rooms = queryset_rules_filter(
-        request,
-        (
-            Room.objects.annotate(lessons_count=Count("lesson_periods"))
-            .filter(lessons_count__gt=0)
-            .order_by("short_name", "name")
-        ),
-        "chronos.view_timetable",
-    )
+    user = request.user
+    teachers, classes, rooms = get_teachers(user), get_classes(user), get_rooms(user)
 
     context["teachers"] = teachers
     context["classes"] = classes
-- 
GitLab