Skip to content

Commit

Permalink
chore: finished course logic with sensible permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
EwoutV committed Mar 8, 2024
1 parent b2aba5e commit fe9c170
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 70 deletions.
13 changes: 9 additions & 4 deletions backend/api/models/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,20 @@ def __str__(self) -> str:
"""The string representation of the course."""
return str(self.name)

def clone(self, year=None) -> Self:
# To-do: add more control over the cloning process.
return Course(
def clone(self, clone_assistants=True) -> Self:
"""Clone the course to the next academic start year"""
course = Course(
name=self.name,
description=self.description,
academic_startyear=year or self.academic_startyear + 1,
academic_startyear=self.academic_startyear + 1,
parent_course=self
)

if clone_assistants:
course.assistants.add(self.assistants)

return course

@property
def academic_year(self) -> str:
"""The academic year of the course."""
Expand Down
60 changes: 43 additions & 17 deletions backend/api/permissions/course_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
from authentication.models import User
from api.models.teacher import Teacher
from api.models.assistant import Assistant
from api.permissions.role_permissions import is_student, is_assistant, is_teacher
from api.models.course import Course


Expand All @@ -13,37 +12,64 @@ def has_permission(self, request: Request, view: ViewSet) -> bool:
"""Check if user has permission to view a general course endpoint."""
user: User = request.user

# Logged-in users can fetch course information.
if request.method in SAFE_METHODS:
# Logged-in users can fetch course lists.
return request.user.is_authenticated

# We only allow teachers to create new courses.
return hasattr(user, "teacher") and user.teacher.exists()
# Only teachers can create courses.
return is_teacher(user)

def has_object_permission(self, request: Request, view: ViewSet, course: Course) -> bool:
"""Check if user has permission to view a detailed course endpoint"""
user: User = request.user

# Logged-in users can fetch course details.
if request.method in SAFE_METHODS:
# Logged-in users can fetch course details.
return user.is_authenticated

# We only allow teachers and assistants to modify specified courses.
role: Teacher | Assistant = hasattr(user, "teacher") and user.teacher or \
hasattr(user, "assistant") and user.assistant
# We only allow teachers and assistants to modify their own courses.
return is_teacher(user) and user.teacher.courses.contains(course) or \
is_assistant(user) and user.assistant.courses.contains(course)

return role and \
role.courses.filter(id=course.id).exists()


class CourseTeacherPermission(CoursePermission):
"""Permission class for teacher-only course endpoints."""
class CourseAssistantPermission(CoursePermission):
"""Permission class for assistant related endpoints."""
def has_object_permission(self, request: Request, view: ViewSet, course: Course) -> bool:
user: User = request.user

# Logged-in users can fetch course assistants.
if request.method in SAFE_METHODS:
return user.is_authenticated

# Only teachers can modify assistants of their own courses.
return is_teacher(user) and user.teacher.courses.contains(course)


class CourseStudentPermission(CoursePermission):
"""Permission class for student related endpoints."""
def has_object_permission(self, request: Request, view: ViewSet, course: Course):
user: User = request.user

# Logged-in users can fetch course students.
if request.method in SAFE_METHODS:
return user.is_authenticated

# Only students can add or remove themselves from a course.
if is_student(user) and request.data.get("id") == user.id:
return True

# Teachers and assistants can add and remove any student.
return super().has_object_permission(request, view, course)


class CourseProjectPermission(CoursePermission):
"""Permission class for project related endpoints."""
def has_object_permission(self, request: Request, view: ViewSet, course: Course):
user: User = request.user

# Logged-in users can fetch course projects.
if request.method in SAFE_METHODS:
# Logged-in users can still fetch course details.
return user.is_authenticated

return hasattr(user, "teacher") and \
user.teacher.courses.filter(id=course.id).exists()
# Teachers and assistants can modify projects.
return super().has_object_permission(request, view, course)
24 changes: 16 additions & 8 deletions backend/api/permissions/role_permissions.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from authentication.models import User
from api.models.student import Student
from api.models.assistant import Assistant
from api.models.teacher import Teacher


def is_student(user: User):
return Student.objects.filter(id=user.id).exists()

def is_assistant(user: User):
return Assistant.objects.filter(id=user.id).exists()

def is_teacher(user: User):
return Teacher.objects.filter(id=user.id).exists()

class IsStudent(IsAuthenticated):
def has_permission(self, request, view):
def has_permission(self, request: Request, view):
"""Returns true if the request contains a user,
with said user being a student"""
return super().has_permission(request, view) and \
Student.objects.filter(id=request.user.id).exists()
return super().has_permission(request, view) and is_student(request.user)


class IsTeacher(IsAuthenticated):
def has_permission(self, request, view):
def has_permission(self, request: Request, view):
"""Returns true if the request contains a user,
with said user being a student"""
return super().has_permission(request, view) and \
Teacher.objects.filter(id=request.user.id).exists()
return super().has_permission(request, view) and is_teacher(request.user)


class IsAssistant(IsAuthenticated):
def has_permission(self, request, view):
"""Returns true if the request contains a user,
with said user being a student"""
return super().has_permission(request, view) and \
Assistant.objects.filter(id=request.user.id).exists()
return super().has_permission(request, view) and is_assistant(request.user)
107 changes: 66 additions & 41 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from django.utils.translation import gettext
from rest_framework import viewsets
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.permissions import IsAdminUser
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.request import Request
from api.models.course import Course
from api.models.assistant import Assistant
from api.permissions.course_permissions import CoursePermission, CourseTeacherPermission
from api.permissions.role_permissions import IsStudent
from api.models.student import Student
from api.permissions.course_permissions import CoursePermission, CourseAssistantPermission, CourseStudentPermission
from api.permissions.role_permissions import IsTeacher
from api.serializers.course_serializer import CourseSerializer
from api.serializers.teacher_serializer import TeacherSerializer
from api.serializers.assistant_serializer import AssistantSerializer
Expand All @@ -22,7 +23,7 @@ class CourseViewSet(viewsets.ModelViewSet):
serializer_class = CourseSerializer
permission_classes = [IsAdminUser | CoursePermission]

@action(detail=True, permission_classes=[IsAdminUser | CourseTeacherPermission])
@action(detail=True, permission_classes=[IsAdminUser | CourseAssistantPermission])
def assistants(self, request: Request, **_):
"""Returns a list of assistants for the given course"""
course = self.get_object()
Expand Down Expand Up @@ -76,22 +77,8 @@ def _remove_assistant(self, request: Request, **_):
# Not found
raise NotFound(gettext("assistants.error.404"))

@action(detail=True, methods=["get"])
def teachers(self, request, **_):
"""Returns a list of teachers for the given course"""
# This automatically fetches the course from the URL.
# It automatically gives back a 404 HTTP response in case of not found.
course = self.get_object()
teachers = course.teachers.all()

# Serialize the teacher objects
serializer = TeacherSerializer(
teachers, many=True, context={"request": request}
)

return Response(serializer.data)

@action(detail=True, methods=["get"])
@action(detail=True, methods=["get"], permission_classes=[IsAdminUser | CourseStudentPermission])
def students(self, request, **_):
"""Returns a list of students for the given course"""
course = self.get_object()
Expand All @@ -104,6 +91,58 @@ def students(self, request, **_):

return Response(serializer.data)

@students.mapping.post
@students.mapping.put
def _add_student(self, request: Request, **_):
"""Add a student to the course"""
course = self.get_object()

try:
# Add student to course
student = Student.objects.get(
id=request.data.get("id")
)

course.students.add(student)

return Response({
"message": gettext("courses.success.students.add")
})
except Student.DoesNotExist:
raise NotFound(gettext("students.error.404"))

@students.mapping.delete
def _remove_student(self, request: Request, **_):
"""Remove a student from the course"""
course = self.get_object()

try:
# Add student to course
student = Student.objects.get(
id=request.data.get("id")
)

course.students.remove(student)

return Response({
"message": gettext("courses.success.students.remove")
})
except Student.DoesNotExist:
raise NotFound(gettext("students.error.404"))

@action(detail=True, methods=["get"])
def teachers(self, request, **_):
"""Returns a list of teachers for the given course"""
course = self.get_object()
teachers = course.teachers.all()

# Serialize the teacher objects
serializer = TeacherSerializer(
teachers, many=True, context={"request": request}
)

return Response(serializer.data)

@action(detail=True, methods=["get"])
def projects(self, request, **_):
"""Returns a list of projects for the given course"""
Expand All @@ -117,35 +156,21 @@ def projects(self, request, **_):

return Response(serializer.data)

@action(detail=True, methods=["post"], permission_classes=[IsStudent])
def join(self, request, **_):
"""Enrolls the authenticated student in the project"""
# Add the course to the student's enrollment list.
self.get_object().students.add(
request.user.student
)

return Response({
"message": gettext("courses.success.join")
})

@action(detail=True, methods=["post"], permission_classes=[IsAdminUser | CourseTeacherPermission])
@action(detail=True, methods=["post"], permission_classes=[IsAdminUser | IsTeacher])
def clone(self, request: Request, **__):
"""Copy the course to a new course with the same fields"""
course: Course = self.get_object()

try:
course_serializer = CourseSerializer(
course.child_course, context={"request": request}
)
course = course.child_course
except Course.DoesNotExist:
course_serializer = CourseSerializer(
course.clone(
year=request.data.get("academic_startyear")
),
context={"request": request}
course = course.clone(
clone_assistants=request.data.get("clone_assistants")
)

course_serializer.save()
course.save()

# Return serialized cloned course
course_serializer = CourseSerializer(course, context={"request": request})

return Response(course_serializer.data)

0 comments on commit fe9c170

Please sign in to comment.