From 1ed52e788de9892842cc11e0e38e5ddf0fa360b4 Mon Sep 17 00:00:00 2001 From: Ewout Verlinde Date: Thu, 18 Apr 2024 21:50:57 +0200 Subject: [PATCH] Project view (#338) * feat: endpoint for eagerly fetching all projects for students/teachers/assistants * fix: tests * feat: editor for course description and editor, better project overview list * fix: tests * chore: improved project progressbar, linting * improvement: skeleton loaders for project list * chore: merged migrations, updated tests * chore: merge * chore: better display of HTML description * chore: merge * chore: translations, import fixes and linting * chore: tests * chore: linting * fix: typing * fix: linting * chore: removed vue-tsc * feat: project view (wip) * fix: better models and student project view * feat: start project view for teachers * feat: student project view, start teacher project view * fix: redundant directory --------- Co-authored-by: francis --- development.sh | 2 +- frontend/src/assets/lang/nl.json | 11 +- .../src/components/courses/CourseList.vue | 2 +- frontend/src/components/layout/Title.vue | 1 + .../components/projects/ChooseGroupCard.vue | 141 +++++++++--------- .../components/projects/JoinedGroupCard.vue | 57 ++----- .../src/components/projects/ProjectCard.vue | 79 +--------- .../src/components/projects/ProjectInfo.vue | 38 +++++ .../src/components/projects/ProjectMeter.vue | 66 ++++++++ .../components/projects/SubmissionCard.vue | 70 ++++----- .../TeacherAssistantCard.vue | 4 +- .../AssistantCourseAddButton.vue | 0 .../TeacherCourseAddButton.vue | 0 frontend/src/router/router.ts | 6 +- .../test/unit/services/course_service.test.ts | 88 +++++------ .../test/unit/services/group_service.test.ts | 16 +- .../unit/services/project_service.test.ts | 40 ++--- .../unit/services/structure_check.test.ts | 16 +- frontend/src/test/unit/types/course.test.ts | 10 +- .../test/unit/types/structureCheck.test.ts | 4 +- frontend/src/types/Course.ts | 14 +- frontend/src/types/Group.ts | 28 +++- frontend/src/types/Project.ts | 65 +++++++- frontend/src/types/StructureCheck.ts | 4 +- frontend/src/types/submission/Submission.ts | 32 ++++ .../courses/roles/AssistantCourseView.vue | 15 +- .../views/courses/roles/StudentCourseView.vue | 19 ++- .../views/courses/roles/TeacherCourseView.vue | 18 ++- .../dashboard/roles/TeacherDashboardView.vue | 2 +- frontend/src/views/projects/ProjectView.vue | 66 ++++---- frontend/src/views/projects/ProjectsView.vue | 47 ++++++ .../src/views/projects/SingleProjectView.vue | 41 ----- .../projects/roles/StudentProjectView.vue | 118 ++++++++------- .../projects/roles/TeacherProjectView.vue | 66 +++++--- package-lock.json | 6 - 35 files changed, 693 insertions(+), 499 deletions(-) create mode 100644 frontend/src/components/projects/ProjectInfo.vue create mode 100644 frontend/src/components/projects/ProjectMeter.vue rename frontend/src/components/teachers_assistants/{add_button => button}/AssistantCourseAddButton.vue (100%) rename frontend/src/components/teachers_assistants/{add_button => button}/TeacherCourseAddButton.vue (100%) create mode 100644 frontend/src/views/projects/ProjectsView.vue delete mode 100644 frontend/src/views/projects/SingleProjectView.vue delete mode 100644 package-lock.json diff --git a/development.sh b/development.sh index 6bea4173..71c6f45a 100755 --- a/development.sh +++ b/development.sh @@ -95,7 +95,7 @@ if [ "$data" != "" ]; then rm -f db.sqlite3 > /dev/null 2>&1 echo "Setting up workspace..." - python -m venv .venv_dev > /dev/null + python3 -m venv .venv_dev > /dev/null source .venv_dev/bin/activate pip install poetry > /dev/null poetry install > /dev/null diff --git a/frontend/src/assets/lang/nl.json b/frontend/src/assets/lang/nl.json index dd8a15b3..971dfbf4 100644 --- a/frontend/src/assets/lang/nl.json +++ b/frontend/src/assets/lang/nl.json @@ -48,9 +48,14 @@ "coming": "Aankomende deadlines", "deadline": "Deadline", "days": "Vandaag om {hour} | Morgen om {hour} | Over {count} dagen", + "groupName": "Groepsnaam", + "groupPopulation": "Grootte", + "groupStatus": "Status", "start": "Startdatum", "submissionStatus": "Indienstatus", "group": "Groep", + "groupSize": "Individueel | Groepen van {count} personen", + "noGroups": "Geen groepen beschikbaar", "groupMembers": "Groepsleden", "chooseGroup": "Kies een groep", "joinGroup": "Kies groep", @@ -72,6 +77,7 @@ "submit": "Indienen", "course": "Vak", "chooseFile": "Kies bestand(en)", + "noSubmissions": "Geen indieningen beschikbaar", "hoverText": { "allChecksFailed": "Structuur en extra checks gefaald", "allChecksPassed": "Alle checks geslaagd", @@ -147,9 +153,8 @@ "open": "Details", "newProject": "Nieuw project", "noSubmissions": "Dit project heeft geen indieningen", - "singleSubmission": "Indiening", - "multipleSubmissions": "Indieningen", - "singleGroup": "Groep", + "submissions": "Indiening | Indieningen", + "groups": "Groep | Groepen", "multipleGroups": "Groepen", "testsSucceed": "Geslaagde testen", "testsFail": "Gefaalde testen", diff --git a/frontend/src/components/courses/CourseList.vue b/frontend/src/components/courses/CourseList.vue index 6efce56e..099c6e42 100644 --- a/frontend/src/components/courses/CourseList.vue +++ b/frontend/src/components/courses/CourseList.vue @@ -14,8 +14,8 @@ import { watch } from 'vue'; /* Props */ interface Props { detail?: boolean; - courses: Course[] | null; cols?: number; + courses: Course[] | null; } withDefaults(defineProps(), { diff --git a/frontend/src/components/layout/Title.vue b/frontend/src/components/layout/Title.vue index 44d88f1b..8419de77 100644 --- a/frontend/src/components/layout/Title.vue +++ b/frontend/src/components/layout/Title.vue @@ -16,6 +16,7 @@ withDefaults(defineProps<{ color?: 'primary' | 'contrast' }>(), { color: var(--primary-color); position: relative; display: inline-block; + line-height: 2.8rem; &::after { content: ''; diff --git a/frontend/src/components/projects/ChooseGroupCard.vue b/frontend/src/components/projects/ChooseGroupCard.vue index 134d760f..025c76aa 100644 --- a/frontend/src/components/projects/ChooseGroupCard.vue +++ b/frontend/src/components/projects/ChooseGroupCard.vue @@ -1,23 +1,25 @@ - + diff --git a/frontend/src/components/projects/JoinedGroupCard.vue b/frontend/src/components/projects/JoinedGroupCard.vue index c9ad893c..a9425c63 100644 --- a/frontend/src/components/projects/JoinedGroupCard.vue +++ b/frontend/src/components/projects/JoinedGroupCard.vue @@ -1,6 +1,5 @@ - + diff --git a/frontend/src/components/projects/ProjectCard.vue b/frontend/src/components/projects/ProjectCard.vue index 16fde590..b259df7c 100644 --- a/frontend/src/components/projects/ProjectCard.vue +++ b/frontend/src/components/projects/ProjectCard.vue @@ -1,20 +1,14 @@ + + + + diff --git a/frontend/src/components/projects/ProjectMeter.vue b/frontend/src/components/projects/ProjectMeter.vue new file mode 100644 index 00000000..58d5a8c5 --- /dev/null +++ b/frontend/src/components/projects/ProjectMeter.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/components/projects/SubmissionCard.vue b/frontend/src/components/projects/SubmissionCard.vue index c199e450..bf33f9c6 100644 --- a/frontend/src/components/projects/SubmissionCard.vue +++ b/frontend/src/components/projects/SubmissionCard.vue @@ -1,22 +1,18 @@ -@/types/Project diff --git a/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue b/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue index 48bf0b52..bbb6d98a 100644 --- a/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue +++ b/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue @@ -3,8 +3,8 @@ import Card from 'primevue/card'; import { type User } from '@/types/users/User.ts'; import { type Course } from '@/types/Course'; import { useI18n } from 'vue-i18n'; -import TeacherCourseAddButton from '@/components/teachers_assistants/add_button/TeacherCourseAddButton.vue'; -import AssistantCourseAddButton from '@/components/teachers_assistants/add_button/AssistantCourseAddButton.vue'; +import TeacherCourseAddButton from '@/components/teachers_assistants/button/TeacherCourseAddButton.vue'; +import AssistantCourseAddButton from '@/components/teachers_assistants/button/AssistantCourseAddButton.vue'; import { useAuthStore } from '@/store/authentication.store.ts'; import { storeToRefs } from 'pinia'; diff --git a/frontend/src/components/teachers_assistants/add_button/AssistantCourseAddButton.vue b/frontend/src/components/teachers_assistants/button/AssistantCourseAddButton.vue similarity index 100% rename from frontend/src/components/teachers_assistants/add_button/AssistantCourseAddButton.vue rename to frontend/src/components/teachers_assistants/button/AssistantCourseAddButton.vue diff --git a/frontend/src/components/teachers_assistants/add_button/TeacherCourseAddButton.vue b/frontend/src/components/teachers_assistants/button/TeacherCourseAddButton.vue similarity index 100% rename from frontend/src/components/teachers_assistants/add_button/TeacherCourseAddButton.vue rename to frontend/src/components/teachers_assistants/button/TeacherCourseAddButton.vue diff --git a/frontend/src/router/router.ts b/frontend/src/router/router.ts index 5bcb0b00..0fc69ee0 100644 --- a/frontend/src/router/router.ts +++ b/frontend/src/router/router.ts @@ -17,9 +17,9 @@ import CreateProjectView from '@/views/projects/CreateProjectView.vue'; import UpdateProjectView from '@/views/projects/UpdateProjectView.vue'; import SearchCourseView from '@/views/courses/SearchCourseView.vue'; import SubmissionView from '@/views/submissions/SubmissionView.vue'; -import SingleProjectView from '@/views/projects/SingleProjectView.vue'; import AdminView from '@/views/admin/AdminView.vue'; import UsersView from '@/views/admin/UsersView.vue'; +import ProjectsView from '@/views/projects/ProjectsView.vue'; const routes: RouteRecordRaw[] = [ // Authentication @@ -60,7 +60,7 @@ const routes: RouteRecordRaw[] = [ { path: ':projectId', children: [ - { path: '', component: SingleProjectView, name: 'course-project' }, + { path: '', component: ProjectView, name: 'course-project' }, { path: 'edit', component: UpdateProjectView, name: 'project-edit' }, { path: 'group', @@ -94,7 +94,7 @@ const routes: RouteRecordRaw[] = [ { path: '/calendar', component: CalendarView, name: 'calendar' }, // Projects - { path: '/projects', component: ProjectView, name: 'projects' }, + { path: '/projects', component: ProjectsView, name: 'projects' }, // Users { diff --git a/frontend/src/test/unit/services/course_service.test.ts b/frontend/src/test/unit/services/course_service.test.ts index 6b6d4fb5..bbbb81de 100644 --- a/frontend/src/test/unit/services/course_service.test.ts +++ b/frontend/src/test/unit/services/course_service.test.ts @@ -36,10 +36,10 @@ describe('course', (): void => { expect(course.value?.parent_course).toBeNull(); expect(course.value?.academic_startyear).toBe(2023); expect(course.value?.description).toBe('Math course'); - expect(course.value?.students).toEqual([]); - expect(course.value?.teachers).toEqual([]); - expect(course.value?.assistants).toEqual([]); - expect(course.value?.projects).toEqual([]); + expect(course.value?.students).toBeNull(); + expect(course.value?.teachers).toBeNull(); + expect(course.value?.assistants).toBeNull(); + expect(course.value?.projects).toBeNull(); }); it('gets courses data', async () => { @@ -51,64 +51,64 @@ describe('course', (): void => { expect(courses.value?.[0]?.parent_course).toBeNull(); expect(courses.value?.[0]?.academic_startyear).toBe(2023); expect(courses.value?.[0]?.description).toBe('Math course'); - expect(courses.value?.[0]?.students).toEqual([]); - expect(courses.value?.[0]?.teachers).toEqual([]); - expect(courses.value?.[0]?.assistants).toEqual([]); - expect(courses.value?.[0]?.projects).toEqual([]); + expect(courses.value?.[0]?.students).toBeNull(); + expect(courses.value?.[0]?.teachers).toBeNull(); + expect(courses.value?.[0]?.assistants).toBeNull(); + expect(courses.value?.[0]?.projects).toBeNull(); expect(courses.value?.[1]?.name).toBe('Sel2'); expect(courses.value?.[1]?.parent_course).toBe('3'); expect(courses.value?.[1]?.academic_startyear).toBe(2023); expect(courses.value?.[1]?.description).toBe('Software course'); - expect(courses.value?.[1]?.students).toEqual([]); - expect(courses.value?.[1]?.teachers).toEqual([]); - expect(courses.value?.[1]?.assistants).toEqual([]); - expect(courses.value?.[1]?.projects).toEqual([]); + expect(courses.value?.[1]?.students).toBeNull(); + expect(courses.value?.[1]?.teachers).toBeNull(); + expect(courses.value?.[1]?.assistants).toBeNull(); + expect(courses.value?.[1]?.projects).toBeNull(); expect(courses.value?.[2]?.name).toBe('Sel1'); expect(courses.value?.[2]?.parent_course).toBeNull(); expect(courses.value?.[2]?.academic_startyear).toBe(2022); expect(courses.value?.[2]?.description).toBe('Software course'); - expect(courses.value?.[2]?.students).toEqual([]); - expect(courses.value?.[2]?.teachers).toEqual([]); - expect(courses.value?.[2]?.assistants).toEqual([]); - expect(courses.value?.[2]?.projects).toEqual([]); + expect(courses.value?.[2]?.students).toBeNull(); + expect(courses.value?.[2]?.teachers).toBeNull(); + expect(courses.value?.[2]?.assistants).toBeNull(); + expect(courses.value?.[2]?.projects).toBeNull(); expect(courses.value?.[3]?.name).toBe('Math'); expect(courses.value?.[3]?.parent_course).toBe('1'); expect(courses.value?.[3]?.academic_startyear).toBe(2024); expect(courses.value?.[3]?.description).toBe('Math course'); - expect(courses.value?.[3]?.students).toEqual([]); - expect(courses.value?.[3]?.teachers).toEqual([]); - expect(courses.value?.[3]?.assistants).toEqual([]); - expect(courses.value?.[3]?.projects).toEqual([]); + expect(courses.value?.[3]?.students).toBeNull(); + expect(courses.value?.[3]?.teachers).toBeNull(); + expect(courses.value?.[3]?.assistants).toBeNull(); + expect(courses.value?.[3]?.projects).toBeNull(); expect(courses.value?.[4]?.name).toBe('Math'); expect(courses.value?.[4]?.parent_course).toBe('12'); expect(courses.value?.[4]?.academic_startyear).toBe(2025); expect(courses.value?.[4]?.description).toBe('Math course'); - expect(courses.value?.[4]?.students).toEqual([]); - expect(courses.value?.[4]?.teachers).toEqual([]); - expect(courses.value?.[4]?.assistants).toEqual([]); - expect(courses.value?.[4]?.projects).toEqual([]); + expect(courses.value?.[4]?.students).toBeNull(); + expect(courses.value?.[4]?.teachers).toBeNull(); + expect(courses.value?.[4]?.assistants).toBeNull(); + expect(courses.value?.[4]?.projects).toBeNull(); expect(courses.value?.[5]?.name).toBe('Club brugge'); expect(courses.value?.[5]?.parent_course).toBeNull(); expect(courses.value?.[5]?.academic_startyear).toBe(2023); expect(courses.value?.[5]?.description).toBeNull(); - expect(courses.value?.[5]?.students).toEqual([]); - expect(courses.value?.[5]?.teachers).toEqual([]); - expect(courses.value?.[5]?.assistants).toEqual([]); - expect(courses.value?.[5]?.projects).toEqual([]); + expect(courses.value?.[5]?.students).toBeNull(); + expect(courses.value?.[5]?.teachers).toBeNull(); + expect(courses.value?.[5]?.assistants).toBeNull(); + expect(courses.value?.[5]?.projects).toBeNull(); expect(courses.value?.[6]?.name).toBe('vergeet barbara'); expect(courses.value?.[6]?.parent_course).toBeNull(); expect(courses.value?.[6]?.academic_startyear).toBe(2023); expect(courses.value?.[6]?.description).toBeNull(); - expect(courses.value?.[6]?.students).toEqual([]); - expect(courses.value?.[6]?.teachers).toEqual([]); - expect(courses.value?.[6]?.assistants).toEqual([]); - expect(courses.value?.[6]?.projects).toEqual([]); + expect(courses.value?.[6]?.students).toBeNull(); + expect(courses.value?.[6]?.teachers).toBeNull(); + expect(courses.value?.[6]?.assistants).toBeNull(); + expect(courses.value?.[6]?.projects).toBeNull(); }); it('gets courses data by student', async () => { @@ -123,28 +123,28 @@ describe('course', (): void => { expect(courses.value?.[0]?.parent_course).toBeNull(); expect(courses.value?.[0]?.academic_startyear).toBe(2023); expect(courses.value?.[0]?.description).toBe('Math course'); - expect(courses.value?.[0]?.students).toEqual([]); - expect(courses.value?.[0]?.teachers).toEqual([]); - expect(courses.value?.[0]?.assistants).toEqual([]); - expect(courses.value?.[0]?.projects).toEqual([]); + expect(courses.value?.[0]?.students).toBeNull(); + expect(courses.value?.[0]?.teachers).toBeNull(); + expect(courses.value?.[0]?.assistants).toBeNull(); + expect(courses.value?.[0]?.projects).toBeNull(); expect(courses.value?.[1]?.name).toBe('Sel2'); expect(courses.value?.[1]?.parent_course).toBe('3'); expect(courses.value?.[1]?.academic_startyear).toBe(2023); expect(courses.value?.[1]?.description).toBe('Software course'); - expect(courses.value?.[1]?.students).toEqual([]); - expect(courses.value?.[1]?.teachers).toEqual([]); - expect(courses.value?.[1]?.assistants).toEqual([]); - expect(courses.value?.[1]?.projects).toEqual([]); + expect(courses.value?.[1]?.students).toBeNull(); + expect(courses.value?.[1]?.teachers).toBeNull(); + expect(courses.value?.[1]?.assistants).toBeNull(); + expect(courses.value?.[1]?.projects).toBeNull(); expect(courses.value?.[2]?.name).toBe('Sel1'); expect(courses.value?.[2]?.parent_course).toBeNull(); expect(courses.value?.[2]?.academic_startyear).toBe(2022); expect(courses.value?.[2]?.description).toBe('Software course'); - expect(courses.value?.[2]?.students).toEqual([]); - expect(courses.value?.[2]?.teachers).toEqual([]); - expect(courses.value?.[2]?.assistants).toEqual([]); - expect(courses.value?.[2]?.projects).toEqual([]); + expect(courses.value?.[2]?.students).toBeNull(); + expect(courses.value?.[2]?.teachers).toBeNull(); + expect(courses.value?.[2]?.assistants).toBeNull(); + expect(courses.value?.[2]?.projects).toBeNull(); }); it('create course', async () => { diff --git a/frontend/src/test/unit/services/group_service.test.ts b/frontend/src/test/unit/services/group_service.test.ts index ddac4993..39e56aee 100644 --- a/frontend/src/test/unit/services/group_service.test.ts +++ b/frontend/src/test/unit/services/group_service.test.ts @@ -32,8 +32,8 @@ describe('group', (): void => { expect(group.value?.score).toBe(20); expect(group.value?.id).toBe('0'); expect(group.value?.project).toBeNull(); - expect(group.value?.students).toEqual([]); - expect(group.value?.submissions).toEqual([]); + expect(group.value?.students).toBeNull(); + expect(group.value?.submissions).toBeNull(); }); it('gets groups data by project', async () => { @@ -48,15 +48,15 @@ describe('group', (): void => { expect(groups.value?.[0]?.score).toBe(20); expect(groups.value?.[0]?.id).toBe('0'); expect(groups.value?.[0]?.project).toBeNull(); - expect(groups.value?.[0]?.students).toEqual([]); - expect(groups.value?.[0]?.submissions).toEqual([]); + expect(groups.value?.[0]?.students).toBeNull(); + expect(groups.value?.[0]?.submissions).toBeNull(); expect(groups.value?.[1]).not.toBeNull(); expect(groups.value?.[1]?.score).toBe(18); expect(groups.value?.[1]?.id).toBe('1'); expect(groups.value?.[1]?.project).toBeNull(); - expect(groups.value?.[1]?.students).toEqual([]); - expect(groups.value?.[1]?.submissions).toEqual([]); + expect(groups.value?.[1]?.students).toBeNull(); + expect(groups.value?.[1]?.submissions).toBeNull(); }); it('gets groups data by student', async () => { @@ -71,8 +71,8 @@ describe('group', (): void => { expect(groups.value?.[0]?.score).toBe(20); expect(groups.value?.[0]?.id).toBe('0'); expect(groups.value?.[0]?.project).toBeNull(); - expect(groups.value?.[0]?.students).toEqual([]); - expect(groups.value?.[0]?.submissions).toEqual([]); + expect(groups.value?.[0]?.students).toBeNull(); + expect(groups.value?.[0]?.submissions).toBeNull(); }); it('create group', async () => { diff --git a/frontend/src/test/unit/services/project_service.test.ts b/frontend/src/test/unit/services/project_service.test.ts index e657bfc8..dce64142 100644 --- a/frontend/src/test/unit/services/project_service.test.ts +++ b/frontend/src/test/unit/services/project_service.test.ts @@ -43,10 +43,10 @@ describe('project', (): void => { expect(project.value?.score_visible).toBe(true); expect(project.value?.group_size).toBe(8); expect(project.value?.course.id).toBe('1'); - expect(project.value?.structureChecks).toEqual([]); - expect(project.value?.extra_checks).toEqual([]); - expect(project.value?.groups).toEqual([]); - expect(project.value?.submissions).toEqual([]); + expect(project.value?.structureChecks).toBeNull(); + expect(project.value?.extra_checks).toBeNull(); + expect(project.value?.groups).toBeNull(); + expect(project.value?.submissions).toBeNull(); }); it('gets projects data by course', async () => { @@ -70,10 +70,10 @@ describe('project', (): void => { expect(projects.value?.[0]?.score_visible).toBe(true); expect(projects.value?.[0]?.group_size).toBe(8); expect(projects.value?.[0]?.course.id).toBe('1'); - expect(projects.value?.[0]?.structureChecks).toEqual([]); - expect(projects.value?.[0]?.extra_checks).toEqual([]); - expect(projects.value?.[0]?.groups).toEqual([]); - expect(projects.value?.[0]?.submissions).toEqual([]); + expect(projects.value?.[0]?.structureChecks).toBeNull(); + expect(projects.value?.[0]?.extra_checks).toBeNull(); + expect(projects.value?.[0]?.groups).toBeNull(); + expect(projects.value?.[0]?.submissions).toBeNull(); expect(projects.value?.[1]?.name).toBe('sel3'); expect(projects.value?.[1].course).toBeInstanceOf(Course); @@ -88,10 +88,10 @@ describe('project', (): void => { expect(projects.value?.[1]?.score_visible).toBe(false); expect(projects.value?.[1]?.group_size).toBe(3); expect(projects.value?.[1]?.course.id).toBe('1'); - expect(projects.value?.[1]?.structureChecks).toEqual([]); - expect(projects.value?.[1]?.extra_checks).toEqual([]); - expect(projects.value?.[1]?.groups).toEqual([]); - expect(projects.value?.[1]?.submissions).toEqual([]); + expect(projects.value?.[1]?.structureChecks).toBeNull(); + expect(projects.value?.[1]?.extra_checks).toBeNull(); + expect(projects.value?.[1]?.groups).toBeNull(); + expect(projects.value?.[1]?.submissions).toBeNull(); }); it('gets projects data', async () => { @@ -113,10 +113,10 @@ describe('project', (): void => { expect(projects.value?.[0]?.score_visible).toBe(true); expect(projects.value?.[0]?.group_size).toBe(8); expect(projects.value?.[0]?.course.id).toBe('1'); - expect(projects.value?.[0]?.structureChecks).toEqual([]); - expect(projects.value?.[0]?.extra_checks).toEqual([]); - expect(projects.value?.[0]?.groups).toEqual([]); - expect(projects.value?.[0]?.submissions).toEqual([]); + expect(projects.value?.[0]?.structureChecks).toBeNull(); + expect(projects.value?.[0]?.extra_checks).toBeNull(); + expect(projects.value?.[0]?.groups).toBeNull(); + expect(projects.value?.[0]?.submissions).toBeNull(); expect(projects.value?.[1]?.name).toBe('sel3'); expect(projects.value?.[1]?.course.id).toBe('1'); @@ -129,10 +129,10 @@ describe('project', (): void => { expect(projects.value?.[1]?.max_score).toBe(20); expect(projects.value?.[1]?.score_visible).toBe(false); expect(projects.value?.[1]?.group_size).toBe(3); - expect(projects.value?.[1]?.structureChecks).toEqual([]); - expect(projects.value?.[1]?.extra_checks).toEqual([]); - expect(projects.value?.[1]?.groups).toEqual([]); - expect(projects.value?.[1]?.submissions).toEqual([]); + expect(projects.value?.[1]?.structureChecks).toBeNull(); + expect(projects.value?.[1]?.extra_checks).toBeNull(); + expect(projects.value?.[1]?.groups).toBeNull(); + expect(projects.value?.[1]?.submissions).toBeNull(); }); it('create project', async () => { diff --git a/frontend/src/test/unit/services/structure_check.test.ts b/frontend/src/test/unit/services/structure_check.test.ts index 35f47179..91a596c0 100644 --- a/frontend/src/test/unit/services/structure_check.test.ts +++ b/frontend/src/test/unit/services/structure_check.test.ts @@ -40,22 +40,22 @@ describe('structureCheck', (): void => { expect(structureChecks.value?.[0]?.name).toBe('.'); expect(structureChecks.value?.[0]?.project).toBeNull(); - expect(structureChecks.value?.[0]?.obligated_extensions).toEqual([]); - expect(structureChecks.value?.[0]?.blocked_extensions).toEqual([]); + expect(structureChecks.value?.[0]?.obligated_extensions).toBeNull(); + expect(structureChecks.value?.[0]?.blocked_extensions).toBeNull(); expect(structureChecks.value?.[1]?.name).toBe('folder1'); expect(structureChecks.value?.[1]?.project).toBeNull(); - expect(structureChecks.value?.[1]?.obligated_extensions).toEqual([]); - expect(structureChecks.value?.[1]?.blocked_extensions).toEqual([]); + expect(structureChecks.value?.[1]?.obligated_extensions).toBeNull(); + expect(structureChecks.value?.[1]?.blocked_extensions).toBeNull(); expect(structureChecks.value?.[2]?.name).toBe('folder3'); expect(structureChecks.value?.[2]?.project).toBeNull(); - expect(structureChecks.value?.[2]?.obligated_extensions).toEqual([]); - expect(structureChecks.value?.[2]?.blocked_extensions).toEqual([]); + expect(structureChecks.value?.[2]?.obligated_extensions).toBeNull(); + expect(structureChecks.value?.[2]?.blocked_extensions).toBeNull(); expect(structureChecks.value?.[3]?.name).toBe('folder3/folder3-1'); expect(structureChecks.value?.[3]?.project).toBeNull(); - expect(structureChecks.value?.[3]?.obligated_extensions).toEqual([]); - expect(structureChecks.value?.[3]?.blocked_extensions).toEqual([]); + expect(structureChecks.value?.[3]?.obligated_extensions).toBeNull(); + expect(structureChecks.value?.[3]?.blocked_extensions).toBeNull(); }); }); diff --git a/frontend/src/test/unit/types/course.test.ts b/frontend/src/test/unit/types/course.test.ts index 40ad5c6d..baf5ff09 100644 --- a/frontend/src/test/unit/types/course.test.ts +++ b/frontend/src/test/unit/types/course.test.ts @@ -33,11 +33,11 @@ describe('course type', () => { expect(course.description).toBe(courseData.description); expect(course.academic_startyear).toBe(courseData.academic_startyear); expect(course.parent_course).toBe(courseData.parent_course); - expect(course.faculty).toBe(courseData.faculty); - expect(course.teachers).toStrictEqual(courseData.teachers); - expect(course.assistants).toStrictEqual(courseData.assistants); - expect(course.students).toStrictEqual(courseData.students); - expect(course.projects).toStrictEqual(courseData.projects); + expect(course.faculty).toBeNull(); + expect(course.teachers).toBeNull(); + expect(course.assistants).toBeNull(); + expect(course.students).toBeNull(); + expect(course.projects).toBeNull(); }); it('getCourseYear method', () => { diff --git a/frontend/src/test/unit/types/structureCheck.test.ts b/frontend/src/test/unit/types/structureCheck.test.ts index b8707ed4..fbc16724 100644 --- a/frontend/src/test/unit/types/structureCheck.test.ts +++ b/frontend/src/test/unit/types/structureCheck.test.ts @@ -23,8 +23,8 @@ describe('structureCheck type', () => { expect(structureCheck).toBeInstanceOf(StructureCheck); expect(structureCheck.id).toBe(structureCheckData.id); expect(structureCheck.name).toBe(structureCheckData.name); - expect(structureCheck.obligated_extensions).toStrictEqual(structureCheckData.obligated_extensions); - expect(structureCheck.blocked_extensions).toStrictEqual(structureCheckData.blocked_extensions); + expect(structureCheck.obligated_extensions).toBeNull(); + expect(structureCheck.blocked_extensions).toBeNull(); expect(structureCheck.project).toStrictEqual(structureCheckData.project); }); }); diff --git a/frontend/src/types/Course.ts b/frontend/src/types/Course.ts index 79ee12c8..52526db7 100644 --- a/frontend/src/types/Course.ts +++ b/frontend/src/types/Course.ts @@ -13,10 +13,10 @@ export class Course { public academic_startyear: number, public parent_course: Course | null = null, public faculty: Faculty | null = null, - public teachers: Teacher[] = [], - public assistants: Assistant[] = [], - public students: Student[] = [], - public projects: Project[] = [], + public teachers: Teacher[] | null = null, + public assistants: Assistant[] | null = null, + public students: Student[] | null = null, + public projects: Project[] | null = null, ) {} /** @@ -61,7 +61,8 @@ export class Course { * @param teacher */ public hasTeacher(teacher: Teacher): boolean { - return this.teachers.some((t) => t.id === teacher.id); + const teachers = this.teachers ?? []; + return teachers.some((t) => t.id === teacher.id); } /** @@ -69,7 +70,8 @@ export class Course { * @param assistant */ public hasAssistant(assistant: Assistant): boolean { - return this.assistants.some((a) => a.id === assistant.id); + const assistants = this.assistants ?? []; + return assistants.some((a) => a.id === assistant.id); } } diff --git a/frontend/src/types/Group.ts b/frontend/src/types/Group.ts index 15bc7545..c6810a09 100644 --- a/frontend/src/types/Group.ts +++ b/frontend/src/types/Group.ts @@ -7,10 +7,34 @@ export class Group { public id: string, public score: number = -1, public project: Project | null = null, - public students: Student[] = [], - public submissions: Submission[] = [], + public students: Student[] | null = null, + public submissions: Submission[] | null = null, ) {} + /** + * Check if the group is locked. + * + * @returns {boolean} + */ + public isLocked(): boolean { + const students = this.students ?? []; + + if (this.project !== null) { + return students.length >= this.project?.group_size || this.project.isLocked(); + } + + return true; + } + + /** + * Get the size of the group. + * + * @returns {number} The size of the group. + */ + public getSize(): number { + return this.students?.length ?? 0; + } + /** * Convert a group object to a group instance. * diff --git a/frontend/src/types/Project.ts b/frontend/src/types/Project.ts index ec30fae0..a68dc8f1 100644 --- a/frontend/src/types/Project.ts +++ b/frontend/src/types/Project.ts @@ -20,10 +20,10 @@ export class Project { public group_size: number, public course: Course | null = null, public structure_file: File | null = null, - public structureChecks: StructureCheck[] = [], - public extra_checks: ExtraCheck[] = [], - public groups: Group[] = [], - public submissions: Submission[] = [], + public structureChecks: StructureCheck[] | null = null, + public extra_checks: ExtraCheck[] | null = null, + public groups: Group[] | null = null, + public submissions: Submission[] | null = null, ) {} /** @@ -37,6 +37,63 @@ export class Project { return Math.min(100, Math.round(100 - (now / max) * 100)); } + /** + * Get the formatted start date of the project. + * + * @returns The formatted start date of the project. + */ + public getFormattedStartDate(): string { + return moment(this.start_date).format('DD MMMM YYYY'); + } + + /** + * Get the formatted deadline hour of the project. + * + * @returns The formatted deadline hour of the project. + */ + public getFormattedDeadlineHour(): string { + return moment(this.deadline).format('HH:mm'); + } + + /** + * Get the days left until the deadline of the project. + * + * @returns The days left until the deadline of the project. + */ + public getDaysLeft(): number { + return moment(this.deadline).diff(moment(), 'days'); + } + + /** + * Get the formatted deadline of the project. + * + * @returns The formatted deadline of the project. + */ + public getFormattedDeadline(): string { + return moment(this.deadline).format('DD MMMM YYYY'); + } + + /** + * Get the group number of a group. + * + * @param group The group to get the number of. + * @returns The number of the group. + */ + public getGroupNumber(group: Group): number { + const groups = this.groups ?? []; + + return groups.sort((a, b) => parseInt(a.id) - parseInt(b.id)).findIndex((g) => g.id === group.id) + 1; + } + + /** + * Check if the project is locked. + * + * @returns True if the project is locked, false otherwise. + */ + public isLocked(): boolean { + return !this.visible || this.archived || this.locked_groups || moment(this.start_date).isBefore(); + } + /** * Convert a project object to a project instance. * diff --git a/frontend/src/types/StructureCheck.ts b/frontend/src/types/StructureCheck.ts index 9b2dc569..36452641 100644 --- a/frontend/src/types/StructureCheck.ts +++ b/frontend/src/types/StructureCheck.ts @@ -5,8 +5,8 @@ export class StructureCheck { constructor( public id: string, public name: string, - public obligated_extensions: File_extension[] = [], - public blocked_extensions: File_extension[] = [], + public obligated_extensions: File_extension[] | null = null, + public blocked_extensions: File_extension[] | null = null, public project: Project | null = null, ) {} diff --git a/frontend/src/types/submission/Submission.ts b/frontend/src/types/submission/Submission.ts index f34529ab..ed826879 100644 --- a/frontend/src/types/submission/Submission.ts +++ b/frontend/src/types/submission/Submission.ts @@ -12,6 +12,38 @@ export class Submission { public is_valid: boolean, ) {} + /** + * Check if the submission is passed. + */ + public isPassed(): boolean { + return this.isExtraCheckPassed() && this.isStructureCheckPassed(); + } + + /** + * Check if some of the checks is passed. + */ + public isSomePassed(): boolean { + return this.isExtraCheckPassed() || this.isStructureCheckPassed(); + } + + /** + * Check if the extra check is passed. + */ + public isExtraCheckPassed(): boolean { + const extraChecksPassed = this.extraCheckResults.map((check: ExtraCheckResult) => check.result === 'SUCCESS'); + return extraChecksPassed.every((check: boolean) => check); + } + + /** + * Check if the structure check is passed. + */ + public isStructureCheckPassed(): boolean { + const structureChecksPassed = this.structureCheckResults.map( + (check: StructureCheckResult) => check.result === 'SUCCESS', + ); + return structureChecksPassed.every((check: boolean) => check); + } + /** * Convert a submission object to a submission instance. * diff --git a/frontend/src/views/courses/roles/AssistantCourseView.vue b/frontend/src/views/courses/roles/AssistantCourseView.vue index ec0a437a..3fb579e2 100644 --- a/frontend/src/views/courses/roles/AssistantCourseView.vue +++ b/frontend/src/views/courses/roles/AssistantCourseView.vue @@ -5,7 +5,7 @@ import TeacherAssistantList from '@/components/teachers_assistants/TeacherAssist import { type Course } from '@/types/Course.ts'; import { useI18n } from 'vue-i18n'; import ProjectCreateButton from '@/components/projects/ProjectCreateButton.vue'; -import { watch } from 'vue'; +import { computed, watch } from 'vue'; import { useProject } from '@/composables/services/project.service.ts'; /* Props */ @@ -13,6 +13,15 @@ const props = defineProps<{ course: Course; }>(); +/* State */ +const instructors = computed(() => { + if (props.course.teachers !== null && props.course.assistants !== null) { + return props.course.teachers.concat(props.course.assistants); + } + + return null; +}); + /* Composable injections */ const { t } = useI18n(); const { projects, getProjectsByCourse } = useProject(); @@ -33,7 +42,7 @@ watch( {{ props.course.name }} -
+
@@ -52,7 +61,7 @@ watch(
- + diff --git a/frontend/src/views/courses/roles/StudentCourseView.vue b/frontend/src/views/courses/roles/StudentCourseView.vue index 1c44c84d..e6c3ba05 100644 --- a/frontend/src/views/courses/roles/StudentCourseView.vue +++ b/frontend/src/views/courses/roles/StudentCourseView.vue @@ -12,7 +12,7 @@ import { useAuthStore } from '@/store/authentication.store.ts'; import { storeToRefs } from 'pinia'; import { useRouter } from 'vue-router'; import { useProject } from '@/composables/services/project.service.ts'; -import { watch } from 'vue'; +import { computed, watch } from 'vue'; /* Props */ const props = defineProps<{ @@ -28,7 +28,18 @@ const { refreshUser } = useAuthStore(); const { projects, getProjectsByCourse } = useProject(); const { push } = useRouter(); -/* Methods */ +/* State */ +const instructors = computed(() => { + if (props.course.teachers !== null && props.course.assistants !== null) { + return props.course.teachers.concat(props.course.assistants); + } + + return null; +}); + +/** + * Leave the course as a student. + */ async function leaveCourse(): Promise { // Show a confirmation dialog before leaving the course, to prevent accidental clicks confirm.require({ @@ -65,7 +76,7 @@ watch( {{ props.course.name }}
-
+
@@ -80,7 +91,7 @@ watch(
- +
diff --git a/frontend/src/views/courses/roles/TeacherCourseView.vue b/frontend/src/views/courses/roles/TeacherCourseView.vue index ed46ba7f..6ac28968 100644 --- a/frontend/src/views/courses/roles/TeacherCourseView.vue +++ b/frontend/src/views/courses/roles/TeacherCourseView.vue @@ -15,7 +15,7 @@ import { RouterLink } from 'vue-router'; import { PrimeIcons } from 'primevue/api'; import { useCourses } from '@/composables/services/course.service'; import { useProject } from '@/composables/services/project.service.ts'; -import { ref, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; /* Props */ const props = defineProps<{ @@ -28,11 +28,22 @@ const { t } = useI18n(); const { cloneCourse } = useCourses(); const { projects, getProjectsByCourse } = useProject(); +/* State */ +const instructors = computed(() => { + if (props.course.teachers !== null && props.course.assistants !== null) { + return props.course.teachers.concat(props.course.assistants); + } + + return null; +}); + /* State for the confirm dialog to clone a course */ const cloneAssistants = ref(false); const cloneTeachers = ref(false); -/* Methods */ +/** + * Clones the course with the given ID. + */ async function handleClone(): Promise { // Show a confirmation dialog before cloning the course confirm.require({ @@ -109,6 +120,7 @@ watch(
+
@@ -131,7 +143,7 @@ watch(
- + diff --git a/frontend/src/views/dashboard/roles/TeacherDashboardView.vue b/frontend/src/views/dashboard/roles/TeacherDashboardView.vue index 618db000..514493ce 100644 --- a/frontend/src/views/dashboard/roles/TeacherDashboardView.vue +++ b/frontend/src/views/dashboard/roles/TeacherDashboardView.vue @@ -48,7 +48,7 @@ watch(