Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Course logic #67

Merged
merged 8 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Empty file.
49 changes: 49 additions & 0 deletions backend/api/permissions/course_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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.models.teacher import Teacher
from api.models.assistant import Assistant
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

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

# We only allow teachers to create new courses.
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 user.is_authenticated

# We only allow teachers and assistants to modify specified courses.
role: Teacher | Assistant = hasattr(user, "teacher") and user.teacher or \
hasattr(user, "assistant") and user.assistant

return role and \
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 user.is_authenticated

return hasattr(user, "teacher") and \
user.teacher.courses.filter(id=course.id).exists()
28 changes: 28 additions & 0 deletions backend/api/permissions/role_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from rest_framework.permissions import IsAuthenticated
from api.models.student import Student
from api.models.assistant import Assistant
from api.models.teacher import Teacher


class IsStudent(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 \
Student.objects.filter(id=request.user.id).exists()


class IsTeacher(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 \
Teacher.objects.filter(id=request.user.id).exists()


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 \
Assistant.objects.filter(id=request.user.id).exists()
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)
185 changes: 121 additions & 64 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,151 @@
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, IsAuthenticated
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.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
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 | CourseTeacherPermission])
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")
)

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"])
def students(self, request, pk=None):
"""Returns a list of students for the given course"""
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()

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

# Serialize the student objects
serializer = StudentSerializer(
students, 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"}
)
@action(detail=True, methods=["get"])
def students(self, request, **_):
"""Returns a list of students for the given course"""
course = self.get_object()
students = course.students.all()

# Serialize the student objects
serializer = StudentSerializer(
students, 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=[IsStudent])
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
)

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:
# Invalid course ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Course not found"}
course_serializer = CourseSerializer(
course.clone(
year=request.data.get("academic_startyear")
),
context={"request": request}
)

course_serializer.save()

return Response(course_serializer.data)
7 changes: 0 additions & 7 deletions backend/authentication/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -35,12 +34,6 @@ class User(AbstractBaseUser):
USERNAME_FIELD = "username"
EMAIL_FIELD = "email"

def has_role(self, model: Type[Self]):
"""Simple generic implementation of roles.
This function looks if there exists a model (inheriting from User) with the same ID.
"""
model.objects.exists(self.id)

@staticmethod
def get_dummy_admin():
return User(
Expand Down
Loading