From 6e33863546708df8c756396cfbd00bdbf3fb6ea4 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Sun, 14 Jun 2020 12:59:39 +0200
Subject: [PATCH] Add rules and permissions for some views

---
 aleksis/apps/alsijil/models.py               |  9 +++
 aleksis/apps/alsijil/rules.py                | 46 ++++++++++++
 aleksis/apps/alsijil/util/alsijil_helpers.py | 73 ++++++++++++++++++++
 aleksis/apps/alsijil/util/predicates.py      | 33 +++++++++
 aleksis/apps/alsijil/views.py                | 62 +++++------------
 5 files changed, 179 insertions(+), 44 deletions(-)
 create mode 100644 aleksis/apps/alsijil/rules.py
 create mode 100644 aleksis/apps/alsijil/util/alsijil_helpers.py
 create mode 100644 aleksis/apps/alsijil/util/predicates.py

diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 3867f6c5a..1a32039e1 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -81,3 +81,12 @@ class PersonalNoteFilter(ExtensibleModel):
         verbose_name = _("Personal note filter")
         verbose_name_plural = _("Personal note filters")
         ordering = ["identifier"]
+
+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/rules.py b/aleksis/apps/alsijil/rules.py
new file mode 100644
index 000000000..c6c6baf84
--- /dev/null
+++ b/aleksis/apps/alsijil/rules.py
@@ -0,0 +1,46 @@
+from rules import add_perm
+
+from aleksis.core.util.predicates import (
+    has_any_object,
+    has_global_perm,
+    has_object_perm,
+    has_person,
+)
+
+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")
+)
+add_perm("alsijil.view_lesson", view_lesson_predicate)
+
+# View week overview
+view_week_predicate = has_person & (
+    has_global_perm("alsijil.view_week") | has_week_perm("alsijil")
+)
+add_perm("alsijil.view_week", view_week_predicate)
+
+# Register absence
+register_absence_predicate = has_person & (
+    has_global_perm("alsijil.register_absence")
+)
+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)
+
+# Edit personal note filter
+edit_personal_note_filter_predicate = has_person & (
+    has_global_perm("alsijil.change_personal_note_filter")
+    | has_object_perm("alsijil.change_personal_note_filter")
+)
+add_perm("alsijil.edit_personal_note_filter", edit_personal_note_filter_predicate)
+
+# Delete personal note filter
+delete_personal_note_filter_predicate = has_person & (
+    has_global_perm("alsijil.delete_personal_note_filter")
+    | has_object_perm("alsijil.delete_personal_note_filter")
+)
+add_perm("alsijil.delete_personal_note_filter", delete_personal_note_filter_predicate)
diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py
new file mode 100644
index 000000000..ec27bcb8b
--- /dev/null
+++ b/aleksis/apps/alsijil/util/alsijil_helpers.py
@@ -0,0 +1,73 @@
+from typing import Optional
+
+from django.db.models import Count, Exists, OuterRef, Q, Sum
+from django.http import HttpRequest, HttpResponseNotFound
+from django.shortcuts import get_object_or_404
+
+from calendarweek import CalendarWeek
+
+from aleksis.apps.chronos.managers import TimetableType
+from aleksis.apps.chronos.models import LessonPeriod
+from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
+from ..models import LessonDocumentation
+
+
+def get_lesson_period_by_pk(
+    request: HttpRequest,
+    year: Optional[int] = None,
+    week: Optional[int] = None,
+    period_id: Optional[int] = None,
+):
+    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
+
+
+def get_lesson_periods_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)
+    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
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
new file mode 100644
index 000000000..94c69e0fe
--- /dev/null
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -0,0 +1,33 @@
+from django.contrib.auth.models import User
+
+from rules import predicate
+
+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(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))):
+            return True
+        return check_object_permission(user, perm, obj)
+
+    return fn
+
+
+@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}"
+
+    @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))):
+            return True
+        return check_object_permission(user, perm, obj)
+
+    return fn
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 7b594f25b..da8d97642 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -12,11 +12,12 @@ from django.utils.translation import ugettext as _
 
 from calendarweek import CalendarWeek
 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.util import messages
