From c99bcf92e718dcb5a5b29c39c20888f9bfb20a55 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Sun, 20 Mar 2022 21:34:46 +0100
Subject: [PATCH] Allow the owner of a parent group to be able to perform the
 same operations on the related object as the owners of all other groups the
 object is a member of

---
 CHANGELOG.rst                                |  6 +++
 aleksis/apps/alsijil/forms.py                | 44 +++++++++++++---
 aleksis/apps/alsijil/preferences.py          | 11 ++++
 aleksis/apps/alsijil/rules.py                | 53 +++++++++++++++++++-
 aleksis/apps/alsijil/util/alsijil_helpers.py | 24 +++++++--
 aleksis/apps/alsijil/util/predicates.py      | 26 ++++++++++
 aleksis/apps/alsijil/views.py                | 27 ++++++++--
 7 files changed, 178 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e1b242414..62b0fc17c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,12 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Changed
+~~~~~~~
+
+* Owners of one of the parent groups of a object can now have the same rights on it
+as a group owner (can be toggled with a preference).
+
 `2.0.1`_ - 2022-02-12
 ---------------------
 
diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 3549c6088..39ddbebf1 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -111,13 +111,21 @@ class SelectForm(forms.Form):
         if not check_global_permission(self.request.user, "alsijil.view_week"):
             # 1) All groups the user is allowed to see the week view by object permissions
             # 2) All groups the user is a member of an owner of
+            # 3) If the corresponding preference is turned on:
+            # All groups that have a parent group the user is an owner of
             group_qs = (
                 group_qs.filter(
                     pk__in=get_objects_for_user(
                         self.request.user, "core.view_week_class_register_group", Group
                     ).values_list("pk", flat=True)
                 )
-            ).union(group_qs.filter(Q(members=person) | Q(owners=person)))
+            ).union(
+                group_qs.filter(
+                    Q(members=person) | Q(owners=person) | Q(parent_groups__owners=person)
+                    if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]
+                    else Q(members=person) | Q(owners=person)
+                )
+            )
 
         # Flatten query by filtering groups by pk
         self.fields["group"].queryset = Group.objects.filter(
@@ -130,9 +138,18 @@ class SelectForm(forms.Form):
 
         # Filter selectable teachers by permissions
         if not check_global_permission(self.request.user, "alsijil.view_week"):
-            # If the user hasn't the global permission,
-            # the user is only allowed to see his own person
-            teacher_qs = teacher_qs.filter(pk=person.pk)
+            # If the user hasn't got the global permission and the inherit privileges preference is
+            # turned off, the user is only allowed to see his own person. Otherwise, the user
+            # is allowed to see all persons that teach lessons that the given groups attend.
+            if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]:
+                teacher_pks = []
+                for group in group_qs:
+                    for lesson in group.lessons.all():
+                        for teacher in lesson.teachers.all():
+                            teacher_pks.append(teacher.pk)
+                teacher_qs = teacher_qs.filter(pk__in=teacher_pks)
+            else:
+                teacher_qs = teacher_qs.filter(pk=person.pk)
 
         self.fields["teacher"].queryset = teacher_qs
 
@@ -261,16 +278,31 @@ class AssignGroupRoleForm(forms.ModelForm):
             if get_site_preferences()["alsijil__group_owners_can_assign_roles_to_parents"]:
                 persons = persons.filter(
                     Q(member_of__owners=self.request.user.person)
+                    | Q(member_of__parent_groups__owners=self.request.user.person)
+                    | Q(children__member_of__owners=self.request.user.person)
+                    | Q(children__member_of__parent_groups__owners=self.request.user.person)
+                    if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]
+                    else Q(member_of__owners=self.request.user.person)
                     | Q(children__member_of__owners=self.request.user.person)
                 )
             else:
-                persons = persons.filter(member_of__owners=self.request.user.person)
+                persons = persons.filter(
+                    Q(member_of__owners=self.request.user.person)
+                    | Q(member_of__parent_groups__owners=self.request.user.person)
+                    if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]
+                    else Q(member_of__owners=self.request.user.person)
+                )
             self.fields["person"].queryset = persons.distinct()
 
             if "groups" not in initial:
                 groups = (
                     Group.objects.for_current_school_term_or_all()
-                    .filter(owners=self.request.user.person)
+                    .filter(
+                        Q(owners=self.request.user.person)
+                        | Q(parent_groups__owners=self.request.user.person)
+                        if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]
+                        else Q(owners=self.request.user.person)
+                    )
                     .distinct()
                 )
                 self.fields["groups"].queryset = groups
diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py
index c56332115..5e1a29131 100644
--- a/aleksis/apps/alsijil/preferences.py
+++ b/aleksis/apps/alsijil/preferences.py
@@ -35,6 +35,17 @@ class RegisterAbsenceAsPrimaryGroupOwner(BooleanPreference):
     )
 
 
