From 3dea579e1eb11d360f765f24c888b035dc6a2d88 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Thu, 23 May 2024 00:54:57 +0200 Subject: [PATCH] feat: project views --- backend/api/permissions/role_permissions.py | 7 +- backend/api/serializers/group_serializer.py | 30 ++- backend/api/views/project_view.py | 22 ++ frontend/src/assets/lang/app/en.json | 14 ++ frontend/src/assets/lang/app/nl.json | 14 ++ frontend/src/components/Loading.vue | 2 +- .../src/components/help/HelpBaseLayout.vue | 28 --- frontend/src/components/help/HelpHeader.vue | 208 ------------------ .../TeacherAssistantCard.vue | 4 +- .../TeacherAssistantList.vue | 0 .../TeacherAssistantSearch.vue | 0 .../TeacherAssistantUpdateButton.vue | 0 .../buttons/CourseRoleAddButton.vue | 0 .../buttons/LeaveCourseButton.vue | 0 frontend/src/components/layout/Footer.vue | 25 --- ...on.vue => ProjectDownloadGradesButton.vue} | 39 ++-- ...pload.vue => ProjectExtraChecksEditor.vue} | 0 .../src/components/projects/ProjectForm.vue | 4 +- ...ureTree.vue => ProjectStructureEditor.vue} | 0 ...hooseGroupCard.vue => GroupChooseCard.vue} | 40 ++-- ...oinedGroupCard.vue => GroupJoinedCard.vue} | 13 +- .../projects/groups/GroupsManageTable.vue | 130 +++++++++++ .../components/submissions/ProjectMeter.vue | 16 +- .../components/submissions/SubmissionCard.vue | 64 +++--- .../src/composables/services/group.service.ts | 26 ++- frontend/src/composables/services/helpers.ts | 5 +- .../composables/services/project.service.ts | 4 +- frontend/src/config/endpoints.ts | 1 + frontend/src/router/router.ts | 45 ---- frontend/src/types/Group.ts | 18 +- frontend/src/types/Project.ts | 10 +- frontend/src/views/admin/DockerImagesView.vue | 6 +- frontend/src/views/admin/UsersView.vue | 6 +- .../src/views/authentication/LoginView.vue | 4 +- .../src/views/authentication/VerifyView.vue | 4 +- frontend/src/views/calendar/CalendarView.vue | 4 +- frontend/src/views/courses/CourseView.vue | 2 +- .../src/views/courses/CreateCourseView.vue | 4 +- frontend/src/views/courses/JoinCourseView.vue | 6 +- .../src/views/courses/SearchCourseView.vue | 4 +- .../src/views/courses/UpdateCourseView.vue | 4 +- .../courses/roles/AssistantCourseView.vue | 4 +- .../views/courses/roles/StudentCourseView.vue | 4 +- .../views/courses/roles/TeacherCourseView.vue | 6 +- .../src/views/dashboard/DashboardView.vue | 2 +- .../roles/AssistantDashboardView.vue | 2 +- .../dashboard/roles/StudentDashboardView.vue | 2 +- .../dashboard/roles/TeacherDashboardView.vue | 2 +- frontend/src/views/help/HelpDashboard.vue | 58 ----- .../src/{components => views}/layout/Body.vue | 0 frontend/src/views/layout/Footer.vue | 64 ++++++ .../{components => views}/layout/Title.vue | 0 .../layout/admin/AdminHeader.vue | 0 .../layout/admin/AdminLayout.vue | 6 +- .../layout/admin/AdminSidebar.vue | 0 .../layout/base/BaseHeader.vue | 0 .../layout/base/BaseLayout.vue | 6 +- .../src/views/projects/CreateProjectView.vue | 4 +- frontend/src/views/projects/ProjectView.vue | 68 ++---- frontend/src/views/projects/ProjectsView.vue | 4 +- .../src/views/projects/UpdateProjectView.vue | 4 +- .../projects/roles/StudentProjectView.vue | 203 +++++++++++------ .../projects/roles/TeacherProjectView.vue | 156 ++++++++++--- .../src/views/submissions/SubmissionView.vue | 4 +- 64 files changed, 731 insertions(+), 681 deletions(-) delete mode 100644 frontend/src/components/help/HelpBaseLayout.vue delete mode 100644 frontend/src/components/help/HelpHeader.vue rename frontend/src/components/{teachers_assistants => instructors}/TeacherAssistantCard.vue (92%) rename frontend/src/components/{teachers_assistants => instructors}/TeacherAssistantList.vue (100%) rename frontend/src/components/{teachers_assistants => instructors}/TeacherAssistantSearch.vue (100%) rename frontend/src/components/{teachers_assistants => instructors}/TeacherAssistantUpdateButton.vue (100%) rename frontend/src/components/{teachers_assistants => instructors}/buttons/CourseRoleAddButton.vue (100%) rename frontend/src/components/{teachers_assistants => instructors}/buttons/LeaveCourseButton.vue (100%) delete mode 100644 frontend/src/components/layout/Footer.vue rename frontend/src/components/projects/{DownloadCSVButton.vue => ProjectDownloadGradesButton.vue} (63%) rename frontend/src/components/projects/{ExtraChecksUpload.vue => ProjectExtraChecksEditor.vue} (100%) rename frontend/src/components/projects/{ProjectStructureTree.vue => ProjectStructureEditor.vue} (100%) rename frontend/src/components/projects/groups/{ChooseGroupCard.vue => GroupChooseCard.vue} (72%) rename frontend/src/components/projects/groups/{JoinedGroupCard.vue => GroupJoinedCard.vue} (75%) create mode 100644 frontend/src/components/projects/groups/GroupsManageTable.vue delete mode 100644 frontend/src/views/help/HelpDashboard.vue rename frontend/src/{components => views}/layout/Body.vue (100%) create mode 100644 frontend/src/views/layout/Footer.vue rename frontend/src/{components => views}/layout/Title.vue (100%) rename frontend/src/{components => views}/layout/admin/AdminHeader.vue (100%) rename frontend/src/{components => views}/layout/admin/AdminLayout.vue (70%) rename frontend/src/{components => views}/layout/admin/AdminSidebar.vue (100%) rename frontend/src/{components => views}/layout/base/BaseHeader.vue (100%) rename frontend/src/{components => views}/layout/base/BaseLayout.vue (69%) 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);