diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7151f1d9b7d918f6cf56be6406f2924137694fec..da3074ee02f31ac95dc7b400339d45331b39cadb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,8 @@ Unreleased Added ~~~~~ +* 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). * Integrate seating plans in lesson overview * Add option to set LessonDocumentation data for all lessons in one week at once. * Excuse types can now be marked as `Count as absent`, which they are per default. If not, they aren't counted in the overviews. diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index 6f4e65b2022192c3cf5c0a7c4b44cc3e1731919e..e23d8c47b344e2e7aa2b7a67ce344c42e4fa205d 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -135,13 +135,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( @@ -154,9 +162,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 their 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 @@ -294,7 +311,12 @@ class AssignGroupRoleForm(forms.ModelForm): 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/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index d18d66867ddfa2bf4c9fe763842b1e3401d6d24e..5b0e5ac4b1337dfdc44c97470c36cd954fc7b915 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -11,6 +11,7 @@ from calendarweek import CalendarWeek from aleksis.apps.alsijil.managers import PersonalNoteQuerySet from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.core.models import Group, Person +from aleksis.core.util.core_helpers import get_site_preferences from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote @@ -385,8 +386,16 @@ def get_groups_with_lessons(cls: Group): def get_owner_groups_with_lessons(self: Person): """Get all groups the person is an owner of and which have related lessons. - Groups which have child groups with related lessons are also included. + Groups which have child groups with related lessons are also included, as well as all + child groups of the groups owned by the person with related lessons if the + inherit_privileges_from_parent_group preference is turned on. """ + if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]: + return ( + Group.get_groups_with_lessons() + .filter(Q(owners=self) | Q(parent_groups__owners=self)) + .distinct() + ) return Group.get_groups_with_lessons().filter(owners=self).distinct() diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py index 35d0984effae2264de0e53844682b09eb60ac3f8..2fd34fa7fc4f802a52ead1d2d0291789be4fb9ce 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 23686bf17d9e2f54a95bc26c672cc4a046ba0131..b2640b8cfbf2eb8a1874926ee5a5bce2809ef1dd 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -24,6 +24,7 @@ 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_primary_group_owner, is_personal_note_lesson_original_teacher, @@ -52,6 +53,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 +69,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 +96,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 +116,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 +130,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 +146,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") ) @@ -133,7 +158,7 @@ add_perm("alsijil.view_week_personalnote_rule", view_week_personal_notes_predica # Register absence register_absence_predicate = has_person & ( ( - is_person_primary_group_owner + is_person_group_owner & is_site_preference_set("alsijil", "register_absence_as_primary_group_owner") ) | has_global_perm("alsijil.register_absence") @@ -145,6 +170,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 +190,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") ) @@ -263,6 +296,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") ) diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py index 640431a902c147d6322d485ba666fdb244635155..8e203e8f61f023af2c812efe0f21cf9a34511c60 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,16 @@ 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 = inherit_privileges_preference and ( + Group.objects.filter( + child_groups__in=Group.objects.filter(lessons__lesson_periods=lesson_period), + owners=filter_dict.get("person"), + ).exists() + ) for week in weeks: day = week[lesson_period.period.weekday] @@ -205,10 +216,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 parent_group_owned_by_person ): continue diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py index 1759a5446d204d61c87f5bd1cbe9e3e031d95be2..d66ebb41d052e17dac94087526997df71824c2cc 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -206,6 +206,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 use_prefetched(obj, "parent_groups"): + if user.person in use_prefetched(parent_group, "owners"): + 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. @@ -247,11 +257,15 @@ def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) -> Checks whether the person linked to the user is the owner of any parent groups of any groups of the given LessonPeriod lesson of the given PersonalNote. + If so, also checks whether the person linked to the personal note actually is a member of this + parent group. """ if hasattr(obj, "register_object"): for group in obj.register_object.get_groups().all(): for parent_group in group.parent_groups.all(): - if user.person in list(parent_group.owners.all()): + if user.person in use_prefetched( + parent_group, "owners" + ) and obj.person in use_prefetched(parent_group, "members"): return True return False diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 8bbdd98d592699dcccdb805ab5555159389e8f72..c1ef90f6998d6426239141aa0dc7e40495303836 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -240,7 +240,9 @@ def register_object( else: persons = Person.objects.all() - persons_qs = register_object.get_personal_notes(persons, wanted_week) + persons_qs = register_object.get_personal_notes(persons, wanted_week).filter( + person__member_of__in=request.user.person.owner_of.all() + ) # Annotate group roles if show_group_roles: @@ -368,9 +370,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: @@ -453,9 +476,13 @@ def week_view( if not request.user.has_perm("alsijil.view_week_personalnote_rule", instance): persons_qs = persons_qs.filter(pk=request.user.person.pk) elif group: - persons_qs = persons_qs.filter(member_of=group) + persons_qs = persons_qs.filter(member_of=group).filter( + member_of__in=request.user.person.owner_of.all() + ) else: - persons_qs = persons_qs.filter(member_of__in=groups) + persons_qs = persons_qs.filter(member_of__in=groups).filter( + member_of__in=request.user.person.owner_of.all() + ) # Prefetch object permissions for persons and groups the persons are members of # because the object permissions are checked for both persons and groups @@ -770,7 +797,7 @@ def my_students(request: HttpRequest) -> HttpResponse: "primary_group__owners", Prefetch("member_of", queryset=relevant_groups, to_attr="member_of_prefetched"), ) - ) + ).filter(member_of__in=request.user.person.owner_of.all()) persons_for_group = [] for person in persons: person.set_object_permission_checker(checker) @@ -803,7 +830,11 @@ class StudentsList(PermissionRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["group"] = self.object - context["persons"] = self.object.generate_person_list_with_class_register_statistics() + context[ + "persons" + ] = self.object.generate_person_list_with_class_register_statistics().filter( + member_of__in=self.request.user.person.owner_of.all() + ) context["extra_marks"] = ExtraMark.objects.all() context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True) context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False) diff --git a/docs/admin/40_preferences.rst b/docs/admin/40_preferences.rst index b6e5ccf08856bea311776d34c4abe239f0679d81..d7252147276e289883c93e050230bdb866acf02e 100644 --- a/docs/admin/40_preferences.rst +++ b/docs/admin/40_preferences.rst @@ -13,6 +13,9 @@ following preferences: * **Allow primary group owners to register future absences for students in their groups**: This allows owners of the student's primary group (e. g. the class) to register future absences like doctor's appointments or family celebrations. +* **Grant the owner of a parent group the same privileges as the owners of the respective child groups**: + The owner of a group can perform all operations on child groups and related objects an owner of + the respected child groups is allowed to (e. g. editing the lesson documentation). * **Allow original teachers to edit their lessons although they are substituted:** In the case of substitute teaching, absent teachers can be given write-in privileges for the lesson. * **Carry over data from first lesson period to the following lesson periods in lessons over multiple periods:**