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

feat: project views, footer #452

Merged
merged 8 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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:
Copy link
Contributor

@bsilkyn bsilkyn May 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't it now be the case that an assistant, who's also a student, can see the scores of all groups of a project of a course of which they aren't an assistant, but a student instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

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
10 changes: 4 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,7 @@ 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 +72,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}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These need punctuation marks

"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
28 changes: 0 additions & 28 deletions frontend/src/components/help/HelpBaseLayout.vue

This file was deleted.

Loading
Loading