diff --git a/backend/api/permissions/role_permissions.py b/backend/api/permissions/role_permissions.py index 68cc428e..b0846746 100644 --- a/backend/api/permissions/role_permissions.py +++ b/backend/api/permissions/role_permissions.py @@ -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 @@ -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() diff --git a/backend/api/serializers/group_serializer.py b/backend/api/serializers/group_serializer.py index ae2e474f..b62102c7 100644 --- a/backend/api/serializers/group_serializer.py +++ b/backend/api/serializers/group_serializer.py @@ -13,8 +13,8 @@ class GroupSerializer(serializers.ModelSerializer): - project = ProjectSerializer( - read_only=True, + project = serializers.HyperlinkedIdentityField( + view_name="project-detail" ) students = serializers.HyperlinkedIdentityField( @@ -22,28 +22,32 @@ class GroupSerializer(serializers.ModelSerializer): 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") @@ -51,14 +55,20 @@ def to_representation(self, instance): 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): diff --git a/backend/api/views/project_view.py b/backend/api/views/project_view.py index 180dd5d1..9267dbfc 100644 --- a/backend/api/views/project_view.py +++ b/backend/api/views/project_view.py @@ -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 @@ -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 | IsStudent], 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""" diff --git a/frontend/src/assets/lang/app/en.json b/frontend/src/assets/lang/app/en.json index dbcd356f..4d812ac4 100644 --- a/frontend/src/assets/lang/app/en.json +++ b/frontend/src/assets/lang/app/en.json @@ -15,6 +15,13 @@ "nl": "Nederlands", "en": "English" } + }, + "footer": { + "home": "Dashboard", + "about": "Help", + "privacy": "Cookies", + "contact": "Contact", + "rights": "All rights reserved" } }, "views": { @@ -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", diff --git a/frontend/src/assets/lang/app/nl.json b/frontend/src/assets/lang/app/nl.json index be118d94..b933ef13 100644 --- a/frontend/src/assets/lang/app/nl.json +++ b/frontend/src/assets/lang/app/nl.json @@ -15,6 +15,13 @@ "nl": "Nederlands", "en": "English" } + }, + "footer": { + "home": "Dashboard", + "about": "Help", + "privacy": "Cookies", + "contact": "Contacteer ons", + "rights": "Alle rechten voorbehouden" } }, "views": { @@ -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", diff --git a/frontend/src/components/Loading.vue b/frontend/src/components/Loading.vue index eac23369..272c1aa0 100644 --- a/frontend/src/components/Loading.vue +++ b/frontend/src/components/Loading.vue @@ -6,7 +6,7 @@ withDefaults(defineProps<{ height?: string }>(), { height: '4rem', }); -const show = useTimeout(250); +const show = useTimeout(350);