From cd25196b6025b699a4a4837aefeb629a8caa894d Mon Sep 17 00:00:00 2001 From: EwoutV Date: Mon, 15 Apr 2024 18:10:28 +0200 Subject: [PATCH 01/16] feat: endpoint for eagerly fetching all projects for students/teachers/assistants --- backend/api/serializers/project_serializer.py | 36 ++++----- backend/api/views/assistant_view.py | 17 +++- backend/api/views/student_view.py | 16 ++++ backend/api/views/teacher_view.py | 16 ++++ .../src/components/projects/ProjectList.vue | 43 ++--------- .../composables/services/assistant.service.ts | 17 +--- .../composables/services/project.service.ts | 38 +++++---- .../composables/services/student.service.ts | 17 +--- .../composables/services/teacher.service.ts | 17 +--- frontend/src/config/endpoints.ts | 5 +- frontend/src/types/Project.ts | 8 +- frontend/src/views/calendar/CalendarView.vue | 77 +++++-------------- .../roles/AssistantDashboardView.vue | 5 +- .../dashboard/roles/StudentDashboardView.vue | 5 +- .../dashboard/roles/TeacherDashboardView.vue | 5 +- frontend/src/views/projects/ProjectView.vue | 49 ++++++------ 16 files changed, 157 insertions(+), 214 deletions(-) diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index 7c6833c0..1d299a5b 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -1,9 +1,10 @@ - from api.logic.check_folder_structure import parse_zip_file from api.models.checks import FileExtension +from api.models.course import Course from api.models.group import Group from api.models.project import Project from api.serializers.checks_serializer import StructureCheckSerializer +from api.serializers.course_serializer import CourseSerializer from api.serializers.submission_serializer import SubmissionSerializer from django.conf import settings from django.core.files.storage import FileSystemStorage @@ -14,11 +15,8 @@ class ProjectSerializer(serializers.ModelSerializer): - course = serializers.HyperlinkedRelatedField( - many=False, - view_name="course-detail", - read_only=True - ) + # We want the course to be eager loaded + course = CourseSerializer(read_only=True) structure_checks = serializers.HyperlinkedIdentityField( view_name="project-structure-checks", @@ -40,11 +38,8 @@ class ProjectSerializer(serializers.ModelSerializer): read_only=True ) - class Meta: - model = Project - fields = "__all__" - def validate(self, data): + """Validate the serializer data""" if not self.partial: # Only require course if it is not a partial update if "course" in self.context: @@ -56,7 +51,7 @@ def validate(self, data): if "start_date" in data and data["start_date"] < timezone.now().replace(hour=0, minute=0, second=0): raise ValidationError(gettext("project.errors.start_date_in_past")) - # Set the start date depending if it is a partial update and whether it was given by the user + # Set the start date depending on if it is a partial update and whether it was given by the user if "start_date" not in data: if self.partial: start_date = self.instance.start_date @@ -66,14 +61,15 @@ def validate(self, data): start_date = data["start_date"] # Check if deadline of the project is before the start date - - # Data will always contain start_date if it's not a partial update. Same goes for deadline - start_date = data["start_date"] if "start_date" in data else self.instance.start_date if "deadline" in data and data["deadline"] < start_date: raise ValidationError(gettext("project.errors.deadline_before_start_date")) return data + class Meta: + model = Project + fields = "__all__" + class CreateProjectSerializer(ProjectSerializer): number_groups = serializers.IntegerField(min_value=1, required=False) @@ -106,7 +102,7 @@ def create(self, validated_data): # If a zip_structure is provided, parse it to create the structure checks if zip_structure is not None: - # Define tje temporary storage location + # Define the temporary storage location temp_storage = FileSystemStorage(location=settings.MEDIA_ROOT) # Save the file to the temporary location temp_file_path = temp_storage.save(f"tmp/{zip_structure.name}", zip_structure) @@ -155,20 +151,20 @@ def validate(self, data): obl_ext = set() for ext in self.context["obligated"]: - extensie, _ = FileExtension.objects.get_or_create( + extension, _ = FileExtension.objects.get_or_create( extension=ext ) - obl_ext.add(extensie) + obl_ext.add(extension) data["obligated_extensions"] = obl_ext block_ext = set() for ext in self.context["blocked"]: - extensie, _ = FileExtension.objects.get_or_create( + extension, _ = FileExtension.objects.get_or_create( extension=ext ) - if extensie in obl_ext: + if extension in obl_ext: raise ValidationError(gettext("project.error.structure_checks.extension_blocked_and_obligated")) - block_ext.add(extensie) + block_ext.add(extension) data["blocked_extensions"] = block_ext return data diff --git a/backend/api/views/assistant_view.py b/backend/api/views/assistant_view.py index c4cdc200..8d8fb4fb 100644 --- a/backend/api/views/assistant_view.py +++ b/backend/api/views/assistant_view.py @@ -5,15 +5,17 @@ from rest_framework.viewsets import ModelViewSet from rest_framework.permissions import IsAdminUser from drf_yasg.utils import swagger_auto_schema + +from api.models.project import Project from api.permissions.assistant_permissions import AssistantPermission from api.models.assistant import Assistant from api.serializers.assistant_serializer import AssistantSerializer, AssistantIDSerializer from api.serializers.course_serializer import CourseSerializer +from api.serializers.project_serializer import ProjectSerializer from authentication.serializers import UserIDSerializer class AssistantViewSet(ModelViewSet): - queryset = Assistant.objects.all() serializer_class = AssistantSerializer permission_classes = [IsAdminUser | AssistantPermission] @@ -53,3 +55,16 @@ def courses(self, request, **_): ) return Response(serializer.data) + + @action(detail=True) + def projects(self, request: Request, **_) -> Response: + """Returns a list of projects for the given assistant""" + assistant = self.get_object() + projects = Project.objects.filter(course__in=assistant.courses.all()).select_related('course') + + # Serialize the project objects + serializer = ProjectSerializer( + projects, many=True, context={"request": request} + ) + + return Response(serializer.data) diff --git a/backend/api/views/student_view.py b/backend/api/views/student_view.py index ff1fba92..0ccd5ef8 100644 --- a/backend/api/views/student_view.py +++ b/backend/api/views/student_view.py @@ -5,8 +5,11 @@ from rest_framework.response import Response from rest_framework.permissions import IsAdminUser from drf_yasg.utils import swagger_auto_schema + +from api.models.project import Project from api.permissions.student_permissions import StudentPermission from api.models.student import Student +from api.serializers.project_serializer import ProjectSerializer from api.serializers.student_serializer import StudentSerializer, CreateStudentSerializer, StudentIDSerializer from api.serializers.course_serializer import CourseSerializer from api.serializers.group_serializer import GroupSerializer @@ -66,3 +69,16 @@ def groups(self, request, **_): groups, many=True, context={"request": request} ) return Response(serializer.data) + + @action(detail=True) + def projects(self, request: Request, **_) -> Response: + """Returns a list of projects for the given student""" + student = self.get_object() + projects = Project.objects.filter(course__in=student.courses.all()).select_related('course') + + # Serialize the project objects + serializer = ProjectSerializer( + projects, many=True, context={"request": request} + ) + + return Response(serializer.data) diff --git a/backend/api/views/teacher_view.py b/backend/api/views/teacher_view.py index fcb63414..284ceda0 100644 --- a/backend/api/views/teacher_view.py +++ b/backend/api/views/teacher_view.py @@ -6,7 +6,10 @@ from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAuthenticated from drf_yasg.utils import swagger_auto_schema + +from api.models.project import Project from api.models.teacher import Teacher +from api.serializers.project_serializer import ProjectSerializer from api.serializers.teacher_serializer import TeacherSerializer, TeacherIDSerializer from api.serializers.course_serializer import CourseSerializer from api.permissions.teacher_permissions import TeacherPermission @@ -53,3 +56,16 @@ def courses(self, request, pk=None): ) return Response(serializer.data) + + @action(detail=True) + def projects(self, request: Request, **_) -> Response: + """Returns a list of projects for the given teacher""" + teacher = self.get_object() + projects = Project.objects.filter(course__in=teacher.courses.all()).select_related('course') + + # Serialize the project objects + serializer = ProjectSerializer( + projects, many=True, context={"request": request} + ) + + return Response(serializer.data) diff --git a/frontend/src/components/projects/ProjectList.vue b/frontend/src/components/projects/ProjectList.vue index 68cfd2f2..adf5acad 100644 --- a/frontend/src/components/projects/ProjectList.vue +++ b/frontend/src/components/projects/ProjectList.vue @@ -1,17 +1,16 @@ diff --git a/frontend/src/composables/services/assistant.service.ts b/frontend/src/composables/services/assistant.service.ts index 829699c2..11b8a748 100644 --- a/frontend/src/composables/services/assistant.service.ts +++ b/frontend/src/composables/services/assistant.service.ts @@ -4,7 +4,6 @@ import { Response } from '@/types/Response'; import { type Ref, ref } from 'vue'; import { endpoints } from '@/config/endpoints.ts'; import { get, getList, create, deleteId, deleteIdWithData } from '@/composables/services/helpers.ts'; -import { useCourses } from '@/composables/services/course.service.ts'; interface AssistantState { assistants: Ref; @@ -25,16 +24,9 @@ export function useAssistant(): AssistantState { const assistant = ref(null); const response = ref(null); - /* Nested state */ - const { courses, getCourseByAssistant } = useCourses(); - - async function getAssistantByID(id: string, init: boolean = false): Promise { + async function getAssistantByID(id: string): Promise { const endpoint = endpoints.assistants.retrieve.replace('{id}', id); await get(endpoint, assistant, Assistant.fromJSON); - - if (init) { - await initAssistant(assistant.value); - } } async function getAssistantsByCourse(courseId: string): Promise { @@ -74,13 +66,6 @@ export function useAssistant(): AssistantState { await deleteId(endpoint, assistant, Assistant.fromJSON); } - async function initAssistant(assistant: Assistant | null): Promise { - if (assistant !== null) { - await getCourseByAssistant(assistant.id); - assistant.courses = courses.value ?? []; - } - } - return { assistants, assistant, diff --git a/frontend/src/composables/services/project.service.ts b/frontend/src/composables/services/project.service.ts index ad118c11..594f7917 100644 --- a/frontend/src/composables/services/project.service.ts +++ b/frontend/src/composables/services/project.service.ts @@ -1,9 +1,8 @@ import { Project } from '@/types/Project'; -import { Course } from '@/types/Course'; import { type Ref, ref } from 'vue'; import { endpoints } from '@/config/endpoints.ts'; import axios from 'axios'; -import { get, getList, getListMerged, create, deleteId, processError } from '@/composables/services/helpers.ts'; +import { create, deleteId, get, getList, processError } from '@/composables/services/helpers.ts'; interface ProjectState { projects: Ref; @@ -11,6 +10,8 @@ interface ProjectState { getProjectByID: (id: string) => Promise; getProjectsByCourse: (courseId: string) => Promise; getProjectsByStudent: (studentId: string) => Promise; + getProjectsByAssistant: (assistantId: string) => Promise; + getProjectsByTeacher: (teacherId: string) => Promise; getProjectsByCourseAndDeadline: (courseId: string, deadlineDate: Date) => Promise; createProject: (projectData: Project, courseId: string) => Promise; deleteProject: (id: string) => Promise; @@ -31,20 +32,18 @@ export function useProject(): ProjectState { } async function getProjectsByStudent(studentId: string): Promise { - const endpoint = endpoints.courses.byStudent.replace('{studentId}', studentId); - const courses = ref(null); - await getList(endpoint, courses, Course.fromJSON); + const endpoint = endpoints.projects.byStudent.replace('{studentId}', studentId); + await getList(endpoint, projects, Project.fromJSON); + } - const endpList = []; - let coursesValue: Course[] | null = courses.value; - if (coursesValue === null) { - coursesValue = []; - } - for (const course of coursesValue) { - endpList.push(endpoints.projects.byCourse.replace('{courseId}', course.id.toString())); - } + async function getProjectsByAssistant(assistantId: string): Promise { + const endpoint = endpoints.projects.byAssistant.replace('{assistantId}', assistantId); + await getList(endpoint, projects, Project.fromJSON); + } - await getListMerged(endpList, projects, Project.fromJSON); + async function getProjectsByTeacher(teacherId: string): Promise { + const endpoint = endpoints.projects.byTeacher.replace('{teacherId}', teacherId); + await getList(endpoint, projects, Project.fromJSON); } async function getProjectsByCourseAndDeadline(courseId: string, deadlineDate: Date): Promise { @@ -56,13 +55,10 @@ export function useProject(): ProjectState { const allProjects = response.data.map((projectData: Project) => Project.fromJSON(projectData)); // Filter projects based on the deadline date - const projectsWithMatchingDeadline = allProjects.filter((project: Project) => { - const projectDeadlineDate = project.deadline; - return projectDeadlineDate.toDateString() === deadlineDate.toDateString(); - }); - // Update the projects ref with the filtered projects - projects.value = projectsWithMatchingDeadline; + projects.value = allProjects.filter((project: Project) => { + return project.deadline.toDateString() === deadlineDate.toDateString(); + }); }) .catch((error) => { if (axios.isAxiosError(error)) { @@ -109,6 +105,8 @@ export function useProject(): ProjectState { getProjectsByCourse, getProjectsByCourseAndDeadline, getProjectsByStudent, + getProjectsByTeacher, + getProjectsByAssistant, createProject, deleteProject, diff --git a/frontend/src/composables/services/student.service.ts b/frontend/src/composables/services/student.service.ts index 5fe6cf89..86115851 100644 --- a/frontend/src/composables/services/student.service.ts +++ b/frontend/src/composables/services/student.service.ts @@ -3,7 +3,6 @@ import { Response } from '@/types/Response'; import { type Ref, ref } from 'vue'; import { endpoints } from '@/config/endpoints.ts'; import { get, getList, create, deleteId, deleteIdWithData } from '@/composables/services/helpers.ts'; -import { useCourses } from '@/composables/services/course.service.ts'; interface StudentsState { students: Ref; @@ -27,16 +26,9 @@ export function useStudents(): StudentsState { const student = ref(null); const response = ref(null); - /* Nested state */ - const { courses, getCoursesByStudent } = useCourses(); - - async function getStudentByID(id: string, init: boolean = false): Promise { + async function getStudentByID(id: string): Promise { const endpoint = endpoints.students.retrieve.replace('{id}', id); await get(endpoint, student, Student.fromJSON); - - if (init) { - await initStudent(student.value); - } } async function getStudents(): Promise { @@ -93,13 +85,6 @@ export function useStudents(): StudentsState { await deleteId(endpoint, student, Student.fromJSON); } - async function initStudent(student: Student | null): Promise { - if (student !== null) { - await getCoursesByStudent(student.id); - student.courses = courses.value ?? []; - } - } - return { students, student, diff --git a/frontend/src/composables/services/teacher.service.ts b/frontend/src/composables/services/teacher.service.ts index 03daf28c..3c6f956b 100644 --- a/frontend/src/composables/services/teacher.service.ts +++ b/frontend/src/composables/services/teacher.service.ts @@ -5,7 +5,6 @@ import { Response } from '@/types/Response'; import { type Ref, ref } from 'vue'; import { endpoints } from '@/config/endpoints.ts'; import { get, getList, create, deleteId, deleteIdWithData } from '@/composables/services/helpers.ts'; -import { useCourses } from '@/composables/services/course.service.ts'; interface TeacherState { teachers: Ref; @@ -26,16 +25,9 @@ export function useTeacher(): TeacherState { const teacher = ref(null); const response = ref(null); - /* Nested state */ - const { courses, getCoursesByTeacher } = useCourses(); - - async function getTeacherByID(id: string, init: boolean = false): Promise { + async function getTeacherByID(id: string): Promise { const endpoint = endpoints.teachers.retrieve.replace('{id}', id); await get(endpoint, teacher, Teacher.fromJSON); - - if (init) { - await initTeacher(teacher.value); - } } async function getTeachersByCourse(courseId: string): Promise { @@ -75,13 +67,6 @@ export function useTeacher(): TeacherState { await deleteId(endpoint, teacher, Teacher.fromJSON); } - async function initTeacher(teacher: Teacher | null): Promise { - if (teacher !== null) { - await getCoursesByTeacher(teacher.id); - teacher.courses = courses.value ?? []; - } - } - return { teachers, teacher, diff --git a/frontend/src/config/endpoints.ts b/frontend/src/config/endpoints.ts index e1152735..3ee49439 100644 --- a/frontend/src/config/endpoints.ts +++ b/frontend/src/config/endpoints.ts @@ -13,10 +13,10 @@ export const endpoints = { index: '/api/courses/', search: '/api/courses/search/', retrieve: '/api/courses/{id}/', + clone: '/api/courses/{courseId}/clone/', byStudent: '/api/students/{studentId}/courses/', byTeacher: '/api/teachers/{teacherId}/courses/', byAssistant: '/api/assistants/{assistantId}/courses/', - clone: '/api/courses/{courseId}/clone/', }, students: { index: '/api/students/', @@ -50,6 +50,9 @@ export const endpoints = { projects: { retrieve: '/api/projects/{id}', byCourse: '/api/courses/{courseId}/projects/', + byStudent: '/api/students/{studentId}/projects/', + byTeacher: '/api/teachers/{teacherId}/projects/', + byAssistant: '/api/assistants/{assistantId}/projects/', }, submissions: { retrieve: '/api/submissions/{id}', diff --git a/frontend/src/types/Project.ts b/frontend/src/types/Project.ts index d9eed542..29732170 100644 --- a/frontend/src/types/Project.ts +++ b/frontend/src/types/Project.ts @@ -1,4 +1,4 @@ -import { type Course } from './Course.ts'; +import { Course } from './Course.ts'; import { type ExtraCheck } from './ExtraCheck.ts'; import { type Group } from './Group.ts'; import { type StructureCheck } from './StructureCheck.ts'; @@ -17,9 +17,8 @@ export class Project { public max_score: number, public score_visible: boolean, public group_size: number, - + public course: Course, public structure_file: File | null = null, - public course: Course | null = null, public structureChecks: StructureCheck[] = [], public extra_checks: ExtraCheck[] = [], public groups: Group[] = [], @@ -32,6 +31,8 @@ export class Project { * @param project */ static fromJSON(project: Project): Project { + const course = Course.fromJSON(project.course); + return new Project( project.id, project.name, @@ -44,6 +45,7 @@ export class Project { project.max_score, project.score_visible, project.group_size, + course, ); } } diff --git a/frontend/src/views/calendar/CalendarView.vue b/frontend/src/views/calendar/CalendarView.vue index 90d9e1fd..d13ddce3 100644 --- a/frontend/src/views/calendar/CalendarView.vue +++ b/frontend/src/views/calendar/CalendarView.vue @@ -11,8 +11,8 @@ import { useI18n } from 'vue-i18n'; import { useAuthStore } from '@/store/authentication.store.ts'; import { storeToRefs } from 'pinia'; import { type Project } from '@/types/Project.ts'; -import { type RoleUser } from '@/types/users/Generics.ts'; import { useRoute, useRouter } from 'vue-router'; +import { useCourses } from '@/composables/services/course.service.ts'; /* Composable injections */ const { t, locale } = useI18n(); @@ -20,12 +20,12 @@ const { query } = useRoute(); const { push } = useRouter(); /* Component state */ -const allProjects = ref(null); const selectedDate = ref(getQueryDate()); /* Service injection */ const { user } = storeToRefs(useAuthStore()); -const { projects, getProjectsByCourse } = useProject(); +const { courses, getCoursesByTeacher, getCourseByAssistant } = useCourses(); +const { projects, getProjectsByTeacher, getProjectsByAssistant, getProjectsByStudent } = useProject(); /* Formatted date */ const formattedDate = computed(() => { @@ -34,53 +34,33 @@ const formattedDate = computed(() => { }); /* Filter the projects on the date selected on the calendar */ -const projectsWithDeadline = computed(() => { +const projectsWithDeadline = computed(() => { return ( - allProjects.value?.filter((project) => { + projects.value?.filter((project) => { return moment(project.deadline).isSame(moment(selectedDate.value), 'day'); }) ?? null ); }); -/* Courses that take place on the selected date in the calendar => no display when the date is in the past */ -const coursesWithProjectCreationPossibility = computed(() => { - if (user.value !== null && moment(selectedDate.value).isAfter(moment())) { - return (user.value as RoleUser).courses.filter((course) => { - return course.academic_startyear === selectedAcademicYear(); - }); - } else { - return []; - } -}); - /** * Load the projects of the user. */ async function loadProjects(): Promise { if (user.value !== null) { - // Clear the old data, so that the data from another role is not displayed - allProjects.value = null; - - // Load the projects of the courses - if (user.value.isSpecificRole()) { - let _allProjects: Project[] = []; + projects.value = null; - // Cast the generic user to a specific role - const role = user.value as RoleUser; - - for (const course of role.courses) { - await getProjectsByCourse(course.id); - - // Assign the course to the project - projects.value?.forEach((project) => { - project.course = course; - }); + if (user.value.isStudent()) { + await getProjectsByStudent(user.value.id); + } - // Concatenate the projects - _allProjects = _allProjects.concat(projects.value ?? []); - } + if (user.value.isTeacher()) { + await getProjectsByTeacher(user.value.id); + await getCoursesByTeacher(user.value.id); + } - allProjects.value = _allProjects; + if (user.value.isAssistant()) { + await getProjectsByAssistant(user.value.id); + await getCourseByAssistant(user.value.id); } } } @@ -113,7 +93,7 @@ function hasDeadline(date: CalendarDateSlotOptions): boolean { const dateObj = new Date(date.year, date.month, date.day); return ( - allProjects.value?.some((project) => { + projects.value?.some((project) => { return moment(project.deadline).isSame(moment(dateObj), 'day'); }) ?? false ); @@ -128,24 +108,12 @@ function countDeadlines(date: CalendarDateSlotOptions): number { const dateObj = new Date(date.year, date.month, date.day); return ( - allProjects.value?.filter((project) => { + projects.value?.filter((project) => { return moment(project.deadline).isSame(moment(dateObj), 'day'); }).length ?? 0 ); } -/** - * Get the academic year of the selected date. - * - * @returns The academic year of the selected date. - */ -const selectedAcademicYear = (): number => { - const selectedYear = moment(selectedDate.value).year(); - const selectedMonth = moment(selectedDate.value).month(); - - return selectedMonth < 8 ? selectedYear - 1 : selectedYear; -}; - /* Watch the user and load the projects */ watch( user, @@ -236,18 +204,13 @@ watch(selectedDate, (date) => { - diff --git a/frontend/src/views/dashboard/roles/StudentDashboardView.vue b/frontend/src/views/dashboard/roles/StudentDashboardView.vue index 11a52de8..9093cd32 100644 --- a/frontend/src/views/dashboard/roles/StudentDashboardView.vue +++ b/frontend/src/views/dashboard/roles/StudentDashboardView.vue @@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'; import { computed, ref, watch } from 'vue'; import { useCourses } from '@/composables/services/course.service.ts'; import { getAcademicYear, getAcademicYears } from '@/types/Course.ts'; +import { useProject } from '@/composables/services/project.service.ts'; /* Props */ const props = defineProps<{ @@ -16,6 +17,7 @@ const props = defineProps<{ /* Composable injections */ const { t } = useI18n(); +const { projects, getProjectsByStudent } = useProject(); const { courses, getCoursesByStudent } = useCourses(); /* State */ @@ -31,6 +33,7 @@ watch( props.student, () => { getCoursesByStudent(props.student.id); + getProjectsByStudent(props.student.id); }, { immediate: true, @@ -58,7 +61,7 @@ watch( - + diff --git a/frontend/src/views/dashboard/roles/TeacherDashboardView.vue b/frontend/src/views/dashboard/roles/TeacherDashboardView.vue index fb3de675..7c06c294 100644 --- a/frontend/src/views/dashboard/roles/TeacherDashboardView.vue +++ b/frontend/src/views/dashboard/roles/TeacherDashboardView.vue @@ -12,6 +12,7 @@ import { useI18n } from 'vue-i18n'; import { computed, ref, watch } from 'vue'; import { useCourses } from '@/composables/services/course.service.ts'; import { getAcademicYear, getAcademicYears } from '@/types/Course.ts'; +import { useProject } from '@/composables/services/project.service.ts'; /* Props */ const props = defineProps<{ @@ -20,6 +21,7 @@ const props = defineProps<{ /* Composable injections */ const { t } = useI18n(); +const { projects, getProjectsByTeacher } = useProject(); const { courses, getCoursesByTeacher } = useCourses(); /* State */ @@ -35,6 +37,7 @@ watch( props.teacher, () => { getCoursesByTeacher(props.teacher.id); + getProjectsByTeacher(props.teacher.id); }, { immediate: true, @@ -78,7 +81,7 @@ watch( - + diff --git a/frontend/src/views/projects/ProjectView.vue b/frontend/src/views/projects/ProjectView.vue index 2f8e25d5..2a16923e 100644 --- a/frontend/src/views/projects/ProjectView.vue +++ b/frontend/src/views/projects/ProjectView.vue @@ -2,46 +2,45 @@ import BaseLayout from '@/components/layout/BaseLayout.vue'; import ProjectList from '@/components/projects/ProjectList.vue'; import Title from '@/components/layout/Title.vue'; -import YearSelector from '@/components/YearSelector.vue'; -import { useCourses } from '@/composables/services/course.service.ts'; -import { computed, onMounted, ref } from 'vue'; +import { watch } from 'vue'; import { storeToRefs } from 'pinia'; import { useAuthStore } from '@/store/authentication.store.ts'; import { useI18n } from 'vue-i18n'; -import { getAcademicYear, getAcademicYears } from '@/types/Course.ts'; +import { useProject } from '@/composables/services/project.service.ts'; /* Composable injections */ const { t } = useI18n(); const { user } = storeToRefs(useAuthStore()); -const { courses, getCoursesByStudent } = useCourses(); +const { projects, getProjectsByStudent, getProjectsByTeacher, getProjectsByAssistant } = useProject(); -/* State */ -const selectedYear = ref(getAcademicYear()); -const allYears = computed(() => getAcademicYears(...(courses.value?.map((course) => course.academic_startyear) ?? []))); +watch( + user, + async () => { + if (user.value !== null) { + projects.value = null; -onMounted(async () => { - if (user.value?.id != null) { - await getCoursesByStudent(user.value.id); - } -}); + if (user.value.isStudent()) { + await getProjectsByStudent(user.value.id); + } -const filteredCourses = computed( - () => courses.value?.filter((course) => course.academic_startyear === selectedYear.value) ?? [], + if (user.value.isTeacher()) { + await getProjectsByTeacher(user.value.id); + } + + if (user.value.isAssistant()) { + await getProjectsByAssistant(user.value.id); + } + } + }, + { immediate: true }, ); From 12da6aa57c128c4558e6e6aaf0864ce5e468ca54 Mon Sep 17 00:00:00 2001 From: EwoutV Date: Mon, 15 Apr 2024 18:22:18 +0200 Subject: [PATCH 02/16] fix: tests --- backend/api/tests/test_group.py | 6 +----- backend/api/tests/test_project.py | 30 +++++------------------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/backend/api/tests/test_group.py b/backend/api/tests/test_group.py index ead8611f..39c9aeb7 100644 --- a/backend/api/tests/test_group.py +++ b/backend/api/tests/test_group.py @@ -80,15 +80,11 @@ def test_group_project(self): # Parse the JSON content from the response content_json = json.loads(response.content.decode("utf-8")) - expected_course_url = settings.TESTING_BASE_LINK + reverse( - "course-detail", args=[str(course.id)] - ) - 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"], expected_course_url) + self.assertEqual(content_json["course"]["id"], course.id) def test_group_students(self): """Able to retrieve students details of a group.""" diff --git a/backend/api/tests/test_project.py b/backend/api/tests/test_project.py index e482ebd0..edd98586 100644 --- a/backend/api/tests/test_project.py +++ b/backend/api/tests/test_project.py @@ -279,15 +279,11 @@ def test_project_exists(self): retrieved_project = content_json - expected_course_url = settings.TESTING_BASE_LINK + reverse( - "course-detail", args=[str(course.id)] - ) - self.assertEqual(retrieved_project["name"], project.name) self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - self.assertEqual(retrieved_project["course"], expected_course_url) + self.assertEqual(content_json["course"]["id"], course.id) def test_project_course(self): """ @@ -319,21 +315,9 @@ def test_project_course(self): self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - - response = self.client.get(retrieved_project["course"], follow=True) - - # Check if the response was successful - self.assertEqual(response.status_code, 200) - - # Assert that the response is JSON - self.assertEqual(response.accepted_media_type, "application/json") - - # Parse the JSON content from the response - content_json = json.loads(response.content.decode("utf-8")) - - self.assertEqual(content_json["name"], course.name) - self.assertEqual(content_json["academic_startyear"], course.academic_startyear) - self.assertEqual(content_json["description"], course.description) + self.assertEqual(content_json["course"]["name"], course.name) + self.assertEqual(content_json["course"]["academic_startyear"], course.academic_startyear) + self.assertEqual(content_json["course"]["description"], course.description) def test_project_structure_checks(self): """ @@ -371,15 +355,11 @@ def test_project_structure_checks(self): retrieved_project = content_json - expected_course_url = settings.TESTING_BASE_LINK + reverse( - "course-detail", args=[str(course.id)] - ) - self.assertEqual(retrieved_project["name"], project.name) self.assertEqual(retrieved_project["description"], project.description) self.assertEqual(retrieved_project["visible"], project.visible) self.assertEqual(retrieved_project["archived"], project.archived) - self.assertEqual(retrieved_project["course"], expected_course_url) + self.assertEqual(content_json["course"]["id"], course.id) response = self.client.get(retrieved_project["structure_checks"], follow=True) From 50249977d0291ba8a4c6277fd4075e4619a3d9fe Mon Sep 17 00:00:00 2001 From: EwoutV Date: Tue, 16 Apr 2024 00:28:17 +0200 Subject: [PATCH 03/16] feat: editor for course description and editor, better project overview list --- ...t_alter_checkresult_submission_and_more.py | 35 ++++ backend/api/models/course.py | 3 + backend/api/serializers/course_serializer.py | 6 + backend/api/serializers/project_serializer.py | 3 + backend/poetry.lock | 27 +++- backend/pyproject.toml | 1 + frontend/package-lock.json | 122 +++++++++++--- frontend/package.json | 1 + frontend/src/assets/lang/en.json | 6 +- frontend/src/assets/lang/nl.json | 9 +- .../base/components/misc/_progressbar.scss | 4 +- frontend/src/components/YearSelector.vue | 4 +- .../src/components/courses/CourseList.vue | 2 +- frontend/src/components/forms/Editor.vue | 32 ++++ .../src/components/projects/ProjectCard.vue | 150 ++++++++++++------ .../projects/ProjectCreateButton.vue | 2 +- .../src/components/projects/ProjectList.vue | 122 +++++++++----- .../composables/services/course.service.ts | 1 + frontend/src/main.scss | 4 + frontend/src/main.ts | 2 + frontend/src/test/unit/services/setup/data.ts | 2 +- frontend/src/test/unit/types/course.test.ts | 16 +- frontend/src/test/unit/types/data.ts | 1 + frontend/src/types/Course.ts | 13 +- frontend/src/types/Project.ts | 22 ++- .../src/views/courses/CreateCourseView.vue | 40 ++--- .../src/views/courses/UpdateCourseView.vue | 24 ++- .../courses/roles/AssistantCourseView.vue | 2 +- .../views/courses/roles/StudentCourseView.vue | 2 +- .../views/courses/roles/TeacherCourseView.vue | 2 +- .../dashboard/roles/StudentDashboardView.vue | 4 +- .../src/views/projects/CreateProjectView.vue | 12 +- 32 files changed, 493 insertions(+), 183 deletions(-) create mode 100644 backend/api/migrations/0016_course_excerpt_alter_checkresult_submission_and_more.py create mode 100644 frontend/src/components/forms/Editor.vue diff --git a/backend/api/migrations/0016_course_excerpt_alter_checkresult_submission_and_more.py b/backend/api/migrations/0016_course_excerpt_alter_checkresult_submission_and_more.py new file mode 100644 index 00000000..e9f327a1 --- /dev/null +++ b/backend/api/migrations/0016_course_excerpt_alter_checkresult_submission_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.4 on 2024-04-15 19:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0015_checkresult_remove_extrachecksresult_error_message_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='excerpt', + field=models.CharField(default='no excerpt provided', max_length=200), + preserve_default=False, + ), + migrations.AlterField( + model_name='checkresult', + name='submission', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.submission'), + ), + migrations.AlterField( + model_name='extracheckresult', + name='extra_check', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_check', to='api.extracheck'), + ), + migrations.AlterField( + model_name='structurecheckresult', + name='structure_check', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='structure_check', to='api.structurecheck'), + ), + ] diff --git a/backend/api/models/course.py b/backend/api/models/course.py index 3612827c..045ee711 100644 --- a/backend/api/models/course.py +++ b/backend/api/models/course.py @@ -16,6 +16,9 @@ class Course(models.Model): # Begin year of the academic year academic_startyear = models.IntegerField(blank=False, null=False) + # The excerpt of the course + excerpt = models.CharField(max_length=200, blank=False, null=False) + # The description of the course description = models.TextField(blank=True, null=True) diff --git a/backend/api/serializers/course_serializer.py b/backend/api/serializers/course_serializer.py index 5ed47e72..0345239b 100644 --- a/backend/api/serializers/course_serializer.py +++ b/backend/api/serializers/course_serializer.py @@ -1,3 +1,4 @@ +from nh3 import clean from django.utils.translation import gettext from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -37,6 +38,11 @@ class CourseSerializer(serializers.ModelSerializer): read_only=True ) + def validate(self, attrs: dict) -> dict: + """Extra custom validation for course serializer""" + attrs['description'] = clean(attrs['description']) + return attrs + class Meta: model = Course fields = "__all__" diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index 1d299a5b..623cafc0 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -1,3 +1,4 @@ +from nh3 import clean from api.logic.check_folder_structure import parse_zip_file from api.models.checks import FileExtension from api.models.course import Course @@ -64,6 +65,8 @@ def validate(self, data): if "deadline" in data and data["deadline"] < start_date: raise ValidationError(gettext("project.errors.deadline_before_start_date")) + data['description'] = clean(data['description']) + return data class Meta: diff --git a/backend/poetry.lock b/backend/poetry.lock index f2fa2810..745909d9 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -814,6 +814,31 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "nh3" +version = "0.2.17" +description = "Python bindings to the ammonia HTML sanitization library." +optional = false +python-versions = "*" +files = [ + {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9"}, + {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f"}, + {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71"}, + {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10"}, + {file = "nh3-0.2.17-cp37-abi3-win32.whl", hash = "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911"}, + {file = "nh3-0.2.17-cp37-abi3-win_amd64.whl", hash = "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb"}, + {file = "nh3-0.2.17.tar.gz", hash = "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028"}, +] + [[package]] name = "openapi-codec" version = "1.3.2" @@ -1484,4 +1509,4 @@ brotli = ["Brotli"] [metadata] lock-version = "2.0" python-versions = "^3.11.4" -content-hash = "cc622e4debf22f8b6ea8f22501b0685aac3579d4fc183c382693d7e3d5e90e77" +content-hash = "3c7e6d71b9a32d7ba01569c0ff22452c6d0b7cfcdd165d7b006ca375c8bf89d5" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index b6c4c30a..38e7137d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -29,6 +29,7 @@ django-seed = "^0.3.1" django-celery-results = "^2.5.1" django-polymorphic = "^3.1.0" django-rest-polymorphic = "^0.1.10" +nh3 = "^0.2.17" [build-system] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 332f22c7..7661561c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primevue": "^3.50.0", + "quill": "^1.3.7", "vue": "^3.4.18", "vue-i18n": "^9.10.2", "vue-router": "^4.3.0", @@ -2354,7 +2355,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2584,6 +2584,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2890,6 +2898,25 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2900,7 +2927,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2917,7 +2943,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -3083,7 +3108,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -3095,7 +3119,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -3727,6 +3750,11 @@ "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -3765,8 +3793,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extract-zip": { "version": "2.0.1", @@ -4038,7 +4065,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4065,7 +4091,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4091,7 +4116,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -4291,7 +4315,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -4340,7 +4363,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -4352,7 +4374,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4364,7 +4385,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4376,7 +4396,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -4391,7 +4410,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4594,6 +4612,21 @@ "node": ">= 0.4" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -4720,7 +4753,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4836,7 +4868,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -5572,11 +5603,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5745,6 +5790,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6158,6 +6208,37 @@ } ] }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/quill-delta/node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -6180,7 +6261,6 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, "dependencies": { "call-bind": "^1.0.6", "define-properties": "^1.2.1", @@ -6477,7 +6557,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -6494,7 +6573,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", diff --git a/frontend/package.json b/frontend/package.json index 7e95e033..3ebd1184 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primevue": "^3.50.0", + "quill": "^1.3.7", "vue": "^3.4.18", "vue-i18n": "^9.10.2", "vue-router": "^4.3.0", diff --git a/frontend/src/assets/lang/en.json b/frontend/src/assets/lang/en.json index be59f82d..7b4c1468 100644 --- a/frontend/src/assets/lang/en.json +++ b/frontend/src/assets/lang/en.json @@ -23,8 +23,8 @@ "dashboard": { "courses": "My courses", "projects": "Current projects", - "no_projects": "No projects available for this academic year.", - "no_courses": "No courses available for this academic year.", + "noProjects": "No projects available for this academic year.", + "noCourses": "No courses available for this academic year.", "select_course": "Select the course for which you want to create a project:", "showPastProjects": "Projects with passed deadline" }, @@ -101,7 +101,7 @@ } }, "components": { - "buttons": { + "button": { "academic_year": "Academic year {0}" }, "card": { diff --git a/frontend/src/assets/lang/nl.json b/frontend/src/assets/lang/nl.json index d2b514a1..e857c56f 100644 --- a/frontend/src/assets/lang/nl.json +++ b/frontend/src/assets/lang/nl.json @@ -23,8 +23,8 @@ "dashboard": { "courses": "Mijn vakken", "projects": "Lopende projecten", - "no_projects": "Geen projecten beschikbaar voor dit academiejaar.", - "no_courses": "Geen vakken beschikbaar voor dit academiejaar.", + "noProjects": "Geen projecten beschikbaar voor dit academiejaar.", + "noCourses": "Geen vakken beschikbaar voor dit academiejaar.", "select_course": "Selecteer het vak waarvoor je een project wil maken:", "showPastProjects": "Projecten met verstreken deadline" }, @@ -43,7 +43,11 @@ "noProjects": "Geen projecten op geselecteerde datum." }, "projects": { + "all": "Alle projecten", + "coming": "Aankomende deadlines", "deadline": "Deadline", + "days": "Vandaag om {hour} | Morgen om {hour} | Over {count} dagen", + "start": "Startdatum", "submissionStatus": "Indienstatus", "group": "Groep", "groupMembers": "Groepsleden", @@ -72,6 +76,7 @@ "edit": "Bewerk vak", "name": "Vaknaam", "description": "Beschrijving", + "excerpt": "Korte beschrijving", "faculty": "Faculteit", "year": "Academiejaar", "search": { diff --git a/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss b/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss index 22971661..dc8bad0b 100644 --- a/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss +++ b/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss @@ -2,13 +2,13 @@ .p-progressbar { position: relative; overflow: hidden; + display: flex; } .p-progressbar-determinate .p-progressbar-value { height: 100%; - width: 0%; + width: 0; position: absolute; - display: none; border: 0 none; display: flex; align-items: center; diff --git a/frontend/src/components/YearSelector.vue b/frontend/src/components/YearSelector.vue index 157c9bbf..06bc81f8 100644 --- a/frontend/src/components/YearSelector.vue +++ b/frontend/src/components/YearSelector.vue @@ -25,12 +25,12 @@ const year = defineModel(); diff --git a/frontend/src/components/forms/Editor.vue b/frontend/src/components/forms/Editor.vue new file mode 100644 index 00000000..62893032 --- /dev/null +++ b/frontend/src/components/forms/Editor.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/components/projects/ProjectCard.vue b/frontend/src/components/projects/ProjectCard.vue index 42c7ad2a..9aa28f84 100644 --- a/frontend/src/components/projects/ProjectCard.vue +++ b/frontend/src/components/projects/ProjectCard.vue @@ -1,11 +1,12 @@ -@/types/Project +µ diff --git a/frontend/src/components/projects/ProjectCreateButton.vue b/frontend/src/components/projects/ProjectCreateButton.vue index 44597f2f..29f24fb9 100644 --- a/frontend/src/components/projects/ProjectCreateButton.vue +++ b/frontend/src/components/projects/ProjectCreateButton.vue @@ -34,7 +34,7 @@ const displayCourseSelection = ref(false); v-model:visible="displayCourseSelection" class="m-3" :draggable="false" - :contentStyle="{ 'min-width': '50vw' }" + :contentStyle="{ 'min-width': '50vw', 'max-width': '1080px' }" modal >