diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index 3632661e3bf3f616c5a06ba4e830b9cbc8d877f8..c39709aa2b1d0161fff0ad4d13d4839b43d7ba06 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -2,15 +2,18 @@ from datetime import datetime from django import forms from django.core.exceptions import ValidationError -from django.db.models import Count +from django.db.models import Count, Q from django.utils.translation import gettext_lazy as _ +from django_global_request.middleware import get_request from django_select2.forms import Select2Widget +from guardian.shortcuts import get_objects_for_user from material import Fieldset, Layout, Row from aleksis.apps.chronos.managers import TimetableType from aleksis.apps.chronos.models import TimePeriod from aleksis.core.models import Group, Person +from aleksis.core.util.predicates import check_global_permission from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote @@ -51,12 +54,7 @@ class SelectForm(forms.Form): queryset=None, label=_("Group"), required=False, widget=Select2Widget, ) teacher = forms.ModelChoiceField( - queryset=Person.objects.annotate( - lessons_count=Count("lessons_as_teacher") - ).filter(lessons_count__gt=0), - label=_("Teacher"), - required=False, - widget=Select2Widget, + queryset=None, label=_("Teacher"), required=False, widget=Select2Widget, ) def clean(self) -> dict: @@ -78,8 +76,40 @@ class SelectForm(forms.Form): return data def __init__(self, *args, **kwargs): + self.request = get_request() super().__init__(*args, **kwargs) - self.fields["group"].queryset = Group.get_groups_with_lessons() + + person = self.request.user.person + + group_qs = Group.get_groups_with_lessons() + + # Filter selectable groups by permissions + 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 + 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))) + + # Flatten query by filtering groups by pk + self.fields["group"].queryset = Group.objects.filter( + pk__in=list(group_qs.values_list("pk", flat=True)) + ) + + teacher_qs = Person.objects.annotate( + lessons_count=Count("lessons_as_teacher") + ).filter(lessons_count__gt=0) + + # 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) + + self.fields["teacher"].queryset = teacher_qs PersonalNoteFormSet = forms.modelformset_factory( @@ -95,11 +125,11 @@ class RegisterAbsenceForm(forms.Form): ) date_start = forms.DateField(label=_("Start date"), initial=datetime.today) date_end = forms.DateField(label=_("End date"), initial=datetime.today) - from_period = forms.ChoiceField(label=_("Start period")) - to_period = forms.ChoiceField(label=_("End period")) person = forms.ModelChoiceField( - label=_("Person"), queryset=Person.objects.all(), widget=Select2Widget + label=_("Person"), queryset=None, widget=Select2Widget ) + from_period = forms.ChoiceField(label=_("Start period")) + to_period = forms.ChoiceField(label=_("End period")) absent = forms.BooleanField(label=_("Absent"), initial=True, required=False) excused = forms.BooleanField(label=_("Excused"), initial=True, required=False) excuse_type = forms.ModelChoiceField( @@ -111,9 +141,41 @@ class RegisterAbsenceForm(forms.Form): remarks = forms.CharField(label=_("Remarks"), max_length=30, required=False) def __init__(self, *args, **kwargs): + self.request = get_request() super().__init__(*args, **kwargs) period_choices = TimePeriod.period_choices + # Filter selectable persons by permissions + if check_global_permission(self.request.user, "alsijil.register_absence"): + # Global permission, user can register absences for all persons + self.fields["person"].queryset = Person.objects.all() + else: + # 1) All persons the user is allowed to register an absence for by object permissions + # 2) All persons the user is the primary group owner + # 3) All persons the user is allowed to register an absence for by object permissions of the person's group + persons_qs = ( + get_objects_for_user( + self.request.user, "core.register_absence_person", Person + ) + .union( + Person.objects.filter( + primary_group__owners=self.request.user.person + ) + ) + .union( + Person.objects.filter( + member_of__in=get_objects_for_user( + self.request.user, "core.register_absence_group", Group + ) + ) + ) + ) + + # Flatten query by getting all pks and filter persons + self.fields["person"].queryset = Person.objects.filter( + pk__in=list(persons_qs.values_list("pk", flat=True)) + ) + self.fields["from_period"].choices = period_choices self.fields["to_period"].choices = period_choices self.fields["from_period"].initial = TimePeriod.period_min diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py index 8b51bb206862364dbed6c57ff9af25d598b48e33..87a1daa3c93fb4985d9ccbf5f6a8afb3e956503a 100644 --- a/aleksis/apps/alsijil/menus.py +++ b/aleksis/apps/alsijil/menus.py @@ -16,49 +16,89 @@ MENUS = { "name": _("Current lesson"), "url": "lesson", "icon": "alarm", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_lesson_menu", + ), + ], }, { "name": _("Current week"), "url": "week_view", "icon": "view_week", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_week_menu", + ), + ], }, { "name": _("My groups"), "url": "my_groups", "icon": "people", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_my_groups", + ), + ], }, { "name": _("My overview"), "url": "overview_me", "icon": "insert_chart", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_person_overview_menu", + ), + ], }, { "name": _("My students"), "url": "my_students", "icon": "people", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_my_students", + ), + ], }, { "name": _("Register absence"), "url": "register_absence", "icon": "rate_review", - "validators": ["menu_generator.validators.is_superuser"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_register_absence", + ), + ], }, { "name": _("Excuse types"), "url": "excuse_types", "icon": "label", - "validators": ["menu_generator.validators.is_superuser"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_excusetypes", + ), + ], }, { "name": _("Extra marks"), "url": "extra_marks", "icon": "label", - "validators": ["menu_generator.validators.is_superuser"], + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_extramarks", + ), + ], }, ], } diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index 847d815c72f1893bbf3a269cff4f273da984c327..b688332997fcc890dd719467c8997cae403758bd 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -3,6 +3,7 @@ from typing import Dict, Optional, Union from django.db.models import Exists, OuterRef, Q, QuerySet from django.db.models.aggregates import Count +from django.utils.translation import gettext as _ import reversion from calendarweek import CalendarWeek @@ -54,16 +55,20 @@ def mark_absent( continue with reversion.create_revision(): - personal_note, created = PersonalNote.objects.update_or_create( - person=self, - lesson_period=lesson_period, - week=wanted_week.week, - year=wanted_week.year, - defaults={ - "absent": absent, - "excused": excused, - "excuse_type": excuse_type, - }, + personal_note, created = ( + PersonalNote.objects.select_related(None) + .prefetch_related(None) + .update_or_create( + person=self, + lesson_period=lesson_period, + week=wanted_week.week, + year=wanted_week.year, + defaults={ + "absent": absent, + "excused": excused, + "excuse_type": excuse_type, + }, + ) ) personal_note.groups_of_person.set(self.member_of.all()) @@ -76,7 +81,7 @@ def mark_absent( @LessonPeriod.method -def get_personal_notes(self, wanted_week: CalendarWeek): +def get_personal_notes(self, persons: QuerySet, wanted_week: CalendarWeek): """Get all personal notes for that lesson in a specified week. Returns all linked `PersonalNote` objects, filtered by the given weeek, @@ -89,7 +94,7 @@ def get_personal_notes(self, wanted_week: CalendarWeek): - Dominik George <dominik.george@teckids.org> """ # Find all persons in the associated groups that do not yet have a personal note for this lesson - missing_persons = Person.objects.annotate( + missing_persons = persons.annotate( no_personal_notes=~Exists( PersonalNote.objects.filter( week=wanted_week.week, @@ -119,11 +124,51 @@ def get_personal_notes(self, wanted_week: CalendarWeek): for personal_note in new_personal_notes: personal_note.groups_of_person.set(personal_note.person.member_of.all()) - return PersonalNote.objects.select_related("person").filter( - lesson_period=self, week=wanted_week.week, year=wanted_week.year + return ( + PersonalNote.objects.filter( + lesson_period=self, + week=wanted_week.week, + year=wanted_week.year, + person__in=persons, + ) + .select_related(None) + .prefetch_related(None) + .select_related("person", "excuse_type") + .prefetch_related("extra_marks") ) +# Dynamically add extra permissions to Group and Person models in core +# Note: requires migrate afterwards +Group.add_permission( + "view_week_class_register_group", + _("Can view week overview of group class register"), +) +Group.add_permission( + "view_lesson_class_register_group", + _("Can view lesson overview of group class register"), +) +Group.add_permission( + "view_personalnote_group", _("Can view all personal notes of a group") +) +Group.add_permission( + "edit_personalnote_group", _("Can edit all personal notes of a group") +) +Group.add_permission( + "view_lessondocumentation_group", _("Can view all lesson documentation of a group") +) +Group.add_permission( + "edit_lessondocumentation_group", _("Can edit all lesson documentation of a group") +) +Group.add_permission("view_full_register_group", _("Can view full register of a group")) +Group.add_permission( + "register_absence_group", _("Can register an absence for all members of a group") +) +Person.add_permission( + "register_absence_person", _("Can register an absence for a person") +) + + @LessonPeriod.method def get_lesson_documentation( self, week: Optional[CalendarWeek] = None diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index ea951fd97a4a7676d775070440a77e8b1b68783e..66f6477d22165c085abd0855502686d505d75c75 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -208,3 +208,13 @@ class ExtraMark(ExtensibleModel): ordering = ["short_name"] verbose_name = _("Extra mark") verbose_name_plural = _("Extra marks") + + +class AlsijilGlobalPermissions(ExtensibleModel): + class Meta: + managed = False + permissions = ( + ("view_week", _("Can view week overview")), + ("register_absence", _("Can register absence")), + ("list_personal_note_filters", _("Can list all personal note filters")), + ) diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py index bcefc075e2ac5025ebaf2b361abe3f2325b16563..d5552c5e03a37d530d1355c99ef56706616e5380 100644 --- a/aleksis/apps/alsijil/preferences.py +++ b/aleksis/apps/alsijil/preferences.py @@ -16,6 +16,24 @@ class BlockPersonalNotesForCancelled(BooleanPreference): verbose_name = _("Block adding personal notes for cancelled lessons") +@site_preferences_registry.register +class ViewOwnPersonalNotes(BooleanPreference): + section = alsijil + name = "view_own_personal_notes" + default = True + verbose_name = _("Allow users to view their own personal notes") + + +@site_preferences_registry.register +class RegisterAbsenceAsPrimaryGroupOwner(BooleanPreference): + section = alsijil + name = "register_absence_as_primary_group_owner" + default = True + verbose_name = _( + "Allow primary group owners to register future absences for students in their groups" + ) + + @site_preferences_registry.register class CarryOverDataToNextPeriods(BooleanPreference): section = alsijil diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py new file mode 100644 index 0000000000000000000000000000000000000000..8ee92ec5eb2f53b472e0b873995a3522d54c6d32 --- /dev/null +++ b/aleksis/apps/alsijil/rules.py @@ -0,0 +1,237 @@ +from rules import add_perm + +from aleksis.core.util.predicates import ( + has_global_perm, + has_object_perm, + has_person, + is_current_person, + is_site_preference_set, +) + +from .util.predicates import ( + has_any_object_absence, + has_lesson_group_object_perm, + has_person_group_object_perm, + has_personal_note_group_perm, + is_group_member, + is_group_owner, + is_lesson_parent_group_owner, + is_lesson_participant, + is_lesson_teacher, + is_none, + is_own_personal_note, + is_person_group_owner, + is_person_primary_group_owner, + is_personal_note_lesson_parent_group_owner, + is_personal_note_lesson_teacher, + is_teacher, +) + +# View lesson +view_lesson_predicate = has_person & ( + has_global_perm("alsijil.view_lesson") + | is_none # View is opened as "Current lesson" + | is_lesson_teacher + | is_lesson_participant + | is_lesson_parent_group_owner + | has_lesson_group_object_perm("core.view_week_class_register_group") +) +add_perm("alsijil.view_lesson", view_lesson_predicate) + +# View lesson in menu +add_perm("alsijil.view_lesson_menu", has_person) + +# View lesson personal notes +view_lesson_personal_notes_predicate = view_lesson_predicate & ( + has_global_perm("alsijil.view_personalnote") + | ~is_lesson_participant + | has_lesson_group_object_perm("core.view_personalnote_group") +) +add_perm("alsijil.view_lesson_personalnote", view_lesson_personal_notes_predicate) + +# Edit personal note +edit_lesson_personal_note_predicate = view_lesson_personal_notes_predicate & ( + has_global_perm("alsijil.change_personalnote") + | ~is_lesson_parent_group_owner + | has_lesson_group_object_perm("core.edit_personalnote_group") +) +add_perm("alsijil.edit_lesson_personalnote", edit_lesson_personal_note_predicate) + +# View personal note +view_personal_note_predicate = has_person & ( + has_global_perm("alsijil.view_personalnote") + | is_personal_note_lesson_teacher + | ( + is_own_personal_note + & is_site_preference_set("alsijil", "view_own_personal_notes") + ) + | is_personal_note_lesson_parent_group_owner + | has_personal_note_group_perm("core.view_personalnote_group") +) +add_perm("alsijil.view_personalnote", view_personal_note_predicate) + +# Edit personal note +edit_personal_note_predicate = view_personal_note_predicate & ( + has_global_perm("alsijil.view_personalnote") + | ~is_own_personal_note + | has_personal_note_group_perm("core.edit_personalnote_group") +) +add_perm("alsijil.edit_personalnote", edit_personal_note_predicate) + +# View lesson documentation +view_lesson_documentation_predicate = view_lesson_predicate +add_perm("alsijil.view_lessondocumentation", view_lesson_documentation_predicate) + +# Edit lesson documentation +edit_lesson_documentation_predicate = view_lesson_predicate & ( + has_global_perm("alsijil.change_lessondocumentation") + | is_lesson_teacher + | has_lesson_group_object_perm("core.edit_lessondocumentation_group") +) +add_perm("alsijil.edit_lessondocumentation", edit_lesson_documentation_predicate) + +# View week overview +view_week_predicate = has_person & ( + has_global_perm("alsijil.view_week") + | is_current_person + | is_group_member + | is_group_owner + | has_object_perm("core.view_week_class_register_group") +) +add_perm("alsijil.view_week", view_week_predicate) + +# View week overview in menu +add_perm("alsijil.view_week_menu", has_person) + +# View week personal notes +view_week_personal_notes_predicate = has_person & ( + has_global_perm("alsijil.view_personalnote") + | has_object_perm("core.view_personalnote_group") + | is_group_owner + | (is_current_person & is_teacher) +) +add_perm("alsijil.view_week_personalnote", view_week_personal_notes_predicate) + +# View register absence page +view_register_absence_predicate = has_person & ( + has_global_perm("alsijil.register_absence") | has_any_object_absence +) +add_perm("alsijil.view_register_absence", view_register_absence_predicate) + +# Register absence +register_absence_predicate = has_person & ( + has_global_perm("alsijil.register_absence") + | has_person_group_object_perm("core.register_absence_group") + | has_object_perm("core.register_absence_person") + | ( + is_person_primary_group_owner + & is_site_preference_set("alsijil", "register_absence_as_primary_group_owner") + ) +) +add_perm("alsijil.register_absence", register_absence_predicate) + +# View full register for group +view_full_register_predicate = has_person & ( + has_global_perm("alsijil.view_full_register") + | has_object_perm("core.view_full_register_group") + | is_group_owner +) +add_perm("alsijil.view_full_register", view_full_register_predicate) + +# View students list +view_my_students_predicate = has_person & is_teacher +add_perm("alsijil.view_my_students", view_my_students_predicate) + +# View groups list +view_my_groups_predicate = has_person & is_teacher +add_perm("alsijil.view_my_groups", view_my_groups_predicate) + +# View person overview +view_person_overview_predicate = has_person & ( + (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes")) + | is_person_group_owner +) +add_perm("alsijil.view_person_overview", view_person_overview_predicate) + +# View person overview +view_person_overview_menu_predicate = has_person +add_perm("alsijil.view_person_overview_menu", view_person_overview_menu_predicate) + +# View person overview personal notes +view_person_overview_personal_notes_predicate = view_person_overview_predicate & ( + has_global_perm("alsijil.view_personalnote") + | has_person_group_object_perm("core.view_personalnote_group") + | is_person_primary_group_owner + | (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes")) +) +add_perm( + "alsijil.view_person_overview_personalnote", + view_person_overview_personal_notes_predicate, +) + +# Edit person overview personal notes +edit_person_overview_personal_notes_predicate = ( + view_person_overview_personal_notes_predicate + & ( + has_global_perm("alsijil.edit_personalnote") + | ~is_current_person + | has_person_group_object_perm("core.edit_personalnote_group") + ) +) +add_perm( + "alsijil.edit_person_overview_personalnote", + edit_person_overview_personal_notes_predicate, +) + +# View person statistics on personal notes +view_person_statistics_personal_notes_predicate = ( + view_person_overview_personal_notes_predicate +) +add_perm( + "alsijil.view_person_statistics_personalnote", + view_person_statistics_personal_notes_predicate, +) + +# View excuse type list +view_excusetypes_predicate = has_person & has_global_perm("alsijil.view_excusetype") +add_perm("alsijil.view_excusetypes", view_excusetypes_predicate) + +# Add excuse type +add_excusetype_predicate = view_excusetypes_predicate & has_global_perm( + "alsijil.add_excusetype" +) +add_perm("alsijil.add_excusetype", add_excusetype_predicate) + +# Edit excuse type +edit_excusetype_predicate = view_excusetypes_predicate & has_global_perm( + "alsijil.change_excusetype" +) +add_perm("alsijil.edit_excusetype", edit_excusetype_predicate) + +# Delete excuse type +delete_excusetype_predicate = view_excusetypes_predicate & has_global_perm( + "alsijil.delete_excusetype" +) +add_perm("alsijil.delete_excusetype", delete_excusetype_predicate) + +# View extra mark list +view_extramarks_predicate = has_person & has_global_perm("alsijil.view_extramark") +add_perm("alsijil.view_extramarks", view_extramarks_predicate) + +# Add extra mark +add_extramark_predicate = view_extramarks_predicate & has_global_perm( + "alsijil.add_extramark" +) +add_perm("alsijil.add_extramark", add_extramark_predicate) + +# Edit extra mark +edit_extramark_predicate = view_extramarks_predicate & has_global_perm( + "alsijil.change_extramark" +) +add_perm("alsijil.edit_extramark", edit_extramark_predicate) + +# Delete extra mark +delete_extramark_predicate = view_extramarks_predicate & has_global_perm( + "alsijil.delete_extramark" +) +add_perm("alsijil.delete_extramark", delete_extramark_predicate) diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py index bd6b47a73f34c5e4343138993ee70162cd19b847..b9a8e68404d6b2672dfbb37d2433e55cad08cf08 100644 --- a/aleksis/apps/alsijil/tables.py +++ b/aleksis/apps/alsijil/tables.py @@ -42,3 +42,9 @@ class ExcuseTypeTable(tables.Table): text=_("Delete"), attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}}, ) + + def before_render(self, request): + if not request.user.has_perm("alsijil.edit_excusetype"): + self.columns.hide("edit") + if not request.user.has_perm("alsijil.delete_excusetype"): + self.columns.hide("delete") diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html index 9fca5de56fc8bd6316a4db3728ae449cd3882cbb..f5a30a7ab3dc5cc9def7d6f5fcfc1011c8385380 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html @@ -1,7 +1,6 @@ {# -*- engine:django -*- #} {% extends "core/base.html" %} -{% load week_helpers material_form_internal %} -{% load material_form i18n static %} +{% load week_helpers material_form_internal material_form i18n static rules %} {% block browser_title %}{% blocktrans %}Lesson{% endblocktrans %}{% endblock %} @@ -29,6 +28,10 @@ {% endblock %} {% block content %} + {% has_perm "alsijil.view_lessondocumentation" user lesson_period as can_view_lesson_documentation %} + {% has_perm "alsijil.edit_lessondocumentation" user lesson_period as can_edit_lesson_documentation %} + {% has_perm "alsijil.edit_lesson_personalnote" user lesson_period as can_edit_lesson_personalnote %} + <div class="row"> <div class="col s12"> {% with prev_lesson=lesson_period.prev %} @@ -50,7 +53,10 @@ </div> <form method="post"> - <p>{% include "core/partials/save_button.html" %}</p> + {% if can_edit_lesson_documentation or can_edit_lesson_personalnote %} + <p>{% include "core/partials/save_button.html" %}</p> + {% endif %} + {% csrf_token %} <div class="row"> @@ -73,7 +79,8 @@ <div class="col s12" id="lesson-documentation"> {% with prev_lesson=lesson_period.prev prev_doc=prev_lesson.get_lesson_documentation %} {% with prev_doc=prev_lesson.get_lesson_documentation absences=prev_lesson.get_absences tardinesses=prev_lesson.get_tardinesses extra_marks=prev_lesson.get_extra_marks %} - {% if prev_doc %} + {% has_perm "alsijil.view_lessondocumentation" user prev_lesson as can_view_prev_lesson_documentation %} + {% if prev_doc and can_view_prev_lesson_documentation %} {% weekday_to_date prev_lesson.week prev_lesson.period.weekday as prev_date %} <div class="card"> @@ -124,7 +131,10 @@ <th>{{ extra_mark.name }}</th> <td> {% for note in notes %} - <span>{{ note.person }}{% if not forloop.last %},{% endif %}</span> + {% has_perm "alsijil.view_personalnote" user note as can_view_personalnote %} + {% if can_view_personalnote %} + <span>{{ note.person }}{% if not forloop.last %},{% endif %}</span> + {% endif %} {% endfor %} </td> </tr> @@ -143,7 +153,36 @@ {% blocktrans %}Lesson documentation{% endblocktrans %} </span> - {% form form=lesson_documentation_form %}{% endform %} + {% if can_edit_lesson_documentation %} + {% form form=lesson_documentation_form %}{% endform %} + {% elif can_view_lesson_documentation %} + <table> + <tr> + <th> + {% trans "Lesson topic" %} + </th> + <td> + {{ lesson_documentation.topic }} + </td> + </tr> + <tr> + <th> + {% trans "Homework" %} + </th> + <td> + {{ lesson_documentation.homework }} + </td> + </tr> + <tr> + <th> + {% trans "Group note" %} + </th> + <td> + {{ lesson_documentation.group_note }} + </td> + </tr> + </table> + {% endif %} </div> </div> </div> @@ -152,10 +191,12 @@ <div class="col s12" id="personal-notes"> <div class="card"> <div class="card-content"> - <span class="card-title"> - {% blocktrans %}Personal notes{% endblocktrans %} - </span> - {% form form=personal_note_formset.management_form %}{% endform %} + <span class="card-title"> + {% blocktrans %}Personal notes{% endblocktrans %} + </span> + {% if can_edit_lesson_personalnote %} + {% form form=personal_note_formset.management_form %}{% endform %} + {% endif %} <table class="striped responsive-table alsijil-table"> <thead> @@ -166,94 +207,104 @@ <th>{% blocktrans %}Excused{% endblocktrans %}</th> <th>{% blocktrans %}Excuse type{% endblocktrans %}</th> <th>{% blocktrans %}Extra marks{% endblocktrans %}</th> - <th>{% blocktrans %}Remarks{% endblocktrans %}</th> </tr> </thead> <tbody> {% for form in personal_note_formset %} - <tr> - {{ form.id }} - <td>{{ form.person_name }}{{ form.person_name.value }}</td> - <td class="center-align"> - <label> - {{ form.absent }} - <span></span> - </label> - </td> - <td> - <div class="input-field"> - {{ form.late }} - <label for="{{ form.absent.id_for_label }}"> - {% trans "Tardiness (in m)" %} + {% if can_edit_lesson_personalnote %} + <tr> + {{ form.id }} + <td>{{ form.person_name }}{{ form.person_name.value }}</td> + <td class="center-align"> + <label> + {{ form.absent }} + <span></span> </label> - </div> - </td> - <td class="center-align"> - <label> - {{ form.excused }} - <span></span> - </label> - </td> - <td> - <div class="input-field"> - {{ form.excuse_type }} - <label for="{{ form.excuse_type.id_for_label }}"> - {% trans "Excuse type" %} + </td> + <td> + <div class="input-field"> + {{ form.late }} + <label for="{{ form.absent.id_for_label }}"> + {% trans "Tardiness (in m)" %} + </label> + </div> + </td> + <td class="center-align"> + <label> + {{ form.excused }} + <span></span> </label> - </div> - </td> - <td> - {% for group, items in form.extra_marks|select_options %} - {% for choice, value, selected in items %} - <label class="{% if selected %} active{% endif %} alsijil-check-box"> - <input type="checkbox" - {% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %} - {% if selected %} checked="checked"{% endif %} - name="{{ form.extra_marks.html_name }}"> - <span>{{ choice }}</span> + </td> + <td> + <div class="input-field"> + {{ form.excuse_type }} + <label for="{{ form.excuse_type.id_for_label }}"> + {% trans "Excuse type" %} </label> + </div> + </td> + <td> + {% for group, items in form.extra_marks|select_options %} + {% for choice, value, selected in items %} + <label class="{% if selected %} active{% endif %} alsijil-check-box"> + <input type="checkbox" + {% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %} + {% if selected %} checked="checked"{% endif %} + name="{{ form.extra_marks.html_name }}"> + <span>{{ choice }}</span> + </label> + {% endfor %} {% endfor %} - {% endfor %} - </td> - <td> - <div class="input-field"> - {{ form.remarks }} - <label for="{{ form.absent.id_for_label }}"> - {% trans "Remarks" %} - </label> - </div> - </td> - <td> - <div class="input-field"> - {{ form.remarks }} - <label for="{{ form.absent.id_for_label }}"> - {% trans "Remarks" %} - </label> - </div> - </td> - </tr> + </td> + <td> + <div class="input-field"> + {{ form.remarks }} + <label for="{{ form.absent.id_for_label }}"> + {% trans "Remarks" %} + </label> + </div> + </td> + </tr> + {% else %} + <tr> + <td>{{ form.person_name.value }}</td> + <td>{{ form.absent.value }}</td> + <td>{{ form.late.value }}</td> + <td>{{ form.excused.value }}</td> + <td>{{ form.excuse_type.value }}</td> + <td> + {% for extra_mark in form.extra_marks.value %} + {{ extra_mark }}{% if not forloop.last %},{% endif %} + {% endfor %} + </td> + <td>{{ form.remarks.value }}</td> + </tr> + {% endif %} {% endfor %} </tbody> </table> </div> </div> </div> - {% endif %} - <div class="col s12" id="version-history"> - <div class="card"> - <div class="card-content"> + {% if can_view_lesson_documentation %} + <div class="col s12" id="version-history"> + <div class="card"> + <div class="card-content"> <span class="card-title"> {% blocktrans %}Change history{% endblocktrans %} </span> - {% include 'core/partials/crud_events.html' with obj=lesson_documentation %} + {% include 'core/partials/crud_events.html' with obj=lesson_documentation %} + </div> </div> </div> - </div> + {% endif %} </div> - <p>{% include "core/partials/save_button.html" %}</p> + {% if can_edit_lesson_documentation or can_edit_lesson_personalnote %} + <p>{% include "core/partials/save_button.html" %}</p> + {% endif %} </form> {% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html index dd907383585e258879539a07508a5e9c0aae7b81..efb5179adbc622ab80c47802d2230eb57a19b679 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html @@ -1,5 +1,6 @@ {# -*- engine:django -*- #} {% extends "core/base.html" %} +{% load rules %} {% load data_helpers %} {% load week_helpers %} {% load i18n %} @@ -14,6 +15,8 @@ {% endblock %} {% block content %} + {% has_perm "alsijil.edit_person_overview_personalnote" user person as can_mark_all_as_excused %} + <div class="row"> <div class="col s12 m12 l6"> <h5>{% trans "Unexcused absences" %}</h5> @@ -22,16 +25,19 @@ {% for note in unexcused_absences %} {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} <li class="collection-item"> - <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> - {% csrf_token %} - {% trans "Mark as" %} - <input type="hidden" value="{{ note.pk }}" name="personal_note"> - {% include "alsijil/partials/mark_as_buttons.html" %} - <a class="btn-flat red-text" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons center">cancel</i> - </a> - </form> + {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %} + {% if can_edit_personal_note %} + <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> + {% csrf_token %} + {% trans "Mark as" %} + <input type="hidden" value="{{ note.pk }}" name="personal_note"> + {% include "alsijil/partials/mark_as_buttons.html" %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> + </form> + {% endif %} <i class="material-icons left red-text">warning</i> <p class="no-margin"> <a href="{% url "lesson_by_week_and_period" note.year note.week note.lesson_period.pk %}">{{ note_date }}, {{ note.lesson_period }}</a> @@ -39,16 +45,18 @@ {% if note.remarks %} <p class="no-margin"><em>{{ note.remarks }}</em></p> {% endif %} - <form action="" method="post" class="hide-on-med-and-up"> - {% csrf_token %} - {% trans "Mark as" %} - <input type="hidden" value="{{ note.pk }}" name="personal_note"> - {% include "alsijil/partials/mark_as_buttons.html" %} - <a class="btn-flat red-text" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons center">cancel</i> - </a> - </form> + {% if can_edit_personal_note %} + <form action="" method="post" class="hide-on-med-and-up"> + {% csrf_token %} + {% trans "Mark as" %} + <input type="hidden" value="{{ note.pk }}" name="personal_note"> + {% include "alsijil/partials/mark_as_buttons.html" %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> + </form> + {% endif %} </li> {% empty %} <li class="collection-item flow-text"> @@ -56,47 +64,49 @@ </li> {% endfor %} </ul> - <h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5> - <ul class="collapsible"> - {% for school_term, stat in stats %} - <li {% if forloop.first %}class="active"{% endif %}> - <div class="collapsible-header"> - <i class="material-icons">date_range</i>{{ school_term }}</div> - <div class="collapsible-body"> - <table> - <tr> - <th colspan="2">{% trans 'Absences' %}</th> - <td>{{ stat.absences_count }}</td> - </tr> - <tr> - <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td> - <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td> - <th class="truncate">{% trans 'Excused' %}</th> - <td>{{ stat.excused }}</td> - </tr> - {% for excuse_type in excuse_types %} - <th>{{ excuse_type.name }}</th> - <td>{{ stat|get_dict:excuse_type.count_label }}</td> - {% endfor %} - <tr> - <th>{% trans 'Unexcused' %}</th> - <td>{{ stat.unexcused }}</td> - </tr> - <tr> - <th colspan="2">{% trans 'Tardiness' %}</th> - <td>{{ stat.tardiness }}'</td> - </tr> - {% for extra_mark in extra_marks %} + {% if stats %} + <h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5> + <ul class="collapsible"> + {% for school_term, stat in stats %} + <li {% if forloop.first %}class="active"{% endif %}> + <div class="collapsible-header"> + <i class="material-icons">date_range</i>{{ school_term }}</div> + <div class="collapsible-body"> + <table> <tr> - <th colspan="2">{{ extra_mark.name }}</th> - <td>{{ stat|get_dict:extra_mark.count_label }}</td> + <th colspan="2">{% trans 'Absences' %}</th> + <td>{{ stat.absences_count }}</td> </tr> - {% endfor %} - </table> - </div> - </li> - {% endfor %} - </ul> + <tr> + <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td> + <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td> + <th class="truncate">{% trans 'Excused' %}</th> + <td>{{ stat.excused }}</td> + </tr> + {% for excuse_type in excuse_types %} + <th>{{ excuse_type.name }}</th> + <td>{{ stat|get_dict:excuse_type.count_label }}</td> + {% endfor %} + <tr> + <th>{% trans 'Unexcused' %}</th> + <td>{{ stat.unexcused }}</td> + </tr> + <tr> + <th colspan="2">{% trans 'Tardiness' %}</th> + <td>{{ stat.tardiness }}'</td> + </tr> + {% for extra_mark in extra_marks %} + <tr> + <th colspan="2">{{ extra_mark.name }}</th> + <td>{{ stat|get_dict:extra_mark.count_label }}</td> + </tr> + {% endfor %} + </table> + </div> + </li> + {% endfor %} + </ul> + {% endif %} </div> <div class="col s12 m12 l6"> <h5>{% trans "Relevant personal notes" %}</h5> @@ -121,21 +131,25 @@ {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} {% ifchanged note_date %} <li class="collection-item"> - <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> - {% csrf_token %} - {% trans "Mark all as" %} - <input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date"> - {% include "alsijil/partials/mark_as_buttons.html" %} - </form> + {% if can_mark_all_as_excused %} + <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> + {% csrf_token %} + {% trans "Mark all as" %} + <input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date"> + {% include "alsijil/partials/mark_as_buttons.html" %} + </form> + {% endif %} <i class="material-icons left">schedule</i> {{ note_date }} - <form action="" method="post" class="hide-on-med-and-up"> - {% csrf_token %} - {% trans "Mark all as" %} - <input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date"> - {% include "alsijil/partials/mark_as_buttons.html" %} - </form> + {% if can_mark_all_as_excused %} + <form action="" method="post" class="hide-on-med-and-up"> + {% csrf_token %} + {% trans "Mark all as" %} + <input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date"> + {% include "alsijil/partials/mark_as_buttons.html" %} + </form> + {% endif %} </li> {% endifchanged %} @@ -154,7 +168,8 @@ </div> <div class="col s12 m7 no-padding"> - {% if note.absent and not note.excused %} + {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %} + {% if note.absent and not note.excused and can_edit_personal_note %} <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> {% csrf_token %} {% trans "Mark as" %} @@ -165,6 +180,11 @@ <i class="material-icons center">cancel</i> </a> </form> + {% elif can_edit_personal_note %} + <a class="btn-flat red-text right hide-on-small-only" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> {% endif %} {% if note.absent %} @@ -196,7 +216,7 @@ </div> <div class="col s12 hide-on-med-and-up"> - {% if note.absent and not note.excused %} + {% if note.absent and not note.excused and can_edit_personal_note %} <form action="" method="post"> {% csrf_token %} {% trans "Mark as" %} @@ -207,6 +227,12 @@ <i class="material-icons center">cancel</i> </a> </form> + {% elif can_edit_personal_note %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons left">cancel</i> + {% trans "Delete" %} + </a> {% endif %} </div> </li> diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html index ea549f7f15783769f79a9d598304b856481adb18..de7c30ecd010294cc05e37a1aa58e27c519036f4 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html @@ -1,7 +1,7 @@ {# -*- engine:django -*- #} {% extends "core/base.html" %} -{% load material_form i18n week_helpers static data_helpers %} +{% load material_form i18n week_helpers static data_helpers rules %} {% block browser_title %}{% blocktrans %}Week view{% endblocktrans %}{% endblock %} @@ -66,38 +66,41 @@ </thead> <tbody> {% for period in periods %} - <tr> - <td class="center-align"> - {% include "alsijil/partials/lesson_status_icon.html" with period=period %} - </td> - <td class="tr-link"> - <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.period.period }}. - </a> - </td> - {% if not group %} + {% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %} + {% if can_view_lesson_documentation %} + <tr> + <td class="center-align"> + {% include "alsijil/partials/lesson_status_icon.html" with period=period %} + </td> + <td class="tr-link"> + <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> + {{ period.period.period }}. + </a> + </td> + {% if not group %} + <td> + <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> + {{ period.lesson.group_names }} + </a> + </td> + {% endif %} <td> <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.lesson.group_names }} + {{ period.get_subject.name }} </a> </td> - {% endif %} - <td> - <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.get_subject.name }} - </a> - </td> - <td> - <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.get_teacher_names }} - </a> - </td> - <td> - <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.get_lesson_documentation.topic }} - </a> - </td> - </tr> + <td> + <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> + {{ period.get_teacher_names }} + </a> + </td> + <td> + <a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> + {{ period.get_lesson_documentation.topic }} + </a> + </td> + </tr> + {% endif %} {% endfor %} </tbody> </table> @@ -113,7 +116,12 @@ </span> {% for person in persons %} <h5 class="card-title"> - <a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a> + {% has_perm "alsijil.view_person_overview" user person.person as can_view_person_overview %} + {% if can_view_person_overview %} + <a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a> + {% else %} + {{ person.person.full_name }} + {% endif %} </h5> <p class="card-text"> {% trans "Absent" %}: {{ person.person.absences_count }} diff --git a/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html b/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html index 2be1f28c96e70f35e63fe4f5cefb50c232e38e0a..9c74d62e127f6a03b1ad7637114badfc2f4d9de2 100644 --- a/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html +++ b/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html @@ -2,7 +2,7 @@ {% extends "core/base.html" %} -{% load i18n %} +{% load i18n rules %} {% load render_table from django_tables2 %} {% block browser_title %}{% blocktrans %}Excuse types{% endblocktrans %}{% endblock %} @@ -11,10 +11,13 @@ {% block content %} {% include "alsijil/excuse_type/warning.html" %} - <a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}"> - <i class="material-icons left">add</i> - {% trans "Create excuse type" %} - </a> + {% has_perm "alsijil.add_excusetype" user as add_excusetype %} + {% if add_excusetype %} + <a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}"> + <i class="material-icons left">add</i> + {% trans "Create excuse type" %} + </a> + {% endif %} {% render_table table %} {% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/absences.html b/aleksis/apps/alsijil/templates/alsijil/partials/absences.html index 6deaa3891c480026b22fbad2cfc75a4f2b260110..c72204c556efee0c5308d607947b5cd9670d85c3 100644 --- a/aleksis/apps/alsijil/templates/alsijil/partials/absences.html +++ b/aleksis/apps/alsijil/templates/alsijil/partials/absences.html @@ -1,6 +1,9 @@ -{% load i18n %} +{% load i18n rules %} {% for note in notes %} - <span class="{% if note.excused %}green-text{% else %}red-text{% endif %}">{{ note.person }} - {% if note.excused %}{% if note.excuse_type %}({{ note.excuse_type.short_name }}){% else %}{% trans "(e)" %}{% endif %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %} - </span> + {% has_perm "alsijil.view_personalnote" user note as can_view_personalnote %} + {% if can_view_personalnote %} + <span class="{% if note.excused %}green-text{% else %}red-text{% endif %}">{{ note.person }} + {% if note.excused %}{% if note.excuse_type %}({{ note.excuse_type.short_name }}){% else %}{% trans "(e)" %}{% endif %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %} + </span> + {% endif %} {% endfor %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html b/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html index b3639ea88b779bd746ab551f0a9f2e5d938fd786..3bb86191612f2bdface729be8d048a6fb05edcd5 100644 --- a/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html +++ b/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html @@ -1,3 +1,7 @@ +{% load rules %} {% for note in notes %} - <span>{{ note.person }} ({{ note.late }}'){% if not forloop.last %},{% endif %}</span> + {% has_perm "alsijil.view_personalnote" user note as can_view_personalnote %} + {% if can_view_personalnote %} + <span>{{ note.person }} ({{ note.late }}'){% if not forloop.last %},{% endif %}</span> + {% endif %} {% endfor %} diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..10bf5b4e7e4c67b2f998209849e7fa60f5659945 --- /dev/null +++ b/aleksis/apps/alsijil/util/alsijil_helpers.py @@ -0,0 +1,52 @@ +from typing import Optional + +from django.http import HttpRequest + +from calendarweek import CalendarWeek + +from aleksis.apps.chronos.models import LessonPeriod +from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk + + +def get_lesson_period_by_pk( + request: HttpRequest, + year: Optional[int] = None, + week: Optional[int] = None, + period_id: Optional[int] = None, +): + """Get LessonPeriod object either by given object_id or by time and current person.""" + wanted_week = CalendarWeek(year=year, week=week) + if period_id: + lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get( + pk=period_id + ) + elif hasattr(request, "user") and hasattr(request.user, "person"): + if request.user.person.lessons_as_teacher.exists(): + lesson_period = ( + LessonPeriod.objects.at_time() + .filter_teacher(request.user.person) + .first() + ) + else: + lesson_period = ( + LessonPeriod.objects.at_time() + .filter_participant(request.user.person) + .first() + ) + else: + lesson_period = None + return lesson_period + + +def get_timetable_instance_by_pk( + request: HttpRequest, + year: Optional[int] = None, + week: Optional[int] = None, + type_: Optional[str] = None, + id_: Optional[int] = None, +): + """Get timetable object (teacher, room or group) by given type and id or the current person.""" + if type_ and id_: + return get_el_by_pk(request, type_, id_) + elif hasattr(request, "user") and hasattr(request.user, "person"): + return request.user.person diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py new file mode 100644 index 0000000000000000000000000000000000000000..3d988c8f4c3134b46451d04b24a570177473c44a --- /dev/null +++ b/aleksis/apps/alsijil/util/predicates.py @@ -0,0 +1,236 @@ +from typing import Any, Union + +from django.contrib.auth.models import User + +from guardian.shortcuts import get_objects_for_user +from rules import predicate + +from aleksis.apps.chronos.models import LessonPeriod +from aleksis.core.models import Group, Person +from aleksis.core.util.predicates import check_object_permission + +from ..models import PersonalNote + + +@predicate +def is_none(user: User, obj: Any) -> bool: + """Predicate that checks if the provided object is None-like.""" + return bool(obj) + + +@predicate +def is_lesson_teacher(user: User, obj: LessonPeriod) -> bool: + """Predicate for teachers of a lesson. + + Checks whether the person linked to the user is a teacher + in the lesson or the substitution linked to the given LessonPeriod. + """ + if obj: + sub = obj.get_substitution() + if sub and sub in user.person.lesson_substitutions.all(): + return True + return user.person in obj.lesson.teachers.all() + return False + + +@predicate +def is_lesson_participant(user: User, obj: LessonPeriod) -> bool: + """Predicate for participants of a lesson. + + Checks whether the person linked to the user is a member in + the groups linked to the given LessonPeriod. + """ + if hasattr(obj, "lesson"): + return obj.lesson.groups.filter(members=user.person).exists() + return False + + +@predicate +def is_lesson_parent_group_owner(user: User, obj: LessonPeriod) -> bool: + """ + Predicate for parent group owners of a lesson. + + Checks whether the person linked to the user is the owner of + any parent groups of any groups of the given LessonPeriods lesson. + """ + if hasattr(obj, "lesson"): + return obj.lesson.groups.filter(parent_groups__owners=user.person).exists() + return False + + +@predicate +def is_group_owner(user: User, obj: Union[Group, Person]) -> bool: + """Predicate for group owners of a given group. + + Checks whether the person linked to the user is the owner of the given group. + If there isn't provided a group, it will return `False`. + """ + if isinstance(obj, Group): + if obj.owners.filter(pk=user.person.pk).exists(): + return True + + return False + + +@predicate +def is_person_group_owner(user: User, obj: Person) -> bool: + """ + Predicate for group owners of any group. + + Checks whether the person linked to the user is + the owner of any group of the given person. + """ + if obj: + return obj.member_of.filter(owners=user.person).exists() + return False + + +@predicate +def is_person_primary_group_owner(user: User, obj: Person) -> bool: + """ + Predicate for group owners of the person's primary group. + + Checks whether the person linked to the user is + the owner of the primary group of the given person. + """ + if obj.primary_group: + return user.person in obj.primary_group.owners.all() + return False + + +def has_person_group_object_perm(perm: str): + """Predicate builder for permissions on a set of member groups. + + Checks whether a user has a permission on any group of a person. + """ + name = f"has_person_group_object_perm:{perm}" + + @predicate(name) + def fn(user: User, obj: Person) -> bool: + for group in obj.member_of.all(): + if check_object_permission(user, perm, group): + return True + return False + + return fn + + +@predicate +def is_group_member(user: User, obj: Union[Group, Person]) -> bool: + """Predicate for group membership. + + Checks whether the person linked to the user is a member of the given group. + If there isn't provided a group, it will return `False`. + """ + if isinstance(obj, Group): + if obj.members.filter(pk=user.person.pk).exists(): + return True + + return False + + +def has_lesson_group_object_perm(perm: str): + """Predicate builder for permissions on lesson groups. + + Checks whether a user has a permission on any group of a LessonPeriod. + """ + name = f"has_lesson_group_object_perm:{perm}" + + @predicate(name) + def fn(user: User, obj: LessonPeriod) -> bool: + if hasattr(obj, "lesson"): + for group in obj.lesson.groups.all(): + if check_object_permission(user, perm, group): + return True + return False + return True + + return fn + + +def has_personal_note_group_perm(perm: str): + """Predicate builder for permissions on personal notes + + Checks whether a user has a permission on any group of a person of a PersonalNote. + """ + name = f"has_personal_note_person_or_group_perm:{perm}" + + @predicate(name) + def fn(user: User, obj: PersonalNote) -> bool: + if hasattr(obj, "person"): + for group in obj.person.member_of.all(): + if check_object_permission(user, perm, group): + return True + return False + + return fn + + +@predicate +def is_own_personal_note(user: User, obj: PersonalNote) -> bool: + """Predicate for users referred to in a personal note + + Checks whether the user referred to in a PersonalNote is the active user. + """ + if hasattr(obj, "person") and obj.person is user.person: + return True + return False + + +@predicate +def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool: + """Predicate for teachers of a lesson referred to in the lesson period of a personal note. + + Checks whether the person linked to the user is a teacher + in the lesson or the substitution linked to the LessonPeriod of the given PersonalNote. + """ + if hasattr(obj, "lesson_period"): + if hasattr(obj.lesson_period, "lesson"): + sub = obj.lesson_period.get_substitution() + if sub and user.person in Person.objects.filter( + lesson_substitutions=obj.lesson_period.get_substitution() + ): + return True + + return user.person in obj.lesson_period.lesson.teachers.all() + + return False + return False + + +@predicate +def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) -> bool: + """ + Predicate for parent group owners of a lesson referred to in the lesson period of a personal note. + + 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 hasattr(obj, "lesson_period"): + if hasattr(obj.lesson_period, "lesson"): + return obj.lesson_period.lesson.groups.filter( + parent_groups__owners=user.person + ).exists() + return False + return False + + +@predicate +def has_any_object_absence(user: User) -> bool: + """ + Predicate which builds a query with all the persons the given users is allowed to register an absence for. + """ + if get_objects_for_user(user, "core.register_absence_person", Person).exists(): + return True + if Person.objects.filter(member_of__owners=user.person).exists(): + return True + if Person.objects.filter( + member_of__in=get_objects_for_user(user, "core.register_absence_group", Group) + ).exists(): + return True + + +@predicate +def is_teacher(user: User, obj: Person) -> bool: + """Predicate which checks if the provided object is a teacher.""" + return user.person.is_teacher diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 0919d0ed4856c947882f5e84d24767bc687cd875..6120e8dca3e0ba06bc0d93ed05c1b11adee003c7 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -13,7 +13,7 @@ import reversion from calendarweek import CalendarWeek from django_tables2 import SingleTableView from reversion.views import RevisionMixin -from rules.contrib.views import PermissionRequiredMixin +from rules.contrib.views import PermissionRequiredMixin, permission_required from aleksis.apps.chronos.managers import TimetableType from aleksis.apps.chronos.models import LessonPeriod, TimePeriod @@ -34,8 +34,10 @@ from .forms import ( ) from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote from .tables import ExcuseTypeTable, ExtraMarkTable +from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk +@permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk) def lesson( request: HttpRequest, year: Optional[int] = None, @@ -44,27 +46,16 @@ def lesson( ) -> HttpResponse: context = {} - if year and week and period_id: - # Get a specific lesson period if provided in URL - wanted_week = CalendarWeek(year=year, week=week) - lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get( - pk=period_id - ) - - date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday) + lesson_period = get_lesson_period_by_pk(request, year, week, period_id) - if ( - date_of_lesson < lesson_period.lesson.validity.date_start - or date_of_lesson > lesson_period.lesson.validity.date_end - ): - return HttpResponseNotFound() - else: - # Determine current lesson by current date and time - lesson_period = ( - LessonPeriod.objects.at_time().filter_teacher(request.user.person).first() - ) + if period_id: + wanted_week = CalendarWeek(year=year, week=week) + elif hasattr(request, "user") and hasattr(request.user, "person"): wanted_week = CalendarWeek() + else: + wanted_week = None + if not all((year, week, period_id)): if lesson_period: return redirect( "lesson_by_week_and_period", @@ -80,6 +71,14 @@ def lesson( ) ) + date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday) + + if ( + date_of_lesson < lesson_period.lesson.validity.date_start + or date_of_lesson > lesson_period.lesson.validity.date_end + ): + return HttpResponseNotFound() + if ( datetime.combine( wanted_week[lesson_period.period.weekday], lesson_period.period.time_start, @@ -110,13 +109,20 @@ def lesson( ) # Create a formset that holds all personal notes for all persons in this lesson - persons_qs = lesson_period.get_personal_notes(wanted_week) + if not request.user.has_perm("alsijil.view_lesson_personalnote", lesson_period): + persons = Person.objects.filter(pk=request.user.person.pk) + else: + persons = Person.objects.all() + + persons_qs = lesson_period.get_personal_notes(persons, wanted_week) personal_note_formset = PersonalNoteFormSet( request.POST or None, queryset=persons_qs, prefix="personal_notes" ) if request.method == "POST": - if lesson_documentation_form.is_valid(): + if lesson_documentation_form.is_valid() and request.user.has_perm( + "alsijil.edit_lessondocumentation", lesson_period + ): lesson_documentation_form.save() messages.success(request, _("The lesson documentation has been saved.")) @@ -126,7 +132,9 @@ def lesson( not getattr(substitution, "cancelled", False) or not get_site_preferences()["alsijil__block_personal_notes_for_cancelled"] ): - if personal_note_formset.is_valid(): + if personal_note_formset.is_valid() and request.user.has_perm( + "alsijil.edit_lesson_personalnote", lesson_period + ): with reversion.create_revision(): instances = personal_note_formset.save() @@ -154,6 +162,7 @@ def lesson( return render(request, "alsijil/class_register/lesson.html", context) +@permission_required("alsijil.view_week", fn=get_timetable_instance_by_pk) def week_view( request: HttpRequest, year: Optional[int] = None, @@ -168,7 +177,9 @@ def week_view( else: wanted_week = CalendarWeek() - lesson_periods = LessonPeriod.objects.annotate( + instance = get_timetable_instance_by_pk(request, year, week, type_, id_) + + lesson_periods = LessonPeriod.objects.in_week(wanted_week).annotate( has_documentation=Exists( LessonDocumentation.objects.filter( ~Q(topic__exact=""), @@ -177,23 +188,16 @@ def week_view( year=wanted_week.year, ) ) - ).in_week(wanted_week) + ) - group = None if type_ and id_: - instance = get_el_by_pk(request, type_, id_) - if isinstance(instance, HttpResponseNotFound): return HttpResponseNotFound() type_ = TimetableType.from_string(type_) - if type_ == TimetableType.GROUP: - group = instance - lesson_periods = lesson_periods.filter_from_type(type_, instance) elif hasattr(request, "user") and hasattr(request.user, "person"): - instance = request.user.person if request.user.person.lessons_as_teacher.exists(): lesson_periods = lesson_periods.filter_teacher(request.user.person) type_ = TimetableType.TEACHER @@ -222,13 +226,20 @@ def week_view( select_form.cleaned_data["instance"].pk, ) + if type_ == TimetableType.GROUP: + group = instance + else: + group = None + if lesson_periods: # Aggregate all personal notes for this group and week lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True)) persons_qs = Person.objects.filter(is_active=True) - if group: + if not request.user.has_perm("alsijil.view_week_personalnote", instance): + persons_qs = persons_qs.filter(pk=request.user.person.pk) + elif group: persons_qs = persons_qs.filter(member_of=group) else: persons_qs = persons_qs.filter( @@ -340,6 +351,9 @@ def week_view( return render(request, "alsijil/class_register/week_view.html", context) +@permission_required( + "alsijil.view_full_register", fn=objectgetter_optional(Group, None, False) +) def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: context = {} @@ -461,6 +475,7 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: return render(request, "alsijil/print/full_register.html", context) +@permission_required("alsijil.view_my_students") def my_students(request: HttpRequest) -> HttpResponse: context = {} relevant_groups = request.user.person.get_owner_groups_with_lessons() @@ -469,12 +484,17 @@ def my_students(request: HttpRequest) -> HttpResponse: return render(request, "alsijil/class_register/persons.html", context) +@permission_required("alsijil.view_my_groups",) def my_groups(request: HttpRequest) -> HttpResponse: context = {} context["groups"] = request.user.person.get_owner_groups_with_lessons() return render(request, "alsijil/class_register/groups.html", context) +@permission_required( + "alsijil.view_person_overview", + fn=objectgetter_optional(Person, "request.user.person", True), +) def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: context = {} person = objectgetter_optional( @@ -505,6 +525,11 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp request.POST["date"], "%Y-%m-%d" ).date() + if not request.user.has_perm( + "alsijil.edit_person_overview_personalnote", person + ): + raise PermissionDenied() + notes = person.personal_notes.filter( week=date.isocalendar()[1], lesson_period__period__weekday=date.weekday(), @@ -530,6 +555,8 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp note = PersonalNote.objects.get( pk=int(request.POST["personal_note"]) ) + if not request.user.has_perm("alsijil.edit_personalnote", note): + raise PermissionDenied() if note.absent: note.excused = True note.excuse_type = excuse_type @@ -543,10 +570,17 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp person.refresh_from_db() - unexcused_absences = person.personal_notes.filter(absent=True, excused=False) + if request.user.has_perm("alsijil.view_person_overview_personalnote", person): + allowed_personal_notes = person.personal_notes.all() + else: + allowed_personal_notes = person.personal_notes.filter( + lesson_period__lesson__groups__owners=request.user.person + ) + + unexcused_absences = allowed_personal_notes.filter(absent=True, excused=False) context["unexcused_absences"] = unexcused_absences - personal_notes = person.personal_notes.filter( + personal_notes = allowed_personal_notes.filter( Q(absent=True) | Q(late__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False) ).order_by( "-lesson_period__lesson__validity__date_start", @@ -557,60 +591,66 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp context["personal_notes"] = personal_notes context["excuse_types"] = ExcuseType.objects.all() - school_terms = SchoolTerm.objects.all().order_by("-date_start") - stats = [] - for school_term in school_terms: - stat = {} - personal_notes = PersonalNote.objects.filter( - person=person, lesson_period__lesson__validity__school_term=school_term - ) - - if not personal_notes.exists(): - continue - - stat.update( - personal_notes.filter(absent=True).aggregate(absences_count=Count("absent")) - ) - stat.update( - personal_notes.filter( - absent=True, excused=True, excuse_type__isnull=True - ).aggregate(excused=Count("absent")) - ) - stat.update( - personal_notes.filter(absent=True, excused=False).aggregate( - unexcused=Count("absent") + if request.user.has_perm("alsijil.view_person_statistics_personalnote", person): + school_terms = SchoolTerm.objects.all().order_by("-date_start") + stats = [] + for school_term in school_terms: + stat = {} + personal_notes = PersonalNote.objects.filter( + person=person, lesson_period__lesson__validity__school_term=school_term ) - ) - stat.update(personal_notes.aggregate(tardiness=Sum("late"))) - for extra_mark in ExtraMark.objects.all(): + if not personal_notes.exists(): + continue + stat.update( - personal_notes.filter(extra_marks=extra_mark).aggregate( - **{extra_mark.count_label: Count("pk")} + personal_notes.filter(absent=True).aggregate( + absences_count=Count("absent") ) ) - - for excuse_type in ExcuseType.objects.all(): stat.update( - personal_notes.filter(absent=True, excuse_type=excuse_type).aggregate( - **{excuse_type.count_label: Count("absent")} + personal_notes.filter( + absent=True, excused=True, excuse_type__isnull=True + ).aggregate(excused=Count("absent")) + ) + stat.update( + personal_notes.filter(absent=True, excused=False).aggregate( + unexcused=Count("absent") ) ) + stat.update(personal_notes.aggregate(tardiness=Sum("late"))) - stats.append((school_term, stat)) - context["stats"] = stats + for extra_mark in ExtraMark.objects.all(): + stat.update( + personal_notes.filter(extra_marks=extra_mark).aggregate( + **{extra_mark.count_label: Count("pk")} + ) + ) + + for excuse_type in ExcuseType.objects.all(): + stat.update( + personal_notes.filter( + absent=True, excuse_type=excuse_type + ).aggregate(**{excuse_type.count_label: Count("absent")}) + ) + + stats.append((school_term, stat)) + context["stats"] = stats context["excuse_types"] = ExcuseType.objects.all() context["extra_marks"] = ExtraMark.objects.all() return render(request, "alsijil/class_register/person.html", context) +@permission_required("alsijil.view_register_absence") def register_absence(request: HttpRequest) -> HttpResponse: context = {} register_absence_form = RegisterAbsenceForm(request.POST or None) if request.method == "POST": - if register_absence_form.is_valid(): + if register_absence_form.is_valid() and request.user.has_perm( + "alsijil.register_absence", register_absence_form.cleaned_data["person"] + ): # Get data from form person = register_absence_form.cleaned_data["person"] start_date = register_absence_form.cleaned_data["date_start"] @@ -649,9 +689,10 @@ def register_absence(request: HttpRequest) -> HttpResponse: return render(request, "alsijil/absences/register.html", context) -class DeletePersonalNoteView(DetailView): +class DeletePersonalNoteView(PermissionRequiredMixin, DetailView): model = PersonalNote template_name = "core/pages/delete.html" + permission_required = "alsijil.edit_personalnote" def post(self, request, *args, **kwargs): note = self.get_object() @@ -662,83 +703,83 @@ class DeletePersonalNoteView(DetailView): return redirect("overview_person", note.person.pk) -class ExtraMarkListView(SingleTableView, PermissionRequiredMixin): +class ExtraMarkListView(PermissionRequiredMixin, SingleTableView): """Table of all extra marks.""" model = ExtraMark table_class = ExtraMarkTable - permission_required = "core.view_extramark" + permission_required = "alsijil.view_extramark" template_name = "alsijil/extra_mark/list.html" -class ExtraMarkCreateView(AdvancedCreateView, PermissionRequiredMixin): +class ExtraMarkCreateView(PermissionRequiredMixin, AdvancedCreateView): """Create view for extra marks.""" model = ExtraMark form_class = ExtraMarkForm - permission_required = "core.create_extramark" + permission_required = "alsijil.create_extramark" template_name = "alsijil/extra_mark/create.html" success_url = reverse_lazy("extra_marks") success_message = _("The extra mark has been created.") -class ExtraMarkEditView(AdvancedEditView, PermissionRequiredMixin): +class ExtraMarkEditView(PermissionRequiredMixin, AdvancedEditView): """Edit view for extra marks.""" model = ExtraMark form_class = ExtraMarkForm - permission_required = "core.edit_extramark" + permission_required = "alsijil.edit_extramark" template_name = "alsijil/extra_mark/edit.html" success_url = reverse_lazy("extra_marks") success_message = _("The extra mark has been saved.") -class ExtraMarkDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin): +class ExtraMarkDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView): """Delete view for extra marks""" model = ExtraMark - permission_required = "core.delete_extramark" + permission_required = "alsijil.delete_extramark" template_name = "core/pages/delete.html" success_url = reverse_lazy("extra_marks") success_message = _("The extra mark has been deleted.") -class ExcuseTypeListView(SingleTableView, PermissionRequiredMixin): +class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView): """Table of all excuse types.""" model = ExcuseType table_class = ExcuseTypeTable - permission_required = "core.view_excusetype" + permission_required = "alsijil.view_excusetypes" template_name = "alsijil/excuse_type/list.html" -class ExcuseTypeCreateView(AdvancedCreateView, PermissionRequiredMixin): +class ExcuseTypeCreateView(PermissionRequiredMixin, AdvancedCreateView): """Create view for excuse types.""" model = ExcuseType form_class = ExcuseTypeForm - permission_required = "core.create_excusetype" + permission_required = "alsijil.add_excusetype" template_name = "alsijil/excuse_type/create.html" success_url = reverse_lazy("excuse_types") success_message = _("The excuse type has been created.") -class ExcuseTypeEditView(AdvancedEditView, PermissionRequiredMixin): +class ExcuseTypeEditView(PermissionRequiredMixin, AdvancedEditView): """Edit view for excuse types.""" model = ExcuseType form_class = ExcuseTypeForm - permission_required = "core.edit_excusetype" + permission_required = "alsijil.edit_excusetype" template_name = "alsijil/excuse_type/edit.html" success_url = reverse_lazy("excuse_types") success_message = _("The excuse type has been saved.") -class ExcuseTypeDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin): +class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView): """Delete view for excuse types""" model = ExcuseType - permission_required = "core.delete_excusetype" + permission_required = "alsijil.delete_excusetype" template_name = "core/pages/delete.html" success_url = reverse_lazy("excuse_types") success_message = _("The excuse type has been deleted.")