Skip to content

Commit

Permalink
Merge branch 'refs/heads/development' into better_error
Browse files Browse the repository at this point in the history
# Conflicts:
#	frontend/src/composables/services/group.service.ts
#	frontend/src/composables/services/project.service.ts
  • Loading branch information
EwoutV committed May 23, 2024
2 parents c25e7a6 + 694a932 commit 5c2fe8a
Show file tree
Hide file tree
Showing 67 changed files with 791 additions and 735 deletions.
45 changes: 20 additions & 25 deletions backend/api/permissions/group_permissions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from api.models.assistant import Assistant
from api.models.group import Group
from api.models.project import Project
from api.models.student import Student
from api.models.teacher import Teacher
from api.permissions.role_permissions import (is_assistant, is_student,
is_teacher)
from authentication.models import User
Expand Down Expand Up @@ -62,34 +66,25 @@ class GroupSubmissionPermission(BasePermission):
"""Permission class for submission related group endpoints"""

def has_permission(self, request: Request, view: APIView) -> bool:
user: User = request.user
group_id = view.kwargs.get('pk')
group: Group | None = Group.objects.get(id=group_id) if group_id else None

if group is None:
return True
"""Check if user has permission to view a general group submission endpoint."""
user = request.user

# Teachers and assistants of that course can view all submissions
if is_teacher(user):
return group.project.course.teachers.filter(id=user.teacher.id).exists()
# Get the individual permission clauses.
return request.method in SAFE_METHODS or is_teacher(user) or is_assistant(user)

if is_assistant(user):
return group.project.course.assistants.filter(id=user.assistant.id).exists()

return is_student(user) and group.students.filter(id=user.student.id).exists()

def had_object_permission(self, request: Request, view: ViewSet, group) -> bool:
user: User = request.user
def had_object_permission(self, request: Request, view: ViewSet, group: Group) -> bool:
"""Check if user has permission to view a detailed group submission endpoint"""
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 group can view the submissions of the group
return teacher_or_assitant or (is_student(user) and user.student.groups.filter(id=group.id).exists())
# Check if the user is a teacher that has the course linked to the project.
teacher = Teacher.objects.filter(id=user.id).first()
assistant = Assistant.objects.filter(id=user.id).first()
student = Student.objects.filter(id=user.id).first()

# Student can only add submissions to their own group
if is_student(user) and request.data.get("student") == user.id and view.action == "create": # type: ignore
return user.student.courses.filter(id=course.id).exists()
# Get the individual permission clauses.
teacher_permission = teacher is not None and teacher.courses.filter(id=course.id).exists()
assistant_permission = assistant is not None and assistant.courses.filter(id=course.id).exists()
student_permission = student is not None and student.groups.filter(id=group.id).exists()

return teacher_or_assitant
return teacher_permission or assistant_permission or student_permission
53 changes: 32 additions & 21 deletions backend/api/permissions/project_permissions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from api.models.assistant import Assistant
from api.models.student import Student
from api.models.teacher import Teacher
from api.permissions.role_permissions import (is_assistant, is_student,
is_teacher)
from authentication.models import User
Expand All @@ -11,38 +14,46 @@ class ProjectPermission(BasePermission):

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

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

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()
user = request.user

# Check if the user is a teacher that has the course linked to the project.
teacher = Teacher.objects.filter(id=user.id).first()
assistant = Assistant.objects.filter(id=user.id).first()
student = Student.objects.filter(id=user.id).first()

# Get the individual permission clauses.
teacher_permission = teacher is not None and teacher.courses.filter(id=project.course.id).exists()
assistant_permission = assistant is not None and assistant.courses.filter(id=project.course.id).exists()
student_permission = student is not None and student.courses.filter(id=project.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())
return teacher_permission or assistant_permission or student_permission

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


class ProjectGroupPermission(BasePermission):
"""Permission class for project related group endpoints"""
def has_permission(self, request: Request, view: ViewSet) -> bool:
"""Check if user has permission to view a general project group endpoint."""
return is_teacher(request.user) or is_assistant(request.user) or request.method in SAFE_METHODS

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()
"""Check if user has permission to view a detailed project group endpoint"""
user = request.user

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())
# Check if the user is a teacher that has the course linked to the project.
teacher = Teacher.objects.filter(id=user.id).first()
assistant = Assistant.objects.filter(id=user.id).first()
student = Student.objects.filter(id=user.id).first()

