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

Group logic #72

Merged
merged 15 commits into from
Mar 9, 2024
12 changes: 12 additions & 0 deletions backend/api/fixtures/groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,15 @@
students:
- '1'
- '2'
- model: api.group
pk: 3
fields:
project: 123456
score: 8
students: []
- model: api.group
pk: 2
fields:
project: 1
score: 8
students: []
12 changes: 12 additions & 0 deletions backend/api/fixtures/projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@
group_size: 3
max_score: 20
course: 2
- model: api.project
pk: 1
fields:
name: sel3
description: make a project
visible: true
archived: false
start_date: 2024-02-26 00:00:00+00:00
deadline: 2024-02-27 00:00:00+00:00
group_size: 3
max_score: 20
course: 1
4 changes: 4 additions & 0 deletions backend/api/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ class Group(models.Model):

# Score of the group
score = models.FloatField(blank=True, null=True)

def is_full(self) -> bool:
"""Check if the group is full."""
return self.students.count() >= self.project.group_size
4 changes: 4 additions & 0 deletions backend/api/models/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ class Student(User):
related_name="students",
blank=True,
)

def is_enrolled_in_group(self, project):
"""Check if the student is enrolled in a group for the given project."""
return self.groups.filter(project=project).exists()
55 changes: 55 additions & 0 deletions backend/api/permissions/group_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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


class GroupPermission(BasePermission):

def has_permission(self, request: Request, view: ViewSet) -> bool:
"""Check if user has permission to view a general group endpoint."""
user: User = request.user

# The general group endpoint that lists all groups is not accessible for any role.
if request.method in SAFE_METHODS:
return True

# We only allow teachers and assistants to create new groups.
return is_teacher(user) or is_assistant(user)

def has_object_permission(self, request: Request, view: ViewSet, group) -> bool:
"""Check if user has permission to view a detailed group endpoint"""
user: User = request.user
course = group.project.course
teacher_or_assitant = is_teacher(user) and user.teacher.courses.filter(id=course.id).exists() or \
is_assistant(user) and user.assistant.courses.filter(id=course.id).exists()

if request.method in SAFE_METHODS:
# Users that are linked to the course can view the group.
return teacher_or_assitant or (is_student(user) and user.student.courses.filter(id=course.id).exists())

# We only allow teachers and assistants to modify specified groups.
return teacher_or_assitant


class GroupStudentPermission(BasePermission):
"""Permission class for student related group endpoints"""

def has_object_permission(self, request: Request, view: ViewSet, group) -> bool:
user: User = request.user
course = group.project.course
teacher_or_assitant = is_teacher(user) and user.teacher.courses.filter(id=course.id).exists() or \
is_assistant(user) and user.assistant.courses.filter(id=course.id).exists()

if request.method in SAFE_METHODS:
# Users related to the course can view the students of the group.
return teacher_or_assitant or (is_student(user) and user.student.courses.filter(id=course.id).exists())

# Students can only add and remove themselves from a group.
if is_student(user) and request.data.get("student_id") == user.id:
# Make sure the student is actually part of the course.
return user.student.courses.filter(id=course.id).exists()

# Teachers and assistants can add and remove any student from a group
return teacher_or_assitant
51 changes: 51 additions & 0 deletions backend/api/permissions/project_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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


class ProjectPermission(BasePermission):
"""Permission class for project related endpoints"""

def has_permission(self, request: Request, view: ViewSet) -> bool:
"""Check if user has permission to view a general project endpoint."""
user: User = request.user

# The general project endpoint that lists all projects is not accessible for any role.
if request.method in SAFE_METHODS:
return True

# We only allow teachers and assistants to create new projects.
return is_teacher(user) or is_assistant(user)

def has_object_permission(self, request: Request, view: ViewSet, project) -> bool:
"""Check if user has permission to view a detailed project endpoint"""
user: User = request.user
course = project.course
teacher_or_assistant = is_teacher(user) and user.teacher.courses.filter(id=course.id).exists() or \
is_assistant(user) and user.assistant.courses.filter(id=course.id).exists()

if request.method in SAFE_METHODS:
# Users that are linked to the course can view the project.
return teacher_or_assistant or (is_student(user) and user.student.courses.filter(id=course.id).exists())

# We only allow teachers and assistants to modify specified projects.
return teacher_or_assistant


class ProjectGroupPermission(BasePermission):
"""Permission class for project related group endpoints"""

def has_object_permission(self, request: Request, view: ViewSet, project) -> bool:
user: User = request.user
course = project.course
teacher_or_assistant = is_teacher(user) and user.teacher.courses.filter(id=course.id).exists() or \
is_assistant(user) and user.assistant.courses.filter(id=course.id).exists()

if request.method in SAFE_METHODS:
# Users that are linked to the course can view the group.
return teacher_or_assistant or (is_student(user) and user.student.courses.filter(id=course.id).exists())

# We only allow teachers and assistants to create new groups.
return teacher_or_assistant
59 changes: 58 additions & 1 deletion backend/api/serializers/group_serializer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from django.utils.translation import gettext
from rest_framework import serializers
from ..models.group import Group
from rest_framework.exceptions import ValidationError
from api.models.group import Group
from api.models.student import Student
from api.serializers.student_serializer import StudentIDSerializer


class GroupSerializer(serializers.ModelSerializer):
Expand All @@ -15,3 +19,56 @@ class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ["id", "project", "students", "score"]

def validate(self, data):
# Make sure the score of the group is lower or equal to the maximum score
group: Group = self.instance

if "score" in data and data["score"] > group.project.max_score:
raise ValidationError(gettext("group.errors.score_exceeds_max"))

return data


class StudentJoinGroupSerializer(StudentIDSerializer):

def validate(self, data):
# The validator needs the group context.
if "group" not in self.context:
raise ValidationError(gettext("group.error.context"))

# Get the group and student
group: Group = self.context["group"]
student: Student = data["student_id"]

# Make sure the group is not already full
if group.is_full():
raise ValidationError(gettext("group.errors.full"))

# Make sure the student is part of the course
if not group.project.course.students.filter(id=student.id).exists():
raise ValidationError(gettext("group.errors.not_in_course"))

# Make sure the student is not already in a group
if student.is_enrolled_in_group(group.project):
raise ValidationError(gettext("group.errors.already_in_group"))

return data


class StudentLeaveGroupSerializer(StudentIDSerializer):

def validate(self, data):
# The validator needs the group context.
if "group" not in self.context:
raise ValidationError(gettext("group.error.context"))

# Get the group and student
group: Group = self.context["group"]
student: Student = data["student_id"]

# Make sure the student was in the group
if not group.students.filter(id=student.id).exists():
raise ValidationError(gettext("group.errors.not_present"))

return data
4 changes: 4 additions & 0 deletions backend/api/serializers/project_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ class Meta:
"course",
"groups"
]


class TeacherCreateGroupSerializer(serializers.Serializer):
number_groups = serializers.IntegerField(min_value=1)
Loading
Loading