From 4082b4a57d40f0c7ce611e799864ecc3a6259587 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Fri, 8 Mar 2024 21:51:47 +0100 Subject: [PATCH] chore: serializers for validation --- backend/api/models/course.py | 5 + .../api/serializers/assistant_serializer.py | 6 + backend/api/serializers/course_serializer.py | 49 +++++++- backend/api/serializers/student_serializer.py | 6 + backend/api/views/course_view.py | 107 +++++++++--------- 5 files changed, 120 insertions(+), 53 deletions(-) diff --git a/backend/api/models/course.py b/backend/api/models/course.py index 6c657c2c..02f346e8 100644 --- a/backend/api/models/course.py +++ b/backend/api/models/course.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Self from django.db import models @@ -31,6 +32,10 @@ def __str__(self) -> str: """The string representation of the course.""" return str(self.name) + def is_past(self) -> bool: + """Returns whether the course is from a past academic year""" + return datetime(self.academic_startyear + 1, 10, 1) < datetime.now() + def clone(self, clone_assistants=True) -> Self: """Clone the course to the next academic start year""" course = Course( diff --git a/backend/api/serializers/assistant_serializer.py b/backend/api/serializers/assistant_serializer.py index 16e26206..639b184b 100644 --- a/backend/api/serializers/assistant_serializer.py +++ b/backend/api/serializers/assistant_serializer.py @@ -24,3 +24,9 @@ class Meta: "create_time", "courses", ] + + +class AssistantIDSerializer(serializers.Serializer): + assistant_id = serializers.PrimaryKeyRelatedField( + queryset=Assistant.objects.all() + ) diff --git a/backend/api/serializers/course_serializer.py b/backend/api/serializers/course_serializer.py index 4d6edede..de7d0b70 100644 --- a/backend/api/serializers/course_serializer.py +++ b/backend/api/serializers/course_serializer.py @@ -1,5 +1,8 @@ +from django.utils.translation import gettext from rest_framework import serializers -from ..models.course import Course +from rest_framework.exceptions import ValidationError +from api.serializers.student_serializer import StudentIDSerializer +from api.models.course import Course class CourseSerializer(serializers.ModelSerializer): @@ -40,3 +43,47 @@ class Meta: "students", "projects", ] + + +class CourseIDSerializer(serializers.Serializer): + student_id = serializers.PrimaryKeyRelatedField( + queryset=Course.objects.all() + ) + + +class StudentJoinSerializer(StudentIDSerializer): + def validate(self, data): + # The validator needs the course context. + if "course" not in self.context: + raise ValidationError(gettext("courses.error.context")) + + course: Course = self.context["course"] + + # Check if the student isn't already enrolled. + if course.students.contains(data["student_id"]): + raise ValidationError(gettext("courses.error.students.already_present")) + + # Check if the course is not from a past academic year. + if course.is_past(): + raise ValidationError(gettext("courses.error.students.past_course")) + + return data + + +class StudentLeaveSerializer(StudentIDSerializer): + def validate(self, data): + # The validator needs the course context. + if "course" not in self.context: + raise ValidationError(gettext("courses.error.context")) + + course: Course = self.context["course"] + + # Check if the student isn't already enrolled. + if not course.students.contains(data["student_id"]): + raise ValidationError(gettext("courses.error.students.not_present")) + + # Check if the course is not from a past academic year. + if course.is_past(): + raise ValidationError(gettext("courses.error.students.past_course")) + + return data diff --git a/backend/api/serializers/student_serializer.py b/backend/api/serializers/student_serializer.py index 9cd1f245..2c46593f 100644 --- a/backend/api/serializers/student_serializer.py +++ b/backend/api/serializers/student_serializer.py @@ -20,3 +20,9 @@ class StudentSerializer(serializers.ModelSerializer): class Meta: model = Student fields = '__all__' + + +class StudentIDSerializer(serializers.Serializer): + student_id = serializers.PrimaryKeyRelatedField( + queryset=Student.objects.all() + ) diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index 416ab1f5..be9bd152 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -1,18 +1,19 @@ 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.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.models.student import Student -from api.permissions.course_permissions import CoursePermission, CourseAssistantPermission, CourseStudentPermission +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.course_serializer import CourseSerializer, StudentJoinSerializer, StudentLeaveSerializer from api.serializers.teacher_serializer import TeacherSerializer -from api.serializers.assistant_serializer import AssistantSerializer +from api.serializers.assistant_serializer import AssistantSerializer, AssistantIDSerializer from api.serializers.student_serializer import StudentSerializer from api.serializers.project_serializer import ProjectSerializer @@ -42,40 +43,38 @@ def _add_assistant(self, request: Request, **_): """Add an assistant to the course""" course = self.get_object() - try: - # Add assistant to course - assistant = Assistant.objects.get( - id=request.data.get("id") - ) + # Add assistant to course + serializer = AssistantIDSerializer( + data=request.data + ) - course.assistants.add(assistant) + if serializer.is_valid(raise_exception=True): + course.assistants.add( + serializer.validated_data["assistant_id"] + ) - return Response({ - "message": gettext("courses.success.assistants.add") - }) - except Assistant.DoesNotExist: - # Not found - raise NotFound(gettext("assistants.error.404")) + return Response({ + "message": gettext("courses.success.assistants.add") + }) @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.data.get("id") - ) + # Remove assistant from course + serializer = AssistantIDSerializer( + data=request.data + ) - course.assistants.remove(assistant) + if serializer.is_valid(raise_exception=True): + course.assistants.remove( + serializer.validated_data["assistant_id"] + ) - return Response({ - "message": gettext("courses.success.assistants.delete") - }) - except Assistant.DoesNotExist: - # Not found - raise NotFound(gettext("assistants.error.404")) + return Response({ + "message": gettext("courses.success.assistants.add") + }) @action(detail=True, methods=["get"], permission_classes=[IsAdminUser | CourseStudentPermission]) def students(self, request, **_): @@ -94,40 +93,42 @@ def students(self, request, **_): @students.mapping.put def _add_student(self, request: Request, **_): """Add a student to the course""" + # Get the course course = self.get_object() - try: - # Add student to course - student = Student.objects.get( - id=request.data.get("id") - ) + # Add student to course + serializer = StudentJoinSerializer(data=request.data, context={ + "course": course + }) - course.students.add(student) + if serializer.is_valid(raise_exception=True): + course.students.add( + serializer.validated_data["student_id"] + ) - return Response({ - "message": gettext("courses.success.students.add") - }) - except Student.DoesNotExist: - raise NotFound(gettext("students.error.404")) + return Response({ + "message": gettext("courses.success.students.add") + }) @students.mapping.delete def _remove_student(self, request: Request, **_): """Remove a student from the course""" + # Get the course course = self.get_object() - try: - # Add student to course - student = Student.objects.get( - id=request.data.get("id") - ) + # Add student to course + serializer = StudentLeaveSerializer(data=request.data, context={ + "course": course + }) - course.students.remove(student) + if serializer.is_valid(raise_exception=True): + course.students.remove( + serializer.validated_data["student_id"] + ) - return Response({ - "message": gettext("courses.success.students.remove") - }) - except Student.DoesNotExist: - raise NotFound(gettext("students.error.404")) + return Response({ + "message": gettext("courses.success.students.add") + }) @action(detail=True, methods=["get"]) def teachers(self, request, **_): @@ -161,8 +162,10 @@ def clone(self, request: Request, **__): course: Course = self.get_object() try: + # We should return the already cloned course, if present course = course.child_course except Course.DoesNotExist: + # Else, we clone the course course = course.clone( clone_assistants=request.data.get("clone_assistants") )