diff --git a/codeforlife/user/permissions/__init__.py b/codeforlife/user/permissions/__init__.py index cb7689b5..66153640 100644 --- a/codeforlife/user/permissions/__init__.py +++ b/codeforlife/user/permissions/__init__.py @@ -1,2 +1,10 @@ -from .is_school_member import IsSchoolMember -from .is_school_teacher import IsSchoolTeacher +""" +© Ocado Group +Created on 14/12/2023 at 14:05:06(+00:00). +""" + +from .in_class import InClass +from .in_school import InSchool +from .is_independent import IsIndependent +from .is_student import IsStudent +from .is_teacher import IsTeacher diff --git a/codeforlife/user/permissions/in_class.py b/codeforlife/user/permissions/in_class.py new file mode 100644 index 00000000..c030e6d0 --- /dev/null +++ b/codeforlife/user/permissions/in_class.py @@ -0,0 +1,46 @@ +""" +© Ocado Group +Created on 12/12/2023 at 15:18:10(+00:00). +""" + +import typing as t + +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class InClass(IsAuthenticated): + """Request's user must be in a class.""" + + def __init__(self, class_id: t.Optional[str] = None): + """Initialize permission. + + Args: + class_id: A class' ID. If None, check if user is in any class. + Else, check if user is in the specific class. + """ + + super().__init__() + self.class_id = class_id + + def has_permission(self, request: Request, view: APIView): + user = request.user + if super().has_permission(request, view) and isinstance(user, User): + if user.teacher is not None: + classes = user.teacher.class_teacher + if self.class_id is not None: + classes = classes.filter(access_code=self.class_id) + return classes.exists() + + if user.student is not None: + if self.class_id is None: + return True + return ( + user.student.class_field is not None + and user.student.class_field.access_code == self.class_id + ) + + return False diff --git a/codeforlife/user/permissions/in_school.py b/codeforlife/user/permissions/in_school.py new file mode 100644 index 00000000..1b866d21 --- /dev/null +++ b/codeforlife/user/permissions/in_school.py @@ -0,0 +1,49 @@ +""" +© Ocado Group +Created on 12/12/2023 at 15:18:27(+00:00). +""" + +import typing as t + +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class InSchool(IsAuthenticated): + """Request's user must be in a school.""" + + def __init__(self, school_id: t.Optional[int] = None): + """Initialize permission. + + Args: + school_id: A school's ID. If None, check if user is in any school. + Else, check if user is in the specific school. + """ + + super().__init__() + self.school_id = school_id + + def has_permission(self, request: Request, view: APIView): + def in_school(school_id: int): + return self.school_id is None or self.school_id == school_id + + user = request.user + return ( + super().has_permission(request, view) + and isinstance(user, User) + and ( + ( + user.teacher is not None + and user.teacher.school_id is not None + and in_school(user.teacher.school_id) + ) + or ( + user.student is not None + and user.student.class_field is not None + and in_school(user.student.class_field.teacher.school_id) + ) + ) + ) diff --git a/codeforlife/user/permissions/is_independent.py b/codeforlife/user/permissions/is_independent.py new file mode 100644 index 00000000..5d0f5a8e --- /dev/null +++ b/codeforlife/user/permissions/is_independent.py @@ -0,0 +1,23 @@ +""" +© Ocado Group +Created on 12/12/2023 at 13:55:47(+00:00). +""" + +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class IsIndependent(IsAuthenticated): + """Request's user must be independent.""" + + def has_permission(self, request: Request, view: APIView): + user = request.user + return ( + super().has_permission(request, view) + and isinstance(user, User) + and user.teacher is None + and user.student is None + ) diff --git a/codeforlife/user/permissions/is_school_member.py b/codeforlife/user/permissions/is_school_member.py deleted file mode 100644 index 43894cdf..00000000 --- a/codeforlife/user/permissions/is_school_member.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework.permissions import BasePermission -from rest_framework.request import Request -from rest_framework.views import View - -from ..models import User - - -class IsSchoolMember(BasePermission): - def has_permission(self, request: Request, view: View): - user = request.user - return isinstance(user, User) and ( - (user.is_teacher and user.teacher.school is not None) - or ( - user.student is not None - # TODO: should be user.student.school is not None - and user.student.class_field is not None - ) - ) diff --git a/codeforlife/user/permissions/is_school_teacher.py b/codeforlife/user/permissions/is_school_teacher.py deleted file mode 100644 index ece94675..00000000 --- a/codeforlife/user/permissions/is_school_teacher.py +++ /dev/null @@ -1,15 +0,0 @@ -from rest_framework.permissions import BasePermission -from rest_framework.request import Request -from rest_framework.views import View - -from ..models import User - - -class IsSchoolTeacher(BasePermission): - def has_permission(self, request: Request, view: View): - user = request.user - return ( - isinstance(user, User) - and user.is_teacher - and user.teacher.school is not None - ) diff --git a/codeforlife/user/permissions/is_student.py b/codeforlife/user/permissions/is_student.py new file mode 100644 index 00000000..a4a43e9e --- /dev/null +++ b/codeforlife/user/permissions/is_student.py @@ -0,0 +1,36 @@ +""" +© Ocado Group +Created on 12/12/2023 at 13:55:40(+00:00). +""" + +import typing as t + +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class IsStudent(IsAuthenticated): + """Request's user must be a student.""" + + def __init__(self, student_id: t.Optional[int] = None): + """Initialize permission. + + Args: + student_id: A student's ID. If None, check if the user is any + student. Else, check if the user is the specific student. + """ + + super().__init__() + self.student_id = student_id + + def has_permission(self, request: Request, view: APIView): + user = request.user + return ( + super().has_permission(request, view) + and isinstance(user, User) + and user.student is not None + and (self.student_id is None or user.student.id == self.student_id) + ) diff --git a/codeforlife/user/permissions/is_teacher.py b/codeforlife/user/permissions/is_teacher.py new file mode 100644 index 00000000..255d9c6d --- /dev/null +++ b/codeforlife/user/permissions/is_teacher.py @@ -0,0 +1,47 @@ +""" +© Ocado Group +Created on 12/12/2023 at 13:55:22(+00:00). +""" + +import typing as t + +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.views import APIView + +from ..models import User + + +class IsTeacher(IsAuthenticated): + """Request's user must be a teacher.""" + + def __init__( + self, + teacher_id: t.Optional[int] = None, + is_admin: t.Optional[bool] = None, + ): + """Initialize permission. + + Args: + teacher_id: A teacher's ID. If None, check if the user is any + teacher. Else, check if the user is the specific teacher. + is_admin: If the teacher is an admin. If None, don't check if the + teacher is an admin. Else, check if the teacher is (not) an + admin. + """ + + super().__init__() + self.teacher_id = teacher_id + self.is_admin = is_admin + + def has_permission(self, request: Request, view: APIView): + user = request.user + return ( + super().has_permission(request, view) + and isinstance(user, User) + and user.teacher is not None + and (self.teacher_id is None or user.teacher.id == self.teacher_id) + and ( + self.is_admin is None or user.teacher.is_admin == self.is_admin + ) + ) diff --git a/codeforlife/user/tests/views/test_school.py b/codeforlife/user/tests/views/test_school.py index 4b400521..788b5570 100644 --- a/codeforlife/user/tests/views/test_school.py +++ b/codeforlife/user/tests/views/test_school.py @@ -4,7 +4,7 @@ """ from rest_framework import status -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import InSc from ....tests import ModelViewSetTestCase from ...models import Class, School, Student, Teacher, User, UserProfile @@ -194,24 +194,3 @@ def test_list__student(self): user = self._login_student() self.client.list([user.student.class_field.teacher.school]) - - # pylint: disable-next=pointless-string-statement - """ - General tests that apply to all actions. - """ - - def test_all__requires_authentication(self): - """ - User must be authenticated to call any endpoint. - """ - - assert IsAuthenticated in SchoolViewSet.permission_classes - - def test_all__only_http_get(self): - """ - These model are read-only. - """ - - assert [name.lower() for name in SchoolViewSet.http_method_names] == [ - "get" - ] diff --git a/codeforlife/user/views/klass.py b/codeforlife/user/views/klass.py index c803729b..e4d5ff06 100644 --- a/codeforlife/user/views/klass.py +++ b/codeforlife/user/views/klass.py @@ -5,20 +5,18 @@ import typing as t -from rest_framework.permissions import IsAuthenticated - from ...views import ModelViewSet from ..models import Class, User -from ..permissions import IsSchoolMember +from ..permissions import InSchool from ..serializers import ClassSerializer -# pylint: disable-next=missing-class-docstring,too-few-public-methods +# pylint: disable-next=missing-class-docstring,too-many-ancestors class ClassViewSet(ModelViewSet[Class]): http_method_names = ["get"] lookup_field = "access_code" serializer_class = ClassSerializer - permission_classes = [IsAuthenticated, IsSchoolMember] + permission_classes = [InSchool] # pylint: disable-next=missing-function-docstring def get_queryset(self): diff --git a/codeforlife/user/views/school.py b/codeforlife/user/views/school.py index 099e7bfa..cc3b903c 100644 --- a/codeforlife/user/views/school.py +++ b/codeforlife/user/views/school.py @@ -5,19 +5,17 @@ import typing as t -from rest_framework.permissions import IsAuthenticated - from ...views import ModelViewSet from ..models import School, User -from ..permissions import IsSchoolMember +from ..permissions import InSchool from ..serializers import SchoolSerializer -# pylint: disable-next=missing-class-docstring,too-few-public-methods +# pylint: disable-next=missing-class-docstring,too-many-ancestors class SchoolViewSet(ModelViewSet[School]): http_method_names = ["get"] serializer_class = SchoolSerializer - permission_classes = [IsAuthenticated, IsSchoolMember] + permission_classes = [InSchool] # pylint: disable-next=missing-function-docstring def get_queryset(self): diff --git a/codeforlife/user/views/user.py b/codeforlife/user/views/user.py index 39c18e59..3942b709 100644 --- a/codeforlife/user/views/user.py +++ b/codeforlife/user/views/user.py @@ -11,7 +11,7 @@ from ..serializers import UserSerializer -# pylint: disable-next=missing-class-docstring,too-few-public-methods +# pylint: disable-next=missing-class-docstring,too-many-ancestors class UserViewSet(ModelViewSet[User]): http_method_names = ["get"] serializer_class = UserSerializer