Skip to content

Commit

Permalink
add permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Jan 25, 2024
1 parent 195f1d9 commit 2b1be9c
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 68 deletions.
12 changes: 10 additions & 2 deletions codeforlife/user/permissions/__init__.py
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions codeforlife/user/permissions/in_class.py
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions codeforlife/user/permissions/in_school.py
Original file line number Diff line number Diff line change
@@ -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)
)
)
)
23 changes: 23 additions & 0 deletions codeforlife/user/permissions/is_independent.py
Original file line number Diff line number Diff line change
@@ -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
)
18 changes: 0 additions & 18 deletions codeforlife/user/permissions/is_school_member.py

This file was deleted.

15 changes: 0 additions & 15 deletions codeforlife/user/permissions/is_school_teacher.py

This file was deleted.

36 changes: 36 additions & 0 deletions codeforlife/user/permissions/is_student.py
Original file line number Diff line number Diff line change
@@ -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)
)
47 changes: 47 additions & 0 deletions codeforlife/user/permissions/is_teacher.py
Original file line number Diff line number Diff line change
@@ -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
)
)
23 changes: 1 addition & 22 deletions codeforlife/user/tests/views/test_school.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
]
8 changes: 3 additions & 5 deletions codeforlife/user/views/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
8 changes: 3 additions & 5 deletions codeforlife/user/views/school.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion codeforlife/user/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2b1be9c

Please sign in to comment.