diff --git a/backend/api/permissions/course_permissions.py b/backend/api/permissions/course_permissions.py index afdfa71b..e2f7d987 100644 --- a/backend/api/permissions/course_permissions.py +++ b/backend/api/permissions/course_permissions.py @@ -8,6 +8,7 @@ class CoursePermission(BasePermission): + """Permission class used as default policy for course endpoints.""" def has_permission(self, request: Request, view: ViewSet) -> bool: """Check if user has permission to view a general course endpoint.""" user: User = request.user @@ -31,4 +32,16 @@ def has_object_permission(self, request: Request, view: ViewSet, course: Course) role: Teacher|Assistant = user.teacher or user.assistant return role is not None and \ - role.courses.filter(id=course.id).exists() \ No newline at end of file + role.courses.filter(id=course.id).exists() + +class CourseTeacherPermission(CoursePermission): + """Permission class for teacher-only course endpoints.""" + def has_object_permission(self, request: Request, view: ViewSet, course: Course) -> bool: + user: User = request.user + + if request.method in SAFE_METHODS: + # Logged-in users can still fetch course details. + return request.user.is_authenticated + + return user.teacher.exists() and \ + user.teacher.courses.filter(id=course.id).exists() \ No newline at end of file diff --git a/backend/api/signals.py b/backend/api/signals.py index 85f94211..6395ea75 100644 --- a/backend/api/signals.py +++ b/backend/api/signals.py @@ -2,9 +2,9 @@ from api.models.student import Student -def user_creation(user: User, attributes: dict, **kwargs): +def user_creation(user: User, attributes: dict, **_): """Upon user creation, auto-populate additional properties""" - student_id = attributes.get("ugentStudentID") + student_id: str = attributes.get("ugentStudentID") - if student_id: + if student_id is not None: Student(user_ptr=user, student_id=student_id).save_base(raw=True) diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index 5d80662d..bf4320d2 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -1,12 +1,13 @@ from django.utils.translation import gettext -from rest_framework import viewsets, status +from rest_framework import viewsets +from rest_framework.exceptions import NotFound from rest_framework.permissions import IsAdminUser from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework.exceptions import NotFound -from api.models.student import Student +from rest_framework.request import Request from api.models.course import Course -from api.permissions.course_permissions import CoursePermission +from api.models.assistant import Assistant +from api.permissions.course_permissions import CoursePermission, CourseTeacherPermission from api.permissions.role_permissions import IsStudent from api.serializers.course_serializer import CourseSerializer from api.serializers.teacher_serializer import TeacherSerializer @@ -35,82 +36,81 @@ def teachers(self, request, **_): return Response(serializer.data) - @action(detail=True, methods=["get", "post"]) - def assistants(self, request, **_): - """Returns a list of assistants for the given course""" + @action(detail=True, methods=["get", "post", "delete"], permission_classes=[IsAdminUser | CourseTeacherPermission]) + def assistants(self, request: Request, **_) -> Response: + """Action for managing assistants associated to a 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() - assistants = course.assistants.all() if request.method == "GET": - # Serialize the assistant objects + # Return assistants of a course. + assistants = course.assistants.all() + serializer = AssistantSerializer( assistants, many=True, context={"request": request} ) return Response(serializer.data) - # Add a new assistant to the course, assistant ID in request.get("assistant_id") + try: + assistant = Assistant.objects.get( + id=request.query_params.get("id") + ) + + if request.method == "POST": + # Add a new assistant to the course. + course.assistants.add(assistant) + return Response({ + "message": gettext("courses.success.assistants.add") + }) + elif request.method == "DELETE": + # Remove an assistant from the course. + course.assistants.remove(assistant) + return Response({ + "message": gettext("courses.success.assistants.remove") + }) + except Assistant.DoesNotExist: + # Not found + raise NotFound(gettext("assistants.error.404")) @action(detail=True, methods=["get"]) - def students(self, request, pk=None): + def students(self, request, **_): """Returns a list of students for the given course""" course = self.get_object() + students = course.students.all() - try: - queryset = Course.objects.get(id=pk) - students = queryset.students.all() - - # Serialize the student objects - serializer = StudentSerializer( - students, many=True, context={"request": request} - ) - return Response(serializer.data) + # Serialize the student objects + serializer = StudentSerializer( + students, many=True, context={"request": request} + ) - except Course.DoesNotExist: - # Invalid course ID - raise NotFound(gettext("courses.errors.not_found")) + return Response(serializer.data) @action(detail=True, methods=["get"]) - def projects(self, request, pk=None): + def projects(self, request, **_): """Returns a list of projects for the given course""" + course = self.get_object() + projects = course.projects.all() - try: - queryset = Course.objects.get(id=pk) - projects = queryset.projects.all() - - # Serialize the project objects - serializer = ProjectSerializer( - projects, many=True, context={"request": request} - ) - return Response(serializer.data) + # Serialize the project objects + serializer = ProjectSerializer( + projects, many=True, context={"request": request} + ) - except Course.DoesNotExist: - # Invalid course ID - raise NotFound(gettext("courses.errors.not_found")) + return Response(serializer.data) @action(detail=True, methods=["post"], permission_classes=[IsStudent]) - def join(self, request, pk=None): + 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 + ) - try: - # Add the course to the student's enrollment list. - Student.objects.get(id=request.user.id).courses.add( - Course.objects.get(id=pk) - ) - - return Response({ - "message": gettext("courses.messages.successful_join") - }) - - except Course.DoesNotExist: - # Invalid course ID - raise NotFound(gettext("courses.errors.not_found")) - except Student.DoesNotExist: - # Invalid student user, this should not happen, - # since the IsStudent permission class already checks this. - raise NotFound(gettext("students.errors.not_found")) \ No newline at end of file + return Response({ + "message": gettext("courses.success.join") + }) \ No newline at end of file diff --git a/backend/authentication/models.py b/backend/authentication/models.py index dcf28b4d..066d6fbb 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -1,5 +1,4 @@ from datetime import MINYEAR -from typing import Self, Type from django.db import models from django.db.models import CharField, EmailField, IntegerField, DateTimeField, BooleanField, Model from django.contrib.auth.models import AbstractBaseUser, AbstractUser, PermissionsMixin