diff --git a/aleksis/apps/chronos/menus.py b/aleksis/apps/chronos/menus.py index 879cfa9adb7e667fd3f1c7c9b144d0532ecce529..592e6df90319d6165615e8ab3551101ce6e0fbbd 100644 --- a/aleksis/apps/chronos/menus.py +++ b/aleksis/apps/chronos/menus.py @@ -16,25 +16,33 @@ MENUS = { "name": _("My timetable"), "url": "my_timetable", "icon": "person", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ("aleksis.core.util.predicates.permission_validator", "chronos.view_my_timetable"), + ], }, { "name": _("All timetables"), "url": "all_timetables", "icon": "grid_on", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ("aleksis.core.util.predicates.permission_validator", "chronos.view_timetable_overview"), + ], }, { "name": _("Daily lessons"), "url": "lessons_day", "icon": "calendar_today", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ("aleksis.core.util.predicates.permission_validator", "chronos.view_lessons_day"), + ], }, { "name": _("Substitutions"), "url": "substitutions", "icon": "update", - "validators": ["menu_generator.validators.is_authenticated"], + "validators": [ + ("aleksis.core.util.predicates.permission_validator", "chronos.view_substitutions"), + ], }, ], } diff --git a/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py b/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py new file mode 100644 index 0000000000000000000000000000000000000000..785e2782c95b53321ee9940f289564ad2b495065 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.5 on 2020-04-30 19:03 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0015_rename_abbrev_to_short_name'), + ] + + operations = [ + migrations.CreateModel( + name='GlobalPermissions', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), + ], + options={ + 'permissions': (('view_all_timetables', 'Can view all timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day')), + 'managed': False, + }, + ), + ] diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index dc84b5cf90d3c0a424d8415200a35f82a348c3d9..bf5d2fe72241c692aaf9aff935e603b7b5b1d2c1 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -1111,3 +1111,13 @@ class ExtraLesson(ExtensibleModel, GroupPropertiesMixin): class Meta: verbose_name = _("Extra lesson") verbose_name_plural = _("Extra lessons") + + +class GlobalPermissions(ExtensibleModel): + class Meta: + managed = False + permissions = ( + ("view_all_timetables", _("Can view all timetables")), + ("view_timetable_overview", _("Can view timetable overview")), + ("view_lessons_day", _("Can view all lessons per day")), + ) diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py new file mode 100644 index 0000000000000000000000000000000000000000..ddc398cde95665f12e47e4dfe52f5f7c88d4177a --- /dev/null +++ b/aleksis/apps/chronos/rules.py @@ -0,0 +1,47 @@ +from rules import add_perm, always_allow + +from aleksis.core.util.predicates import ( + has_person, + has_global_perm, + has_any_object, + has_object_perm, +) +from .models import LessonSubstitution +from .util.predicates import ( + has_timetable_perm +) + +# View timetable overview +view_timetable_overview_predicate = has_person & has_global_perm("chronos.view_timetable_overview") +add_perm("chronos.view_timetable_overview", view_timetable_overview_predicate) + +# View my timetable +add_perm("chronos.view_my_timetable", has_person) + +# View timetable +view_timetable_predicate = has_person & ( + has_global_perm("chronos.view_all_timetables") | has_timetable_perm +) +add_perm("chronos.view_timetable", view_timetable_predicate) + +# View all lessons per day +view_lessons_day_predicate = has_person & has_global_perm("chronos.view_lessons_day") +add_perm("chronos.view_lessons_day", view_lessons_day_predicate) + +# Edit substition +edit_substitution_predicate = has_person & ( + has_global_perm("chronos.change_lessonsubstitution") | has_object_perm("chronos.change_lessonsubstitution") +) +add_perm("chronos.edit_substitution", edit_substitution_predicate) + +# Delete substitution +delete_substitution_predicate = has_person & ( + has_global_perm("chronos.delete_lessonsubstitution") | has_object_perm("chronos.delete_lessonsubstitution") +) +add_perm("chronos.delete_substitution", delete_substitution_predicate) + +# View substitutions +view_substitutions_predicate = has_person & ( + has_global_perm("chronos.view_lessonsubstitution") | has_any_object("chronos.view_lessonsubstitution", LessonSubstitution) +) +add_perm("chronos.view_substitutions", view_substitutions_predicate) diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py new file mode 100644 index 0000000000000000000000000000000000000000..11b5d7411208a84361baf9bab63d97353541392d --- /dev/null +++ b/aleksis/apps/chronos/util/predicates.py @@ -0,0 +1,21 @@ +from django.contrib.auth.models import User +from django.db.models import Model + +from rules import predicate + +from aleksis.core.models import Group, Person +from aleksis.apps.chronos.models import Room + + +@predicate +def has_timetable_perm(user: User, obj: Model) -> bool: + """ Predicate which checks whether the user is allowed to access the requested timetable """ + + if obj.model is Group: + return obj in user.person.member_of + elif obj.model is Person: + return user.person == obj + elif obj.model is Room: + return True + else: + return False diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py index 9ebe7de9b927262a372ec68c581c9eef45eb693b..ef128e754e4ebb32e7ed7ef23bf42cd1b4b924fd 100644 --- a/aleksis/apps/chronos/views.py +++ b/aleksis/apps/chronos/views.py @@ -11,8 +11,8 @@ from django.urls import reverse from django.utils import timezone from django.utils.translation import ugettext as _ from django_tables2 import RequestConfig +from rules.contrib.views import permission_required -from aleksis.core.decorators import admin_required from aleksis.core.models import Person, Group, Announcement from aleksis.core.util import messages from .forms import LessonSubstitutionForm @@ -24,7 +24,7 @@ from .util.date import CalendarWeek, get_weeks_for_year from aleksis.core.util.core_helpers import has_person -@login_required +@permission_required("chronos.view_timetable_overview") def all_timetables(request: HttpRequest) -> HttpResponse: context = {} @@ -49,7 +49,7 @@ def all_timetables(request: HttpRequest) -> HttpResponse: return render(request, "chronos/all.html", context) -@login_required +@permission_required("chronos.view_my_timetable") def my_timetable( request: HttpRequest, year: Optional[int] = None, @@ -95,7 +95,25 @@ def my_timetable( return redirect("all_timetables") -@login_required +def get_el_by_pk( + request: HttpRequest, + type_: str, + pk: int, + year: Optional[int] = None, + week: Optional[int] = None, + regular: Optional[str] = None, +): + if type_ == TimetableType.GROUP.value: + return get_object_or_404(Group, pk=pk) + elif type_ == TimetableType.TEACHER.value: + return get_object_or_404(Person, pk=pk) + elif type_ == TimetableType.ROOM.value: + return get_object_or_404(Room, pk=pk) + else: + return HttpResponseNotFound() + + +@permission_required("chronos.view_timetable", fn=get_el_by_pk) def timetable( request: HttpRequest, type_: str, @@ -108,14 +126,7 @@ def timetable( is_smart = regular != "regular" - if type_ == TimetableType.GROUP.value: - el = get_object_or_404(Group, pk=pk) - elif type_ == TimetableType.TEACHER.value: - el = get_object_or_404(Person, pk=pk) - elif type_ == TimetableType.ROOM.value: - el = get_object_or_404(Room, pk=pk) - else: - return HttpResponseNotFound() + el = get_el_by_pk(request, type_, pk) type_ = TimetableType.from_string(type_) @@ -165,7 +176,7 @@ def timetable( return render(request, "chronos/timetable.html", context) -@login_required +@permission_required("chronos.view_lessons_day") def lessons_day( request: HttpRequest, year: Optional[int] = None, @@ -203,16 +214,24 @@ def lessons_day( return render(request, "chronos/lessons_day.html", context) -@admin_required +def get_substitution_by_id(request: HttpRequest, id_: int, week: int): + lesson_period = get_object_or_404(LessonPeriod, pk=id_) + wanted_week = lesson_period.lesson.get_calendar_week(week) + + return LessonSubstitution.objects.filter( + week=wanted_week.week, lesson_period=lesson_period + ).first() + + +@permission_required("chronos.edit_substitution", fn=get_substitution_by_id) def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse: context = {} lesson_period = get_object_or_404(LessonPeriod, pk=id_) wanted_week = lesson_period.lesson.get_calendar_week(week) - lesson_substitution = LessonSubstitution.objects.filter( - week=wanted_week.week, lesson_period=lesson_period - ).first() + lesson_substitution = get_substitution_by_id(request, id_, week) + if lesson_substitution: edit_substitution_form = LessonSubstitutionForm( request.POST or None, instance=lesson_substitution @@ -242,14 +261,12 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse return render(request, "chronos/edit_substitution.html", context) -@admin_required +@permission_required("chronos.delete_substitution", fn=get_substitution_by_id) def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse: lesson_period = get_object_or_404(LessonPeriod, pk=id_) wanted_week = lesson_period.lesson.get_calendar_week(week) - LessonSubstitution.objects.filter( - week=wanted_week.week, lesson_period=lesson_period - ).delete() + get_substitution_by_id(request, id_, week).delete() messages.success(request, _("The substitution has been deleted.")) @@ -260,7 +277,7 @@ def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpRespon ) -@login_required +@permission_required("chronos.view_substitutions") def substitutions( request: HttpRequest, year: Optional[int] = None,