# Get the individual permission clauses.
teacher_permission = teacher is not None and teacher.courses.filter(id=project.course.id).exists()
assistant_permission = assistant is not None and assistant.courses.filter(id=project.course.id).exists()
student_permission = student is not None and student.courses.filter(id=project.course.id).exists()

# We only allow teachers and assistants to create new groups.
return teacher_or_assistant
return teacher_permission or assistant_permission or student_permission
7 changes: 4 additions & 3 deletions backend/api/permissions/role_permissions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.base_user import AbstractBaseUser
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
Expand All @@ -7,17 +8,17 @@
from api.models.teacher import Teacher


def is_student(user: User) -> bool:
def is_student(user: AbstractBaseUser) -> bool:
"""Check whether the user is a student"""
return Student.objects.filter(id=user.id, is_active=True).exists()


def is_assistant(user: User) -> bool:
def is_assistant(user: AbstractBaseUser) -> bool:
"""Check whether the user is an assistant"""
return Assistant.objects.filter(id=user.id, is_active=True).exists()


def is_teacher(user: User) -> bool:
def is_teacher(user: AbstractBaseUser) -> bool:
"""Check whether the user is a teacher"""
return Teacher.objects.filter(id=user.id, is_active=True).exists()

Expand Down
30 changes: 20 additions & 10 deletions backend/api/serializers/group_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,62 @@


