diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index b503bbf8f552e31977e56e64709a019cff265cfc..c27bfc88ade60a7e85a33861be0f852633b866ee 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -1,6 +1,7 @@ from datetime import date -from django.db.models import Exists, F, OuterRef +from django.db.models import Exists, F, OuterRef, QuerySet +from django.utils.translation import ugettext as _ from calendarweek import CalendarWeek @@ -57,7 +58,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, @@ -71,7 +72,7 @@ def get_personal_notes(self, wanted_week: CalendarWeek): """ # 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, lesson_period=self, person__pk=OuterRef("pk") @@ -94,3 +95,13 @@ def get_personal_notes(self, wanted_week: CalendarWeek): return PersonalNote.objects.select_related("person").filter( lesson_period=self, week=wanted_week.week ) + +# Dynamically add extra permissions to Group and Person models in core, requires migration afterwards +Group.add_permission("view_week_class_register_group", _("Can view week 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 a absence for all members of a group")) +Person.add_permission("register_absence_person", _("Can register a absence for a person")) diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index c6c6baf848bbb3a8cd497e67c38801a9cecdc9ad..4c8cf021e02dcc194650927c8f7ee68e109ac138 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -5,31 +5,96 @@ from aleksis.core.util.predicates import ( has_global_perm, has_object_perm, has_person, + is_current_person, +) + +from .util.predicates import ( + is_lesson_teacher, + is_lesson_participant, + is_lesson_parent_group_owner, + has_lesson_group_object_perm, + is_group_member, + is_group_owner, + has_person_group_object_perm, + is_person_group_owner, ) -from .util.predicates import has_lesson_perm, has_week_perm # View lesson view_lesson_predicate = has_person & ( - has_global_perm("chronos.view_lesson_period") | has_lesson_perm("chronos.view_lesson_period") + has_global_perm("alsijil.view_lesson") | is_lesson_teacher | is_lesson_participant | is_lesson_parent_group_owner | has_lesson_group_object_perm("alsijil.view_lesson") ) add_perm("alsijil.view_lesson", view_lesson_predicate) +# View lesson personal notes +view_lesson_personal_notes_predicate = has_person & ( + has_global_perm("alsijil.view_personalnote") | + has_lesson_group_object_perm("core.view_personalnote_group") | + is_lesson_teacher | + is_lesson_parent_group_owner +) +add_perm("alsijil.view_lesson_personalnote", view_lesson_personal_notes_predicate) + +# Edit lesson personal notes +edit_lesson_personal_notes_predicate = has_person & ( + has_global_perm("alsijil.change_personalnote") | + has_lesson_group_object_perm("core.edit_personalnote_group") | + is_lesson_teacher +) +add_perm("alsijil.edit_personalnote", edit_lesson_personal_notes_predicate) + +# View lesson documentation +view_lesson_documentation_predicate = has_person & ( + has_global_perm("alsijil.view_lessondocumentation") | + has_lesson_group_object_perm("core.view_lessondocumentation_group") | + is_lesson_teacher | + is_lesson_parent_group_owner | + is_lesson_participant +) +add_perm("alsijil.view_lessondocumentation", view_lesson_documentation_predicate) + +# Edit lesson documentation +edit_lesson_documentation_predicate = has_person & ( + has_global_perm("alsijil.change_lessondocumentation") | + has_lesson_group_object_perm("core.edit_lessondocumentation_group") | + is_lesson_teacher +) +add_perm("alsijil.edit_lessondocumentation", edit_lesson_documentation_predicate) + # View week overview view_week_predicate = has_person & ( - has_global_perm("alsijil.view_week") | has_week_perm("alsijil") + has_global_perm("alsijil.view_week") | has_object_perm("core.view_week_class_register_group") | is_group_member | is_group_owner | is_current_person ) add_perm("alsijil.view_week", view_week_predicate) +# View week personal notes +view_week_personal_notes_predicate = has_person & ( + has_global_perm("alsijil.view_personalnote") | + has_object_perm("alsijil.view_personalnote") | + is_group_owner +) +add_perm("alsijil.view_week_personalnote", view_week_personal_notes_predicate) + # Register absence register_absence_predicate = has_person & ( - has_global_perm("alsijil.register_absence") + has_global_perm("alsijil.register_absence") | + has_person_group_object_perm("core.register_absence_group") | + has_global_perm("core.register_absence_person") | + is_person_group_owner ) add_perm("alsijil.register_absence", register_absence_predicate) -# List all personal note filters -list_personal_note_filters_predicate = has_person & has_global_perm("alsijil.list_personal_note_filters") -add_perm("alsijil.list_personal_note_filters", list_personal_note_filters_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 all personal note filters +list_personal_note_filters_predicate = has_person & has_global_perm("alsijil.view_personal_note_filter") +add_perm("alsijil.view_personal_note_filters", list_personal_note_filters_predicate) # Edit personal note filter edit_personal_note_filter_predicate = has_person & ( diff --git a/aleksis/apps/alsijil/templates/alsijil/lesson.html b/aleksis/apps/alsijil/templates/alsijil/lesson.html index 82f85bf751b6960e1f20a35b3b3b9402eb78d54b..3b17254adf3b594eac5d4a618b9d4f2be259e30a 100644 --- a/aleksis/apps/alsijil/templates/alsijil/lesson.html +++ b/aleksis/apps/alsijil/templates/alsijil/lesson.html @@ -1,6 +1,6 @@ {# -*- engine:django -*- #} {% extends "core/base.html" %} -{% load material_form i18n static %} +{% load material_form i18n static rules %} {% block browser_title %}{% blocktrans %}Lesson{% endblocktrans %}{% endblock %} @@ -34,9 +34,33 @@ {% blocktrans %}Lesson documentation{% endblocktrans %} </div> {% csrf_token %} - {% form form=lesson_documentation_form %}{% endform %} + {% 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 %} + {% 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> + </table> + {% endif %} </div> </div> + {% if can_view_lesson_documentation %} <div class="col s4"> <div class="card dark-text"> <span class="card-title"> @@ -45,12 +69,14 @@ {% include 'core/crud_events_ul.html' with class_ul='list-group list-group-flush' class_li='list-group-item d-flex justify-content-between align-items-center' obj=lesson_documentation %} </div> </div> + {% endif %} </div> <div class="card dark-text"> <span class="card-title"> {% blocktrans %}Personal notes{% endblocktrans %} </span> + {% has_perm "alsijil.edit_personalnote" user lesson_period as can_edit_personalnote %} {{ personal_note_formset.management_form }} <table class="striped responsive-table"> @@ -62,17 +88,29 @@ <th>{% blocktrans %}Remarks{% endblocktrans %}</th> </tr> {% for form in personal_note_formset %} - {{ form.id }} - <tr> - <td>{{ form.person_name }}</td> - <td>{{ form.absent }}</td> - <td>{{ form.late }}</td> - <td>{{ form.excused }}</td> - <td>{{ form.remarks }}</td> - </tr> + {% if can_edit_personalnote %} + {{ form.id }} + <tr> + <td>{{ form.person_name }}</td> + <td>{{ form.absent }}</td> + <td>{{ form.late }}</td> + <td>{{ form.excused }}</td> + <td>{{ form.remarks }}</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.remarks.value }}</td> + </tr> + {% endif %} {% endfor %} </table> </div> - {% include "core/save_button.html" %} + {% if can_edit_lesson_documentation or can_edit_personalnote %} + {% include "core/save_button.html" %} + {% endif %} </form> {% endblock %} diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py index ec27bcb8b6edb0d2290ae5ef66bc4a36bd9b42c2..f1c1f4a3fb4e96fa239793b11ec9b00efd05a499 100644 --- a/aleksis/apps/alsijil/util/alsijil_helpers.py +++ b/aleksis/apps/alsijil/util/alsijil_helpers.py @@ -20,54 +20,24 @@ def get_lesson_period_by_pk( ): if period_id: lesson_period = LessonPeriod.objects.get(pk=period_id) - wanted_week = CalendarWeek(year=year, week=week) 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() - wanted_week = CalendarWeek() else: - lesson_period = wanted_week = None - return lesson_period, wanted_week + lesson_period = None + return lesson_period -def get_lesson_periods_by_pk( +def get_instance_by_pk( request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None, type_: Optional[str] = None, id_: Optional[int] = None, ): - if year and week: - wanted_week = CalendarWeek(year=year, week=week) - else: - wanted_week = CalendarWeek() - - lesson_periods = LessonPeriod.objects.annotate( - has_documentation=Exists( - LessonDocumentation.objects.filter( - ~Q(topic__exact=""), lesson_period=OuterRef("pk"), week=wanted_week.week - ) - ) - ).in_week(wanted_week) - if type_ and id_: - instance = get_el_by_pk(request, type_, id_) - - if isinstance(instance, HttpResponseNotFound): - return HttpResponseNotFound() - - type_ = TimetableType.from_string(type_) - - lesson_periods = lesson_periods.filter_from_type(type_, instance) + return get_el_by_pk(request, type_, id_) 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 - else: - lesson_periods = lesson_periods.filter_participant(request.user.person) - else: - lesson_periods = None - return lesson_periods, wanted_week, type_, instance \ No newline at end of file + return request.user.person \ No newline at end of file diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py index 94c69e0fe72e3bbd775dc7557285c24fc45a54e2..80faf95f8ddbd3867c024a2e4a3b22d20f4dc2bd 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -1,33 +1,76 @@ +from typing import Union + from django.contrib.auth.models import 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 -def has_lesson_perm(perm: str): - """Build predicate which checks whether the user is allowed to access the requested lesson notes.""" - name = f"has_lesson_perm:{perm}" +@predicate +def is_lesson_teacher(user: User, obj: LessonPeriod) -> bool: + """Predicate which checks whether the person linked to the user is a teacher in the lesson linked to the given LessonPeriod.""" + return user.person in obj.lesson.teachers - @predicate(name) - def fn(user: User, obj: tuple) -> bool: - if (user.person in obj[0].lesson.teachers) or (set(user.person.member_of).intersection(set(obj[0].lesson.groups))): +@predicate +def is_lesson_participant(user: User, obj: LessonPeriod) -> bool: + """Predicate which checks whether the person linked to the user is a member in the groups linked to the given LessonPeriod.""" + return obj.lesson.groups.filter(members=user.person).exists() + +@predicate +def is_lesson_parent_group_owner(user: User, obj: LessonPeriod) -> bool: + """Predicate which checks whether the person linked to the user is the owner of any parent groups of any groups of the given LessonPeriods lesson.""" + return obj.lesson.groups.filter(parent_groups__owners=user.person).exists() + +@predicate +def is_group_owner(user: User, obj: Union[Group, Person]) -> bool: + """Predicate which checks whether the person linked to the user is the owner of the given group.""" + if isinstance(obj, Group): + if obj.owners.filter(pk=user.person.pk).exists(): return True - return check_object_permission(user, perm, obj) - return fn + return False +@predicate +def is_person_group_owner(user: User, obj: Person) -> bool: + """Predicate which checks whether the person linked to the user is the owner of any group of the given person.""" + return obj.filter(member_of__owners=user.person).exists() @predicate -def has_week_perm(perm: str): - """Build predicate which checks whether the user is allowed to access the week overview.""" - name = f"has_week_perm:{perm}" +def has_person_group_object_perm(perm: str): + """Predicate which 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: tuple) -> bool: - if (user.person in obj[0].lesson.teachers) or (set(user.person.member_of).intersection(set(obj[0].lesson.groups))): + 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 which checks whether the person linked to the user is a member of the given group.""" + if isinstance(obj, Group): + if obj.members.filter(pk=user.person.pk).exists(): return True - return check_object_permission(user, perm, obj) + + return False + + +def has_lesson_group_object_perm(perm: str): + """Build predicate which 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: + for group in obj.lesson.groups.all(): + if check_object_permission(user, perm, group): + return True + return False return fn diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index da8d9764279f5e42d64ce7e08f718438034e7a24..f9e8296bcec246e4cf897f2dd6f2488d81734d27 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -15,7 +15,7 @@ from django_tables2 import RequestConfig from rules.contrib.views import permission_required from aleksis.apps.chronos.models import LessonPeriod -from aleksis.core.models import Group, Person, SchoolYear +from aleksis.core.models import Group, Person from aleksis.core.util import messages from aleksis.core.util.core_helpers import objectgetter_optional from .forms import ( @@ -27,7 +27,7 @@ from .forms import ( ) from .models import LessonDocumentation, PersonalNoteFilter from .tables import PersonalNoteFilterTable -from .util.alsijil_helpers import get_lesson_period_by_pk, get_lesson_periods_by_pk +from .util.alsijil_helpers import get_lesson_period_by_pk, get_instance_by_pk @permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk) @@ -39,7 +39,14 @@ def lesson( ) -> HttpResponse: context = {} - lesson_period, wanted_week = get_lesson_period_by_pk(request, year, week, period_id) + lesson_period = get_lesson_period_by_pk(request, year, week, period_id) + + 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 (year and week and period_id): if lesson_period: @@ -77,16 +84,19 @@ 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) + persons = Person.objects + if not request.user.has_perm("alsijil.view_lesson_personalnote", lesson_period): + persons = persons.filter(pk=request.user.pk) + 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() - if personal_note_formset.is_valid(): + if personal_note_formset.is_valid() and request.user.has_perm("alsijil.edit_personalnote", lesson_period): instances = personal_note_formset.save() # Iterate over personal notes and carry changed absences to following lessons @@ -105,13 +115,45 @@ def lesson( return render(request, "alsijil/lesson.html", context) -@permission_required("alsijil.view_week", fn=get_lesson_periods_by_pk) +@permission_required("alsijil.view_week", fn=get_instance_by_pk) def week_view( request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None, type_: Optional[str] = None, id_: Optional[int] = None ) -> HttpResponse: context = {} - lesson_periods, wanted_week, type_, instance = get_lesson_periods_by_pk(request, year, week, type_, id_) + instance = get_instance_by_pk(request, year, week, type_, id_) + + lesson_periods = LessonPeriod.objects + + if type_ and id_: + if isinstance(instance, HttpResponseNotFound): + return HttpResponseNotFound() + + type_ = TimetableType.from_string(type_) + + lesson_periods = lesson_periods.filter_from_type(type_, instance) + 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) + type_ = TimetableType.TEACHER + else: + lesson_periods = lesson_periods.filter_participant(request.user.person) + else: + lesson_periods = None + + + if year and week: + wanted_week = CalendarWeek(year=year, week=week) + else: + wanted_week = CalendarWeek() + + lesson_periods = lesson_periods.annotate( + has_documentation=Exists( + LessonDocumentation.objects.filter( + ~Q(topic__exact=""), lesson_period=OuterRef("pk"), week=wanted_week.week + ) + ) + ).in_week(wanted_week) # Add a form to filter the view if type_: @@ -136,9 +178,14 @@ def week_view( if lesson_periods: # Aggregate all personal notes for this group and week lesson_periods_pk = lesson_periods.values_list("pk", flat=True) - persons = ( - Person.objects.filter(is_active=True) - .filter(member_of__lessons__lesson_periods__in=lesson_periods_pk) + + + persons = Person.objects.filter(is_active=True) + + if not request.user.has_perm("alsijil.view_week_personalnote", instance): + persons = persons.filter(pk=request.user.pk) + + persons = (persons.filter(member_of__lessons__lesson_periods__in=lesson_periods_pk) .distinct() .prefetch_related("personal_notes") .annotate( @@ -195,7 +242,7 @@ def week_view( return render(request, "alsijil/week_view.html", context) -@permission_required("alsijil.full_register_group", fn=objectgetter_optional(Group, None, False)) +@permission_required("alsijil.view_full_register", fn=objectgetter_optional(Group, None, False)) def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: context = {} @@ -270,7 +317,7 @@ def register_absence(request: HttpRequest) -> HttpResponse: 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"] @@ -295,7 +342,7 @@ def register_absence(request: HttpRequest) -> HttpResponse: return render(request, "alsijil/register_absence.html", context) -@permission_required("alsijil.list_personal_note_filters") +@permission_required("alsijil.view_personal_note_filters") def list_personal_note_filters(request: HttpRequest) -> HttpResponse: context = {}