+@site_preferences_registry.register
+class InheritPrivilegesFromParentGroup(BooleanPreference):
+    section = alsijil
+    name = "inherit_privileges_from_parent_group"
+    default = True
+    verbose_name = _(
+        "Grant the owner of a parent group the same privileges "
+        "as the owners of the respective child groups"
+    )
+
+
 @site_preferences_registry.register
 class EditLessonDocumentationAsOriginalTeacher(BooleanPreference):
     section = alsijil
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index 23686bf17..40856e27e 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -24,7 +24,9 @@ from .util.predicates import (
     is_none,
     is_own_personal_note,
     is_owner_of_any_group,
+    is_parent_group_owner,
     is_person_group_owner,
+    is_person_parent_group_owner,
     is_person_primary_group_owner,
     is_personal_note_lesson_original_teacher,
     is_personal_note_lesson_parent_group_owner,
@@ -52,6 +54,10 @@ view_lesson_personal_notes_predicate = view_register_object_predicate & (
     ~is_lesson_participant
     | is_lesson_teacher
     | is_lesson_original_teacher
+    | (
+        is_lesson_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.view_personalnote")
     | has_lesson_group_object_perm("core.view_personalnote_group")
 )
@@ -64,6 +70,10 @@ edit_lesson_personal_note_predicate = view_lesson_personal_notes_predicate & (
         is_lesson_original_teacher
         & is_site_preference_set("alsijil", "edit_lesson_documentation_as_original_teacher")
     )
+    | (
+        is_lesson_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.change_personalnote")
     | has_lesson_group_object_perm("core.edit_personalnote_group")
 )
@@ -87,6 +97,10 @@ edit_personal_note_predicate = view_personal_note_predicate & (
         is_personal_note_lesson_original_teacher
         | ~is_site_preference_set("alsijil", "edit_lesson_documentation_as_original_teacher")
     )
+    | (
+        is_personal_note_lesson_parent_group_owner
+        | is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.view_personalnote")
     | has_personal_note_group_perm("core.edit_personalnote_group")
 )
@@ -103,6 +117,10 @@ edit_lesson_documentation_predicate = view_register_object_predicate & (
         is_lesson_original_teacher
         & is_site_preference_set("alsijil", "edit_lesson_documentation_as_original_teacher")
     )
+    | (
+        is_lesson_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.change_lessondocumentation")
     | has_lesson_group_object_perm("core.edit_lessondocumentation_group")
 )
@@ -113,6 +131,10 @@ view_week_predicate = has_person & (
     is_current_person
     | is_group_member
     | is_group_owner
+    | (
+        is_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.view_week")
     | has_object_perm("core.view_week_class_register_group")
 )
@@ -125,6 +147,10 @@ add_perm("alsijil.view_week_menu_rule", has_person)
 view_week_personal_notes_predicate = has_person & (
     (is_current_person & is_teacher)
     | is_group_owner
+    | (
+        is_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.view_personalnote")
     | has_object_perm("core.view_personalnote_group")
 )
@@ -145,6 +171,10 @@ add_perm("alsijil.register_absence_rule", register_absence_predicate)
 # View full register for group
 view_full_register_predicate = has_person & (
     is_group_owner
+    | (
+        is_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.view_full_register")
     | has_object_perm("core.view_full_register_group")
 )
@@ -161,6 +191,10 @@ add_perm("alsijil.view_my_groups_rule", view_my_groups_predicate)
 # View students list
 view_students_list_predicate = view_my_groups_predicate & (
     is_group_owner
+    | (
+        is_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.view_personalnote")
     | has_object_perm("core.view_personalnote_group")
 )
@@ -170,6 +204,10 @@ add_perm("alsijil.view_students_list_rule", view_students_list_predicate)
 view_person_overview_predicate = has_person & (
     (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes"))
     | is_person_group_owner
+    | (
+        is_person_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
 )
 add_perm("alsijil.view_person_overview_rule", view_person_overview_predicate)
 
@@ -181,6 +219,10 @@ add_perm("alsijil.view_person_overview_menu_rule", view_person_overview_menu_pre
 view_person_overview_personal_notes_predicate = view_person_overview_predicate & (
     (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes"))
     | is_person_primary_group_owner
+    | (
+        is_person_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.view_personalnote")
     | has_person_group_object_perm("core.view_personalnote_group")
 )
@@ -263,6 +305,10 @@ add_perm("alsijil.delete_grouprole_rule", delete_group_role_predicate)
 
 view_assigned_group_roles_predicate = has_person & (
     is_group_owner
+    | (
+        is_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
     | has_global_perm("alsijil.assign_grouprole")
     | has_object_perm("core.assign_grouprole")
 )
@@ -280,7 +326,12 @@ add_perm(
 )
 
 assign_group_role_person_predicate = has_person & (
-    is_person_group_owner | has_global_perm("alsijil.assign_grouprole")
+    is_person_group_owner
+    | (
+        is_person_parent_group_owner
+        & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group")
+    )
+    | has_global_perm("alsijil.assign_grouprole")
 )
 add_perm("alsijil.assign_grouprole_to_person_rule", assign_group_role_person_predicate)
 
diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py
index 640431a90..0274100f0 100644
--- a/aleksis/apps/alsijil/util/alsijil_helpers.py
+++ b/aleksis/apps/alsijil/util/alsijil_helpers.py
@@ -15,6 +15,8 @@ from aleksis.apps.alsijil.forms import FilterRegisterObjectForm
 from aleksis.apps.alsijil.models import LessonDocumentation
 from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod
 from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
+from aleksis.core.models import Group
+from aleksis.core.util.core_helpers import get_site_preferences
 
 
 def get_register_object_by_pk(
@@ -187,7 +189,19 @@ def _generate_dicts_for_lesson_periods(
     weeks = CalendarWeek.weeks_within(date_start, date_end)
 
     register_objects = []
+    inherit_privileges_preference = get_site_preferences()[
+        "alsijil__inherit_privileges_from_parent_group"
+    ]
     for lesson_period in lesson_periods:
+        parent_group_owned_by_person = (
+            filter_dict.get("person")
+            .owner_of.intersection(
+                Group.objects.filter(
+                    child_groups__in=Group.objects.filter(lessons__lesson_periods=lesson_period)
+                )
+            )
+            .exists()
+        )
         for week in weeks:
             day = week[lesson_period.period.weekday]
 
@@ -205,10 +219,14 @@ def _generate_dicts_for_lesson_periods(
             ):
                 sub = lesson_period.get_substitution()
 
-                # Skip lesson period if the person isn't a teacher
-                # or substitution teacher of this lesson period
+                # Skip lesson period if the person isn't a teacher,
+                # substitution teacher or, when the corresponding
+                # preference is switched on, owner of a parent group
+                # of this lesson period
                 if filter_dict.get("person") and (
-                    filter_dict.get("person") not in lesson_period.lesson.teachers.all() and not sub
+                    filter_dict.get("person") not in lesson_period.lesson.teachers.all()
+                    and not sub
+                    and not (inherit_privileges_preference and parent_group_owned_by_person)
                 ):
                     continue
 
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
index 1759a5446..6f8cab752 100644
--- a/aleksis/apps/alsijil/util/predicates.py
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -125,6 +125,22 @@ def is_person_primary_group_owner(user: User, obj: Person) -> bool:
     return False
 
 
+@predicate
+def is_person_parent_group_owner(user: User, obj: Person) -> bool:
+    """
+    Predicate for parent group owners of a person.
+
+    Checks whether the person linked to the user is the owner of
+    any parent groups of any groups of the given person.
+    """
+    if obj:
+        for group in use_prefetched(obj, "member_of"):
+            for parent_group in use_prefetched(group, "parent_groups"):
+                if user.person in use_prefetched(parent_group, "owners"):
+                    return True
+    return False
+
+
 def has_person_group_object_perm(perm: str):
     """Predicate builder for permissions on a set of member groups.
 
@@ -206,6 +222,16 @@ def is_own_personal_note(user: User, obj: PersonalNote) -> bool:
     return False
 
 
+@predicate
+def is_parent_group_owner(user: User, obj: Group) -> bool:
+    """Predicate which checks whether the user is the owner of any parent group of the group."""
+    if hasattr(obj, "parent_groups"):
+        for parent_group in obj.parent_groups.all():
+            if user.person in list(parent_group.owners.all()):
+                return True
+    return False
+
+
 @predicate
 def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool:
     """Predicate for teachers of a register object linked to a personal note.
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 2d8968207..a4ba938d2 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -350,9 +350,30 @@ def week_view(
 
     elif hasattr(request, "user") and hasattr(request.user, "person"):
         if request.user.person.lessons_as_teacher.exists():
-            lesson_periods = lesson_periods.filter_teacher(request.user.person)
-            events = events.filter_teacher(request.user.person)
-            extra_lessons = extra_lessons.filter_teacher(request.user.person)
+            inherit_privileges_preference = get_site_preferences()[
+                "alsijil__inherit_privileges_from_parent_group"
+            ]
+            lesson_periods = (
+                lesson_periods.filter_teacher(request.user.person).union(
+                    lesson_periods.filter_groups(request.user.person.owner_of.all())
+                )
+                if inherit_privileges_preference
+                else lesson_periods.filter_teacher(request.user.person)
+            )
+            events = (
+                events.filter_teacher(request.user.person).union(
+                    events.filter_groups(request.user.person.owner_of.all())
+                )
+                if inherit_privileges_preference
+                else events.filter_teacher(request.user.person)
+            )
+            extra_lessons = (
+                extra_lessons.filter_teacher(request.user.person).union(
+                    extra_lessons.filter_groups(request.user.person.owner_of.all())
+                )
+                if inherit_privileges_preference
+                else extra_lessons.filter_teacher(request.user.person)
+            )
 
             type_ = TimetableType.TEACHER
         else:
-- 
GitLab