diff --git a/backend/api/models/course.py b/backend/api/models/course.py index f4ec41f2..57ff10ff 100644 --- a/backend/api/models/course.py +++ b/backend/api/models/course.py @@ -1,3 +1,4 @@ +from typing import Self from django.db import models @@ -30,6 +31,15 @@ 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( + name=self.name, + description=self.description, + academic_startyear=year or self.academic_startyear + 1, + parent_course=self + ) + @property def academic_year(self) -> str: """The academic year of the course.""" diff --git a/backend/api/permissions/course_permissions.py b/backend/api/permissions/course_permissions.py index 46ee6f50..cabf379f 100644 --- a/backend/api/permissions/course_permissions.py +++ b/backend/api/permissions/course_permissions.py @@ -18,7 +18,7 @@ def has_permission(self, request: Request, view: ViewSet) -> bool: return request.user.is_authenticated # We only allow teachers to create new courses. - return user.teacher.exists() + return hasattr(user, "teacher") and user.teacher.exists() def has_object_permission(self, request: Request, view: ViewSet, course: Course) -> bool: """Check if user has permission to view a detailed course endpoint""" @@ -26,12 +26,13 @@ def has_object_permission(self, request: Request, view: ViewSet, course: Course) if request.method in SAFE_METHODS: # Logged-in users can fetch course details. - return request.user.is_authenticated + return user.is_authenticated # We only allow teachers and assistants to modify specified courses. - role: Teacher | Assistant = user.teacher or user.assistant + role: Teacher | Assistant = hasattr(user, "teacher") and user.teacher or \ + hasattr(user, "assistant") and user.assistant - return role is not None and \ + return role and \ role.courses.filter(id=course.id).exists() @@ -42,7 +43,7 @@ def has_object_permission(self, request: Request, view: ViewSet, course: Course) if request.method in SAFE_METHODS: # Logged-in users can still fetch course details. - return request.user.is_authenticated + return user.is_authenticated - return user.teacher.exists() and \ + return hasattr(user, "teacher") and \ user.teacher.courses.filter(id=course.id).exists() diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index c7123fa1..4414ffa1 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -1,7 +1,7 @@ from django.utils.translation import gettext from rest_framework import viewsets from rest_framework.exceptions import NotFound -from rest_framework.permissions import IsAdminUser +from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.request import Request @@ -17,65 +17,80 @@ class CourseViewSet(viewsets.ModelViewSet): + """Actions for general course logic""" queryset = Course.objects.all() serializer_class = CourseSerializer permission_classes = [IsAdminUser | CoursePermission] - @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. + @action(detail=True, permission_classes=[IsAdminUser | CourseTeacherPermission]) + def assistants(self, request: Request, **_): + """Returns a list of assistants for the given course""" course = self.get_object() - teachers = course.teachers.all() + assistants = course.assistants.all() - # Serialize the teacher objects - serializer = TeacherSerializer( - teachers, many=True, context={"request": request} + # Serialize assistants + serializer = AssistantSerializer( + assistants, many=True, context={"request": request} ) return Response(serializer.data) - @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. + @assistants.mapping.post + @assistants.mapping.put + def _add_assistant(self, request: Request, **_): + """Add an assistant to the course""" course = self.get_object() - if request.method == "GET": - # Return assistants of a course. - assistants = course.assistants.all() - - serializer = AssistantSerializer( - assistants, many=True, context={"request": request} + try: + # Add assistant to course + assistant = Assistant.objects.get( + id=request.data.get("id") ) - return Response(serializer.data) + course.assistants.add(assistant) + + return Response({ + "message": gettext("courses.success.assistants.add") + }) + except Assistant.DoesNotExist: + # Not found + raise NotFound(gettext("assistants.error.404")) + + @assistants.mapping.delete + def _remove_assistant(self, request: Request, **_): + """Remove an assistant from the course""" + course = self.get_object() try: + # Add assistant to course assistant = Assistant.objects.get( - id=request.query_params.get("id") + id=request.data.get("id") ) - if request.method == "POST": - # Add a new assistant to the course. - course.assistants.add(assistant) + course.assistants.remove(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") - }) + return Response({ + "message": gettext("courses.success.assistants.delete") + }) except Assistant.DoesNotExist: # 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"]) def students(self, request, **_): """Returns a list of students for the given course""" @@ -113,3 +128,24 @@ def join(self, request, **_): return Response({ "message": gettext("courses.success.join") }) + + @action(detail=True, methods=["post"], permission_classes=[IsAdminUser | CourseTeacherPermission]) + 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} + ) + except Course.DoesNotExist: + course_serializer = CourseSerializer( + course.clone( + year=request.data.get("academic_startyear") + ), + context={"request": request} + ) + + course_serializer.save() + + return Response(course_serializer.data) \ No newline at end of file