class GroupSerializer(serializers.ModelSerializer):
project = ProjectSerializer(
read_only=True,
project = serializers.HyperlinkedIdentityField(
view_name="project-detail"
)

students = serializers.HyperlinkedIdentityField(
view_name="group-students",
read_only=True,
)

occupation = serializers.SerializerMethodField()

submissions = serializers.HyperlinkedIdentityField(
view_name="group-submissions",
read_only=True,
)

class Meta:
model = Group
fields = "__all__"
def get_occupation(self, instance: Group):
"""Get the number of students in the group"""
return instance.students.count()

def to_representation(self, instance):
def to_representation(self, instance: Group):
"""Convert the group to a JSON representation"""
data = super().to_representation(instance)

user = self.context["request"].user
course_id = instance.project.course.id

# If you are not a student, you can always see the score
if is_student(user):
if is_student(user) and not is_teacher(user) and not is_assistant(user) and not user.is_staff:
student_in_course = user.student.courses.filter(id=course_id).exists()

# Student can not see the score if they are not part of the course associated with group and
# neither an assistant or a teacher,
# or it is not visible yet when they are part of the course associated with the group
if not student_in_course and not is_assistant(user) and not is_teacher(user) or \
if not student_in_course or \
not instance.project.score_visible and student_in_course:
data.pop("score")

return data

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

if group is None:
raise ValueError("Group is not in context")

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

return attrs

class Meta:
model = Group
fields = "__all__"


class StudentJoinGroupSerializer(StudentIDSerializer):

Expand Down
12 changes: 6 additions & 6 deletions backend/api/tests/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from django.urls import reverse
from rest_framework.test import APITestCase

from ypovoli import settings


class GroupModelTests(APITestCase):
def setUp(self) -> None:
Expand Down Expand Up @@ -39,7 +41,9 @@ def test_group_detail_view(self):
content_json = json.loads(response.content.decode("utf-8"))

self.assertEqual(int(content_json["id"]), group.id)
self.assertEqual(content_json["project"]["id"], group.project.id)
self.assertEqual(content_json["project"],
settings.TESTING_BASE_LINK + reverse("project-detail", args=[str(project.id)])
)
self.assertEqual(content_json["score"], group.score)

def test_group_project(self):
Expand Down Expand Up @@ -70,11 +74,7 @@ def test_group_project(self):
# Parse the JSON content from the response
content_json = content_json["project"]

self.assertEqual(content_json["name"], project.name)
self.assertEqual(content_json["description"], project.description)
self.assertEqual(content_json["visible"], project.visible)
self.assertEqual(content_json["archived"], project.archived)
self.assertEqual(content_json["course"]["id"], course.id)
self.assertEqual(content_json, settings.TESTING_BASE_LINK + reverse("project-detail", args=[str(project.id)]))

def test_group_students(self):
"""Able to retrieve students details of a group."""
Expand Down
22 changes: 22 additions & 0 deletions backend/api/views/project_view.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from api.models.group import Group
from api.models.project import Project
from api.models.student import Student
from api.models.submission import Submission
from api.permissions.project_permissions import (ProjectGroupPermission,
ProjectPermission)
from api.permissions.role_permissions import is_student, IsStudent
from api.serializers.checks_serializer import (ExtraCheckSerializer,
StructureCheckSerializer)
from api.serializers.group_serializer import GroupSerializer
Expand Down Expand Up @@ -31,6 +33,26 @@ class ProjectViewSet(RetrieveModelMixin,
serializer_class = ProjectSerializer
permission_classes = [IsAdminUser | ProjectPermission] # GroupPermission has exact the same logic as for a project

@action(detail=True, permission_classes=[IsAdminUser | ProjectGroupPermission], url_path='student-group')
def student_group(self, request: Request, **_) -> Response:
"""Returns the group of the student for the given project"""

# Get the student object from the user
student = Student.objects.get(id=request.user.id)

# Get the group of the student for the project
group = student.groups.filter(project=self.get_object()).first()

if group is None:
return Response(None)

# Serialize the group object
serializer = GroupSerializer(
group, context={"request": request}
)

return Response(serializer.data)

@action(detail=True, permission_classes=[IsAdminUser | ProjectGroupPermission])
def groups(self, request, **_):
"""Returns a list of groups for the given project"""
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/assets/lang/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
"nl": "Nederlands",
"en": "English"
}
},
"footer": {
"home": "Dashboard",
"about": "Help",
"privacy": "Cookies",
"contact": "Contact",
"rights": "All rights reserved"
}
},
"views": {
Expand All @@ -41,11 +48,18 @@
},
"projects": {
"all": "All projects",
"backToCourse": "Back to course",
"coming": "Near deadlines",
"deadline": "Deadline",
"days": "Today at {hour} | Tomorrow at {hour} | In {count} days",
"ago": "1 day ago | {count} days ago",
"chooseGroupMessage": "Choose a group before {0}",
"groupScore": "Group score",
"noGroupScore": "No group score",
"noGroupMembers": "No group members",
"publishScores": "Publish scores",
"groupName": "Group name",
"groups": "Groups",
"groupPopulation": "Size",
"groupStatus": "Status",
"start": "Start date",
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/assets/lang/app/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
"nl": "Nederlands",
"en": "English"
}
},
"footer": {
"home": "Dashboard",
"about": "Help",
"privacy": "Cookies",
"contact": "Contacteer ons",
"rights": "Alle rechten voorbehouden"
}
},
"views": {
Expand Down Expand Up @@ -42,15 +49,22 @@
"projects": {
"all": "Alle projecten",
"coming": "Aankomende deadlines",
"backToCourse": "Terug naar het vak",
"deadline": "Deadline",
"days": "Vandaag om {hour} | Morgen om {hour} | Over {count} dagen",
"ago": "1 dag geleden | {count} dagen geleden",
"chooseGroupMessage": "Kies een groep voor {0}",
"groupScore": "Groepsscore",
"noGroupScore": "Nog geen score",
"publishScores": "Publiceer scores",
"groupName": "Groepsnaam",
"noGroupMembers": "Geen groepsleden",
"groupPopulation": "Grootte",
"groupStatus": "Status",
"start": "Startdatum",
"submissionStatus": "Indienstatus",
"group": "Groep",
"groups": "Groepen",
"groupSize": "Individueel | Groepen van {count} personen",
"noGroups": "Geen groepen beschikbaar",
"groupMembers": "Groepsleden",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Loading.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ withDefaults(defineProps<{ height?: string }>(), {
height: '4rem',
});
const show = useTimeout(250);
const show = useTimeout(350);
</script>

<template>
Expand Down
Loading

0 comments on commit 5c2fe8a

Please sign in to comment.