Skip to content

Commit

Permalink
Merge pull request #72 from SELab-2/group-logic
Browse files Browse the repository at this point in the history
Group logic
  • Loading branch information
EwoutV authored Mar 9, 2024
2 parents 1c1547d + b451035 commit 3abdecb
Show file tree
Hide file tree
Showing 12 changed files with 749 additions and 229 deletions.
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

0 comments on commit 3abdecb

Please sign in to comment.