Skip to content

Commit

Permalink
Merge pull request #67 from SELab-2/course-logic
Browse files Browse the repository at this point in the history
Course logic
  • Loading branch information
EwoutV authored Mar 8, 2024
2 parents 31672e5 + bdb9d9e commit a6cbd08
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 72 deletions.
15 changes: 15 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,20 @@ def __str__(self) -> str:
"""The string representation of the course."""
return str(self.name)

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=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
Empty file.
79 changes: 79 additions & 0 deletions backend/api/permissions/course_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
from authentication.models import User
from api.permissions.role_permissions import is_student, is_assistant, is_teacher
from api.models.course import Course


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

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

# 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:
return user.is_authenticated

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


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:
return user.is_authenticated

# Teachers and assistants can modify projects.
return super().has_object_permission(request, view, course)
39 changes: 39 additions & 0 deletions backend/api/permissions/role_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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: Request, view):
"""Returns true if the request contains a user,
with said user being a student"""
return super().has_permission(request, view) and is_student(request.user)


class IsTeacher(IsAuthenticated):
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 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 is_assistant(request.user)
6 changes: 3 additions & 3 deletions backend/api/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
205 changes: 143 additions & 62 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,175 @@
from rest_framework import viewsets, status
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 ..models.course import Course
from ..serializers.course_serializer import CourseSerializer
from ..serializers.teacher_serializer import TeacherSerializer
from ..serializers.assistant_serializer import AssistantSerializer
from ..serializers.student_serializer import StudentSerializer
from ..serializers.project_serializer import ProjectSerializer
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.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
from api.serializers.student_serializer import StudentSerializer
from api.serializers.project_serializer import ProjectSerializer


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, pk=None):
"""Returns a list of teachers for the given course"""
@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()
assistants = course.assistants.all()

try:
queryset = Course.objects.get(id=pk)
teachers = queryset.teachers.all()
# Serialize assistants
serializer = AssistantSerializer(
assistants, many=True, context={"request": request}
)

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

except Course.DoesNotExist:
# Invalid course ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"}
@assistants.mapping.post
@assistants.mapping.put
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")
)

@action(detail=True, methods=["get"])
def assistants(self, request, pk=None):
"""Returns a list of assistants for the given course"""
course.assistants.add(assistant)

try:
queryset = Course.objects.get(id=pk)
assistants = queryset.assistants.all()
return Response({
"message": gettext("courses.success.assistants.add")
})
except Assistant.DoesNotExist:
# Not found
raise NotFound(gettext("assistants.error.404"))

# Serialize the assistant objects
serializer = AssistantSerializer(
assistants, many=True, context={"request": request}
)
return Response(serializer.data)
@assistants.mapping.delete
def _remove_assistant(self, request: Request, **_):
"""Remove an assistant from the course"""
course = self.get_object()

except Course.DoesNotExist:
# Invalid course ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"}
try:
# Add assistant to course
assistant = Assistant.objects.get(
id=request.data.get("id")
)

@action(detail=True, methods=["get"])
def students(self, request, pk=None):
course.assistants.remove(assistant)

return Response({
"message": gettext("courses.success.assistants.delete")
})
except Assistant.DoesNotExist:
# Not found
raise NotFound(gettext("assistants.error.404"))

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

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

# Serialize the student objects
serializer = StudentSerializer(
students, many=True, context={"request": request}
try:
# Add student to course
student = Student.objects.get(
id=request.data.get("id")
)
return Response(serializer.data)

except Course.DoesNotExist:
# Invalid course ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"}
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, 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}
)

# Serialize the project objects
serializer = ProjectSerializer(
projects, many=True, context={"request": request}
)
return Response(serializer.data)
return Response(serializer.data)

@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 = course.child_course
except Course.DoesNotExist:
# Invalid course ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"}
course = course.clone(
clone_assistants=request.data.get("clone_assistants")
)

course.save()

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

return Response(course_serializer.data)
Loading

0 comments on commit a6cbd08

Please sign in to comment.