-
+from aleksis.core.util.core_helpers import objectgetter_optional
 from .forms import (
     LessonDocumentationForm,
     PersonalNoteFilterForm,
@@ -26,8 +27,10 @@ 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
 
 
+@permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk)
 def lesson(
     request: HttpRequest,
     year: Optional[int] = None,
@@ -36,15 +39,9 @@ def lesson(
 ) -> HttpResponse:
     context = {}
 
-    if year and week and period_id:
-        # Get a specific lesson period if provided in URL
-        lesson_period = LessonPeriod.objects.get(pk=period_id)
-        wanted_week = CalendarWeek(year=year, week=week)
-    else:
-        # Determine current lesson by current date and time
-        lesson_period = LessonPeriod.objects.at_time().filter_teacher(request.user.person).first()
-        wanted_week = CalendarWeek()
+    lesson_period, wanted_week = get_lesson_period_by_pk(request, year, week, period_id)
 
+    if not (year and week and period_id):
         if lesson_period:
             return redirect(
                 "lesson_by_week_and_period", wanted_week.year, wanted_week.week, lesson_period.pk,
@@ -108,46 +105,13 @@ def lesson(
     return render(request, "alsijil/lesson.html", context)
 
 
+@permission_required("alsijil.view_week", fn=get_lesson_periods_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 = {}
 
-    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)
-
-    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
-        else:
-            lesson_periods = lesson_periods.filter_participant(request.user.person)
-    else:
-        lesson_periods = None
+    lesson_periods, wanted_week, type_, instance = get_lesson_periods_by_pk(request, year, week, type_, id_)
 
     # Add a form to filter the view
     if type_:
@@ -164,6 +128,11 @@ def week_view(
                 return redirect("week_view_by_week", wanted_week.year, wanted_week.week,
                                 select_form.cleaned_data["type_"].value, 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 = lesson_periods.values_list("pk", flat=True)
@@ -226,6 +195,7 @@ def week_view(
     return render(request, "alsijil/week_view.html", context)
 
 
+@permission_required("alsijil.full_register_group", fn=objectgetter_optional(Group, None, False))
 def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
     context = {}
 
@@ -293,6 +263,7 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
     return render(request, "alsijil/print/full_register.html", context)
 
 
+@permission_required("alsijil.register_absence")
 def register_absence(request: HttpRequest) -> HttpResponse:
     context = {}
 
@@ -324,6 +295,7 @@ def register_absence(request: HttpRequest) -> HttpResponse:
     return render(request, "alsijil/register_absence.html", context)
 
 
+@permission_required("alsijil.list_personal_note_filters")
 def list_personal_note_filters(request: HttpRequest) -> HttpResponse:
     context = {}
 
@@ -338,6 +310,7 @@ def list_personal_note_filters(request: HttpRequest) -> HttpResponse:
     return render(request, "alsijil/personal_note_filters.html", context)
 
 
+@permission_required("alsijil.edit_personal_note_filter", fn=objectgetter_optional(PersonalNoteFilter, None, False))
 def edit_personal_note_filter(request: HttpRequest, id: Optional["int"] = None) -> HttpResponse:
     context = {}
 
@@ -362,6 +335,7 @@ def edit_personal_note_filter(request: HttpRequest, id: Optional["int"] = None)
     return render(request, "alsijil/manage_personal_note_filter.html", context)
 
 
+@permission_required("alsijil.delete_personal_note_filter", fn=objectgetter_optional(PersonalNoteFilter, None, False))
 def delete_personal_note_filter(request: HttpRequest, id_: int) -> HttpResponse:
     context = {}
 
-- 
GitLab