Skip to content

Commit

Permalink
feat: course cloning
Browse files Browse the repository at this point in the history
  • Loading branch information
EwoutV committed Mar 8, 2024
1 parent 155683d commit 171f12d
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 42 deletions.
10 changes: 10 additions & 0 deletions backend/api/models/course.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Self
from django.db import models


Expand Down Expand Up @@ -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."""
Expand Down
13 changes: 7 additions & 6 deletions backend/api/permissions/course_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@ 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"""
user: User = request.user

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()


Expand All @@ -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()
108 changes: 72 additions & 36 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"""
Expand Down Expand Up @@ -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)

0 comments on commit 171f12d

Please sign in to comment.