From 16e3050b041f56131985bf55279ae0e793b6add3 Mon Sep 17 00:00:00 2001 From: Marieke Date: Wed, 24 Apr 2024 21:51:50 +0200 Subject: [PATCH 01/81] homescreen layout idea --- frontend/src/App.vue | 6 +++ frontend/src/components/ApolloHeader.vue | 2 +- .../components/home/cards/HomeScreenCard.vue | 4 +- .../home/listcontent/DeadlineItem.vue | 46 ++++++++-------- .../home/listcontent/SubjectItem.vue | 7 +-- .../components/navigation/DropDownList.vue | 2 +- frontend/src/components/navigation/NavBar.vue | 2 +- frontend/src/plugins/vuetify.ts | 10 ++-- frontend/src/views/HomeScreenView.vue | 52 +++++++++---------- 9 files changed, 72 insertions(+), 59 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ce658910..6fc4b7d4 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -31,3 +31,9 @@ onBeforeMount(() => { locale.value = selectedLocale; }); + + diff --git a/frontend/src/components/ApolloHeader.vue b/frontend/src/components/ApolloHeader.vue index 35ea45d3..3587afb4 100644 --- a/frontend/src/components/ApolloHeader.vue +++ b/frontend/src/components/ApolloHeader.vue @@ -1,5 +1,5 @@ @@ -26,6 +28,7 @@ import { toRefs, computed } from "vue"; import type Project from "@/models/Project"; import type Group from "@/models/Group"; import type Submission from "@/models/Submission"; +import { SubmissionStatus } from "@/models/Submission"; const props = defineProps<{ group: Group; @@ -36,17 +39,30 @@ const { group, project } = toRefs(props); const { data: submissions } = useSubmissionQuery(); -const latestSubmission = computed( - () => - submissions.value?.filter( - (submission: Submission) => - submission.group_id === group.value.id && submission.project_id === project.value.id - )[0] -); +const latestSubmission = computed(() => { + // Log all submissions before filtering + console.log("All submissions:", submissions.value); + + // Filter submissions for the specific group and project + const filteredSubmissions = submissions.value?.filter( + (submission: Submission) => + submission.group_id === group.value.id && submission.project_id === project.value.id + ); + + // Sort the filtered submissions by date in descending order + const sortedSubmissions = filteredSubmissions?.sort((a, b) => { + return new Date(b.date).getTime() - new Date(a.date).getTime(); + }); + + // Return the latest submission (first item after sorting) + return sortedSubmissions?.[0]; +}); + +const submissionStatus = computed(() =>{ + SubmissionStatus[latestSubmission?.value.status] + return $t() +}); + - + diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 255d0402..07ef0988 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -27,15 +27,18 @@ export default { latest_submission: "Latest submission:", new_submission: "Submit new", status_submission: "Submission was: {status}", + no_submission: "No submission found", }, project: { deadline: "Deadline", details_button: "Project details", - group_button: "Join Group", + group_button: "Join group", needhelp_button: "Need help", group: "Group {number}", assignment: "Assignment:", myProject: "My projects:", + return_course: "Back to subject", + capacity_group: "Maximum amount of group members: ", }, navigation: { home: "Home", diff --git a/frontend/src/i18n/locales/nl.ts b/frontend/src/i18n/locales/nl.ts index 3ea22ba4..86340b01 100644 --- a/frontend/src/i18n/locales/nl.ts +++ b/frontend/src/i18n/locales/nl.ts @@ -23,19 +23,22 @@ export default { add_files_button: "Bestanden toevoegen", no_files_added: "Je hebt nog geen bestanden toegevoegd.", no_files_warning: "Voeg ten minste een bestand toe om een indiening te maken.", - submissions: "Indiening zone", + submissions: "Indieningszone", latest_submission: "Laatste indiening:", new_submission: "Nieuwe indiening", - status_submission: "Indiening was: {status}", + status_submission: "Indiening is: {status}", + no_submission: "Geen indiening teruggevonden", }, project: { deadline: "Deadline", details_button: "Naar project", - group_button: "Vind Groep", + group_button: "Vind groep", needhelp_button: "Hulp nodig", group: "Groep {number}", assignment: "Opdracht:", myProject: "Mijn projecten:", + return_course: "Terug naar vak", + capacity_group: "Maximaal aantal leden per groep: ", }, navigation: { home: "Hoofdscherm", diff --git a/frontend/src/models/Submission.ts b/frontend/src/models/Submission.ts index c87ad7fe..598f8680 100644 --- a/frontend/src/models/Submission.ts +++ b/frontend/src/models/Submission.ts @@ -3,6 +3,12 @@ export default interface Submission { group_id: number; date: Date; project_id: number; - status: string; + status: number; files_uuid: string; } + +export enum SubmissionStatus { + Denied = 0, + InProgress = 1, + Accepted = 2, +} diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 796a8f92..88dd9402 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -8,12 +8,17 @@ - + + + {{ $t("project.return_course") }} + + + {{ $t("project.group", { number: group!.id }) }} - + {{ $t("project.group_button") }} @@ -53,6 +58,8 @@ const { const isDataLoading = computed(() => isProjectLoading.value || isGroupLoading.value); const isDataError = computed(() => isProjectError.value || isGroupError.value); + +const isSoloProject = computed(() => project.value.capacity === 1); diff --git a/frontend/src/components/project/ProjectInfoCard.vue b/frontend/src/components/project/ProjectInfoCard.vue index 03d5bec2..f8b826dc 100644 --- a/frontend/src/components/project/ProjectInfoCard.vue +++ b/frontend/src/components/project/ProjectInfoCard.vue @@ -3,18 +3,28 @@ {{ project.name }} - + {{ $d(project.deadline, "long") }} - + {{ subject?.name }} + + {{ project.capacity }} + {{ instructor?.given_name }} @@ -24,9 +34,7 @@ {{ $t("project.assignment") }} - - {{ project.description }} - +
@@ -36,6 +44,7 @@ import type Project from "@/models/Project"; import { useSubjectInstructorsQuery, useSubjectQuery } from "@/queries/Subject"; import { computed } from "vue"; import { toRefs } from "vue"; +import { Quill } from "@vueup/vue-quill"; const props = defineProps<{ project: Project; @@ -46,6 +55,12 @@ const { project } = toRefs(props); const { data: subject } = useSubjectQuery(computed(() => project.value.subject_id)); const { data: instructors } = useSubjectInstructorsQuery(computed(() => project.value.subject_id)); + +const renderQuillContent = (content: string) => { + const quill = new Quill(document.createElement("div")); + quill.root.innerHTML = content; + return quill.root.innerHTML; +}; diff --git a/frontend/src/components/project/submit/SubmitInfo.vue b/frontend/src/components/project/submit/SubmitInfo.vue index d269db9a..39007414 100644 --- a/frontend/src/components/project/submit/SubmitInfo.vue +++ b/frontend/src/components/project/submit/SubmitInfo.vue @@ -6,17 +6,19 @@ {{ $t("submit.latest_submission") }} {{ $d(latestSubmission.date, "long") }} - No submissions found. -
- {{ - $t("submit.status_submission", { status: latestSubmission.status }) - }} -
- - - {{ $t("submit.new_submission") }} - - + + {{ $t("submit.no_submission") }} + + + {{ $t("submit.status_submission", { status: SubmissionStatus[latestSubmission.status] }) }} + + + + + {{ $t("submit.new_submission") }} + + + @@ -26,6 +28,7 @@ import { toRefs, computed } from "vue"; import type Project from "@/models/Project"; import type Group from "@/models/Group"; import type Submission from "@/models/Submission"; +import { SubmissionStatus } from "@/models/Submission"; const props = defineProps<{ group: Group; @@ -36,17 +39,30 @@ const { group, project } = toRefs(props); const { data: submissions } = useSubmissionQuery(); -const latestSubmission = computed( - () => - submissions.value?.filter( - (submission: Submission) => - submission.group_id === group.value.id && submission.project_id === project.value.id - )[0] -); +const latestSubmission = computed(() => { + // Log all submissions before filtering + console.log("All submissions:", submissions.value); + + // Filter submissions for the specific group and project + const filteredSubmissions = submissions.value?.filter( + (submission: Submission) => + submission.group_id === group.value.id && submission.project_id === project.value.id + ); + + // Sort the filtered submissions by date in descending order + const sortedSubmissions = filteredSubmissions?.sort((a, b) => { + return new Date(b.date).getTime() - new Date(a.date).getTime(); + }); + + // Return the latest submission (first item after sorting) + return sortedSubmissions?.[0]; +}); + +const submissionStatus = computed(() =>{ + SubmissionStatus[latestSubmission?.value.status] + return $t() +}); + - + diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 255d0402..07ef0988 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -27,15 +27,18 @@ export default { latest_submission: "Latest submission:", new_submission: "Submit new", status_submission: "Submission was: {status}", + no_submission: "No submission found", }, project: { deadline: "Deadline", details_button: "Project details", - group_button: "Join Group", + group_button: "Join group", needhelp_button: "Need help", group: "Group {number}", assignment: "Assignment:", myProject: "My projects:", + return_course: "Back to subject", + capacity_group: "Maximum amount of group members: ", }, navigation: { home: "Home", diff --git a/frontend/src/i18n/locales/nl.ts b/frontend/src/i18n/locales/nl.ts index 3ea22ba4..86340b01 100644 --- a/frontend/src/i18n/locales/nl.ts +++ b/frontend/src/i18n/locales/nl.ts @@ -23,19 +23,22 @@ export default { add_files_button: "Bestanden toevoegen", no_files_added: "Je hebt nog geen bestanden toegevoegd.", no_files_warning: "Voeg ten minste een bestand toe om een indiening te maken.", - submissions: "Indiening zone", + submissions: "Indieningszone", latest_submission: "Laatste indiening:", new_submission: "Nieuwe indiening", - status_submission: "Indiening was: {status}", + status_submission: "Indiening is: {status}", + no_submission: "Geen indiening teruggevonden", }, project: { deadline: "Deadline", details_button: "Naar project", - group_button: "Vind Groep", + group_button: "Vind groep", needhelp_button: "Hulp nodig", group: "Groep {number}", assignment: "Opdracht:", myProject: "Mijn projecten:", + return_course: "Terug naar vak", + capacity_group: "Maximaal aantal leden per groep: ", }, navigation: { home: "Hoofdscherm", diff --git a/frontend/src/models/Submission.ts b/frontend/src/models/Submission.ts index c87ad7fe..598f8680 100644 --- a/frontend/src/models/Submission.ts +++ b/frontend/src/models/Submission.ts @@ -3,6 +3,12 @@ export default interface Submission { group_id: number; date: Date; project_id: number; - status: string; + status: number; files_uuid: string; } + +export enum SubmissionStatus { + Denied = 0, + InProgress = 1, + Accepted = 2, +} diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 796a8f92..88dd9402 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -8,12 +8,17 @@
- + + + {{ $t("project.return_course") }} + + + {{ $t("project.group", { number: group!.id }) }} - + {{ $t("project.group_button") }} @@ -53,6 +58,8 @@ const { const isDataLoading = computed(() => isProjectLoading.value || isGroupLoading.value); const isDataError = computed(() => isProjectError.value || isGroupError.value); + +const isSoloProject = computed(() => project.value.capacity === 1); diff --git a/frontend/src/components/project/submit/SubmitInfo.vue b/frontend/src/components/project/submit/SubmitInfo.vue index 39007414..7727de93 100644 --- a/frontend/src/components/project/submit/SubmitInfo.vue +++ b/frontend/src/components/project/submit/SubmitInfo.vue @@ -10,7 +10,7 @@ {{ $t("submit.no_submission") }} - {{ $t("submit.status_submission", { status: SubmissionStatus[latestSubmission.status] }) }} + {{ $t("submit.status_submission", { status: getSubmissionStatus }) }} @@ -29,19 +29,20 @@ import type Project from "@/models/Project"; import type Group from "@/models/Group"; import type Submission from "@/models/Submission"; import { SubmissionStatus } from "@/models/Submission"; +import {useI18n} from "vue-i18n"; const props = defineProps<{ group: Group; project: Project; }>(); +const { t } = useI18n(); + const { group, project } = toRefs(props); const { data: submissions } = useSubmissionQuery(); const latestSubmission = computed(() => { - // Log all submissions before filtering - console.log("All submissions:", submissions.value); // Filter submissions for the specific group and project const filteredSubmissions = submissions.value?.filter( @@ -58,9 +59,13 @@ const latestSubmission = computed(() => { return sortedSubmissions?.[0]; }); -const submissionStatus = computed(() =>{ - SubmissionStatus[latestSubmission?.value.status] - return $t() +const getSubmissionStatus = computed(() => { + if (latestSubmission.value) { + const statusKey = SubmissionStatus[latestSubmission.value.status]; + return t("submit." + statusKey); + } else { + return ""; + } }); diff --git a/frontend/src/composables/useIsTeacher.ts b/frontend/src/composables/useIsTeacher.ts index 8f052a76..31e5c67c 100644 --- a/frontend/src/composables/useIsTeacher.ts +++ b/frontend/src/composables/useIsTeacher.ts @@ -2,7 +2,7 @@ import { computed } from "vue"; import { useUserQuery } from "@/queries/User"; export default function useIsTeacher() { - const { data: user } = useUserQuery(); + const { data: user } = useUserQuery(null); const isTeacher = computed(() => user.value?.is_teacher || false); return { isTeacher }; } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 07ef0988..742c5cbd 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -26,8 +26,11 @@ export default { submissions: "Submission zone", latest_submission: "Latest submission:", new_submission: "Submit new", - status_submission: "Submission was: {status}", + status_submission: "Submission is: {status}", no_submission: "No submission found", + Denied: "denied", + InProgress: "in progress", + Accepted: "accepted", }, project: { deadline: "Deadline", diff --git a/frontend/src/i18n/locales/nl.ts b/frontend/src/i18n/locales/nl.ts index 86340b01..decd4978 100644 --- a/frontend/src/i18n/locales/nl.ts +++ b/frontend/src/i18n/locales/nl.ts @@ -28,6 +28,9 @@ export default { new_submission: "Nieuwe indiening", status_submission: "Indiening is: {status}", no_submission: "Geen indiening teruggevonden", + Denied: "afgewezen", + InProgress: "in behandeling", + Accepted: "geaccepteerd", }, project: { deadline: "Deadline", diff --git a/frontend/src/models/Subject.ts b/frontend/src/models/Subject.ts index 366a626f..57087a95 100644 --- a/frontend/src/models/Subject.ts +++ b/frontend/src/models/Subject.ts @@ -1,6 +1,9 @@ export default interface Subject { id: number; name: string; + academic_year: string; + uuid: string; + email: string; } export interface UserSubjectList { diff --git a/frontend/src/queries/Subject.ts b/frontend/src/queries/Subject.ts index 6cd286d5..a7d2348c 100644 --- a/frontend/src/queries/Subject.ts +++ b/frontend/src/queries/Subject.ts @@ -11,7 +11,6 @@ import { import { type Ref, computed } from "vue"; import type Subject from "@/models/Subject"; import type User from "@/models/User"; -import type Subject from "@/models/Subject"; import type Project from "@/models/Project"; import type SubjectDetails from "@/models/SubjectDetails"; diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 88dd9402..3daeae2f 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -5,7 +5,7 @@
- + @@ -13,17 +13,22 @@ {{ $t("project.return_course") }} - + {{ $t("project.group", { number: group!.id }) }} - + {{ $t("project.group_button") }} - + + + + {{ $t("project.group", { number: group!.id }) }} + +
@@ -32,11 +37,12 @@ diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 50c42419..70dd4fb3 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -5,7 +5,12 @@
- + @@ -13,17 +18,27 @@ {{ $t("project.return_course") }} - + {{ $t("project.group", { number: group!.id }) }} - + {{ $t("project.group_button") }} - + {{ $t("project.edit") }} @@ -38,11 +53,11 @@ diff --git a/frontend/src/components/project/submit/SubmitInfo.vue b/frontend/src/components/project/submit/SubmitInfo.vue index d269db9a..05939494 100644 --- a/frontend/src/components/project/submit/SubmitInfo.vue +++ b/frontend/src/components/project/submit/SubmitInfo.vue @@ -9,23 +9,26 @@ No submissions found.
{{ - $t("submit.status_submission", { status: latestSubmission.status }) + $t("submit.status_submission", { status: Status[latestSubmission.status] }) }}
- - + + {{ $t("submit.new_submission") }} - + + {{ $t("project.submissions_list") }} + + diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index f1554149..3f371178 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -6,6 +6,7 @@ export default { loading: { loading_page: "Loading...", }, + no: "no", }, login: { about: "The official submission application of Ghent University", @@ -28,6 +29,18 @@ export default { new_submission: "Submit new", status_submission: "Submission was: {status}", }, + submission: { + status: "Submission status:", + datetime: "Submission time:", + remarks: "Remarks", + remarks_empty: "No remarks for this submission", + files: "Files", + download_info: "Click on filename to download", + after_deadline: "After deadline", + submissions_title: "Submissions for project {project}", + no_submissions: "No submissions yet", + docker_test: "Tests Output", + }, project: { deadline: "Deadline", details_button: "Project details", @@ -36,6 +49,7 @@ export default { group: "Group {number}", assignment: "Assignment:", myProject: "My projects:", + submissions_list: "All submissions", not_found: "No projects found.", archived: "Archived", not_finished: "Not Finished", diff --git a/frontend/src/i18n/locales/nl.ts b/frontend/src/i18n/locales/nl.ts index 603bbb73..f6b54120 100644 --- a/frontend/src/i18n/locales/nl.ts +++ b/frontend/src/i18n/locales/nl.ts @@ -6,6 +6,7 @@ export default { loading: { loading_page: "Aan het laden...", }, + no: "geen", }, login: { about: "De officiƫle indienapplicatie van de Universiteit Gent", @@ -28,6 +29,18 @@ export default { new_submission: "Nieuwe indiening", status_submission: "Indiening was: {status}", }, + submission: { + status: "Indiening status: {status}", + datetime: "Indiening tijdstip:", + remarks: "Opmerkingen", + remarks_empty: "Geen opmerkingen voor deze indiening", + files: "Bestanden", + download_info: "Klik op bestandsnaam om te downloaden", + after_deadline: "Na deadline", + submissions_title: "Indieningen voor project {project}", + no_submissions: "Nog geen indieningen", + docker_test: "Testen Output", + }, project: { deadline: "Deadline", details_button: "Naar project", @@ -36,6 +49,7 @@ export default { group: "Groep {number}", assignment: "Opdracht:", myProject: "Mijn projecten:", + submissions_list: "Alle indieningen", not_found: "Geen projecten teruggevonden.", archived: "Gearchiveerd", not_finished: "Onafgewerkt", diff --git a/frontend/src/models/File.ts b/frontend/src/models/File.ts new file mode 100644 index 00000000..c9dacd33 --- /dev/null +++ b/frontend/src/models/File.ts @@ -0,0 +1,4 @@ +export default interface FileInfo { + filename: string; + media_type: string; +} diff --git a/frontend/src/models/Submission.ts b/frontend/src/models/Submission.ts index c87ad7fe..afd7dcf6 100644 --- a/frontend/src/models/Submission.ts +++ b/frontend/src/models/Submission.ts @@ -1,8 +1,24 @@ +export enum Status { + InProgress = 1, + Accepted = 2, + Rejected = 3, + Crashed = 4, +} + +export interface TestResult { + succeeded: boolean; + value: string; +} + export default interface Submission { id: number; group_id: number; date: Date; project_id: number; - status: string; + status: Status; files_uuid: string; + remarks: string; + stdout: string | undefined; + stderr: string | undefined; + testresults: TestResult[]; } diff --git a/frontend/src/queries/Group.ts b/frontend/src/queries/Group.ts index 41c78afb..f3b33b56 100644 --- a/frontend/src/queries/Group.ts +++ b/frontend/src/queries/Group.ts @@ -4,7 +4,15 @@ import type { Ref } from "vue"; import type { UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query"; import { computed } from "vue"; -import { createGroups, getGroupWithProjectId, getUserGroups, joinGroup } from "@/services/group"; +import { + createGroups, + getGroup, + getGroupWithProjectId, + getSubmissions, + getUserGroups, + joinGroup, +} from "@/services/group"; +import type Submission from "@/models/Submission"; function USER_GROUPS_QUERY_KEY(): string[] { return ["groups"]; @@ -14,6 +22,22 @@ function PROJECT_USER_GROUP_QUERY_KEY(projectId: number): (string | number)[] { return ["group", "project", projectId]; } +function GROUP_QUERY_KEY(groupId: number): (string | number)[] { + return ["group", groupId]; +} + +function submissionsQueryKey(groupId: number): (string | number)[] { + return ["submissions", groupId]; +} + +export function useGroupQuery(groupId: Ref): UseQueryReturnType { + return useQuery({ + queryKey: GROUP_QUERY_KEY(groupId.value!), + queryFn: () => getGroup(groupId.value!), + enabled: computed(() => groupId.value !== undefined), + }); +} + /** * Get all groups of the current user * @returns An array of Group objects @@ -37,7 +61,7 @@ export function useUserGroupQuery( return useQuery({ queryKey: computed(() => PROJECT_USER_GROUP_QUERY_KEY(projectId.value!)), queryFn: () => getGroupWithProjectId(groups.value!, projectId.value!), - enabled: () => groups.value !== undefined, + enabled: computed(() => groups.value !== undefined), }); } @@ -80,3 +104,14 @@ export function useJoinGroupMutation(): UseMutationReturnType< }, }); } + +// Hook for fetching all submissions belonging to a group +export function useSubmissionsQuery( + groupId: Ref +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => submissionsQueryKey(groupId.value!)), + queryFn: () => getSubmissions(groupId.value!), + enabled: computed(() => groupId.value !== undefined), + }); +} diff --git a/frontend/src/queries/Project.ts b/frontend/src/queries/Project.ts index 144ad603..22f5e843 100644 --- a/frontend/src/queries/Project.ts +++ b/frontend/src/queries/Project.ts @@ -7,14 +7,7 @@ import { } from "@tanstack/vue-query"; import type Project from "@/models/Project"; import type { ProjectForm } from "@/models/Project"; -import type Submission from "@/models/Submission"; -import { - getProject, - createSubmission, - getSubmissions, - createProject, - getProjects, -} from "@/services/project"; +import { getProject, createProject, getProjects } from "@/services/project"; import { type Ref, computed } from "vue"; // Key generator for project queries @@ -26,10 +19,6 @@ function PROJECTS_QUERY_KEY(): string[] { return ["projects"]; } -function SUBMISSIONS_QUERY_KEY(): string[] { - return ["submissions"]; -} - // Hook for fetching project details export function useProjectQuery( projectId: Ref @@ -48,18 +37,6 @@ export function useProjectsQuery(): UseQueryReturnType { }); } -// Hook for creating a new submission -export function useCreateSubmissionMutation( - groupId: Ref -): UseMutationReturnType { - return useMutation({ - mutationFn: (formData) => createSubmission(groupId.value!, formData), - onError: (error) => { - console.error("Submission creation failed", error); - alert("Could not create submission. Please try again."); - }, - }); -} export function useCreateProjectMutation(): UseMutationReturnType< number, Error, @@ -79,10 +56,3 @@ export function useCreateProjectMutation(): UseMutationReturnType< }, }); } - -export function useSubmissionQuery(): UseQueryReturnType { - return useQuery({ - queryKey: SUBMISSIONS_QUERY_KEY(), - queryFn: () => getSubmissions(), - }); -} diff --git a/frontend/src/queries/Submission.ts b/frontend/src/queries/Submission.ts new file mode 100644 index 00000000..cd0ec4d7 --- /dev/null +++ b/frontend/src/queries/Submission.ts @@ -0,0 +1,56 @@ +import type Submission from "@/models/Submission"; +import { createSubmission, getFiles, getSubmission } from "@/services/submission"; +import { computed, type Ref } from "vue"; +import { + useQuery, + type UseMutationReturnType, + type UseQueryReturnType, + useMutation, +} from "@tanstack/vue-query"; +import type FileInfo from "@/models/File"; + +// Key generator for submission queries +function submissionQueryKey(submissionId: number): (string | number)[] { + return ["submission", submissionId]; +} + +function FILES_QUERY_KEY(submissionId: number): (string | number)[] { + return ["files", submissionId]; +} + +// Hook for fetching submission details +export function useSubmissionQuery( + submissionId: Ref +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => submissionQueryKey(submissionId.value!)), + queryFn: () => getSubmission(submissionId.value!), + enabled: computed(() => submissionId.value !== undefined), + retry: false, + }); +} + +// Hook for fetching all files of a submission +export function useFilesQuery( + submissionId: Ref +): UseQueryReturnType { + return useQuery({ + queryKey: FILES_QUERY_KEY(submissionId.value!), + queryFn: () => getFiles(submissionId.value!), + enabled: computed(() => submissionId.value !== undefined), + retry: false, + }); +} + +// Hook for creating a new submission +export function useCreateSubmissionMutation( + groupId: Ref +): UseMutationReturnType { + return useMutation({ + mutationFn: (formData) => createSubmission(groupId.value!, formData), + onError: (error) => { + console.error("Submission creation failed", error); + alert("Could not create submission. Please try again."); + }, + }); +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 5d3bbbc6..aaccd17a 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -82,6 +82,18 @@ const router = createRouter({ component: () => import("../views/SubjectRegisterView.vue"), props: (route) => ({ uuid: String(route.params.uuid) }), }, + { + path: "/submissions/:submissionId(\\d+)", + name: "submission", + component: () => import("../views/SubmissionView.vue"), + props: (route) => ({ submissionId: Number(route.params.submissionId) }), + }, + { + path: "/groups/:groupId(\\d+)/submissions", + name: "submissions", + component: () => import("../views/SubmissionsView.vue"), + props: (route) => ({ groupId: Number(route.params.groupId) }), + }, { path: "/settings", name: "settings", diff --git a/frontend/src/services/group.ts b/frontend/src/services/group.ts index 58fa10f3..05bc74f4 100644 --- a/frontend/src/services/group.ts +++ b/frontend/src/services/group.ts @@ -1,7 +1,11 @@ import type Group from "@/models/Group"; import type { GroupForm } from "@/models/Group"; -import { authorized_fetch } from "@/services/index"; import type Submission from "@/models/Submission"; +import { authorized_fetch } from "@/services/index"; + +export async function getGroup(groupId: number): Promise { + return authorized_fetch(`/api/groups/${groupId}`, { method: "GET" }); +} export async function getUserGroups(): Promise { return authorized_fetch<{ groups: Group[] }>(`/api/users/me/groups`, { method: "GET" }).then( @@ -33,17 +37,6 @@ export async function createGroups(projectId: number, groups: GroupForm[]): Prom return Promise.all(createPromises); } -export async function createSubmission(groupId: number, formData: FormData): Promise { - return authorized_fetch( - `/api/submissions/?group_id=${groupId}`, - { - method: "POST", - body: formData, - }, - true - ); -} - export async function joinGroup(groupId: number, uid: string): Promise { try { await authorized_fetch(`/api/groups/${groupId}/${uid}`, { @@ -57,3 +50,7 @@ export async function joinGroup(groupId: number, uid: string): Promise { throw error; } } + +export async function getSubmissions(groupId: number): Promise { + return authorized_fetch(`/api/groups/${groupId}/submissions`, { method: "GET" }); +} diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts index 7b98d657..d5912551 100644 --- a/frontend/src/services/index.ts +++ b/frontend/src/services/index.ts @@ -13,7 +13,8 @@ const BASE_URL = import.meta.env.VITE_API_URL; export async function authorized_fetch( endpoint: string, requestOptions: RequestInit, - omitContentType: boolean = false + omitContentType: boolean = false, + json: boolean = true ): Promise { const { token, isLoggedIn } = storeToRefs(useAuthStore()); const { refresh } = useAuthStore(); @@ -38,7 +39,8 @@ export async function authorized_fetch( await refresh(); throw new Error("Not authenticated"); } else if (!response.ok) { - throw new Error(response.statusText); + const error = await response.json(); + throw new Error(error.detail); } - return response.json(); + return json ? response.json() : response; } diff --git a/frontend/src/services/project.ts b/frontend/src/services/project.ts index ed562e29..cf108433 100644 --- a/frontend/src/services/project.ts +++ b/frontend/src/services/project.ts @@ -1,6 +1,5 @@ import type Project from "@/models/Project"; import type { ProjectForm } from "@/models/Project"; -import type Submission from "@/models/Submission"; import { authorized_fetch } from "@/services"; export async function createProject(projectData: ProjectForm): Promise { @@ -32,19 +31,3 @@ export async function getProjects(): Promise { }); return result.projects; } - -// Function to create a new submission for a specific group -export async function createSubmission(groupId: number, formData: FormData): Promise { - return authorized_fetch( - `/api/submissions/?group_id=${groupId}`, - { - method: "POST", - body: formData, - }, - true - ); -} - -export async function getSubmissions(): Promise { - return authorized_fetch(`/api/submissions/`, { method: "GET" }); -} diff --git a/frontend/src/services/submission.ts b/frontend/src/services/submission.ts new file mode 100644 index 00000000..e8f70eb9 --- /dev/null +++ b/frontend/src/services/submission.ts @@ -0,0 +1,26 @@ +import type Submission from "@/models/Submission"; +import { authorized_fetch } from "."; +import type FileInfo from "@/models/File"; + +// Function to fetch a specific submission by its ID +export async function getSubmission(submissionId: number): Promise { + return authorized_fetch(`/api/submissions/${submissionId}`, { method: "GET" }); +} + +// Function to create a new submission for a specific group +export async function createSubmission(groupId: number, formData: FormData): Promise { + return authorized_fetch( + `/api/submissions/?group_id=${groupId}`, + { + method: "POST", + body: formData, + }, + true + ); +} + +export async function getFiles(submissionId: number): Promise { + return authorized_fetch(`/api/submissions/${submissionId}/files`, { + method: "GET", + }); +} diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts new file mode 100644 index 00000000..f08c2875 --- /dev/null +++ b/frontend/src/utils.ts @@ -0,0 +1,14 @@ +import { authorized_fetch } from "./services"; + +export async function download_file(url: string, filename: string) { + const data = await authorized_fetch(url, { method: "GET" }, false, false).then( + (response) => response.blob() + ); + + const objectUrl = URL.createObjectURL(data); + const link = document.createElement("a"); + link.href = objectUrl; + link.setAttribute("download", filename); + document.body.appendChild(link); + link.click(); +} diff --git a/frontend/src/views/SubmissionView.vue b/frontend/src/views/SubmissionView.vue new file mode 100644 index 00000000..53fb277c --- /dev/null +++ b/frontend/src/views/SubmissionView.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/views/SubmissionsView.vue b/frontend/src/views/SubmissionsView.vue new file mode 100644 index 00000000..c94742c6 --- /dev/null +++ b/frontend/src/views/SubmissionsView.vue @@ -0,0 +1,58 @@ + + + From 54b87f19a23dbdda3626d52ed370939453521988 Mon Sep 17 00:00:00 2001 From: Pieter Janin Date: Fri, 3 May 2024 17:57:05 +0200 Subject: [PATCH 11/81] alembic readme --- README.md | 33 ++++++++++++++++++------------ backend/README.md | 2 ++ backend/alembic/README | 1 - backend/alembic/README.md | 43 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 14 deletions(-) delete mode 100644 backend/alembic/README create mode 100644 backend/alembic/README.md diff --git a/README.md b/README.md index 55f619f3..fc1ef1c3 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,33 @@ -# UGent-5 +# Apollo -## Rolverdeling +Apollo is een indieningsplatform waar lesgevers flexibel vereisten kunnen stellen waaraan +indieningen van studenten moeten voldoen. Deze vereisten kunnen in de vorm zijn +van checks op de ingediende bestandsstructuur en/of automatisch lopende tests +wanneer een indiening gemaakt wordt. -| Rol | Verantwoordelijke | -| ------------- | ------------- | -| Groepsleider | Marieke Sinnaeve | -| Technische lead | Bram Reyniers | -| Systeembeheerder | Xander Bil | -| Customer Relations Officer | Pieter Janin | -| Frontendbeheerder | Mattis Cauwel | -| Backendbeheerder | Dries Huybens | -| Documentatiebeheerder | Pieter Janin | -| Testbeheerder | Michaƫl Boelaert | +Studenten krijgen feedback te zien over hun indiening en weten zo of hun indiening +voldoet aan de projectvereisten. ## Wiki Informatie over de gebruikte technologieƫn, de gebruikershandleiding en meer kan je vinden in de [wiki](https://github.com/SELab-2/UGent-5/wiki). -## Setup ontwikkelomgeving +## Voor ontwikkelaars De instructies voor het opzetten van de ontwikkelomgeving van de frontend kan je [hier](frontend/README.md) vinden. De instructies voor de backend staan [hier](backend/REAMDE.md). ## API Geautomatiseerde clients kunnen interageren met de webapplicatie via de [API](https://sel2-5.ugent.be/api/docs). + +## Het team + +| | | +|------------------|---------------------------------------------------| +| Xander Bil | Systeembeheerder | +| Michaƫl Boelaert | Testbeheerder | +| Mattis Cauwel | Frontendbeheerder | +| Dries Huybens | Backendbeheerder | +| Pieter Janin | Customer Relations Officer, Documentatiebeheerder | +| Bram Reyniers | Technische lead | +| Marieke Sinnaeve | Team lead | diff --git a/backend/README.md b/backend/README.md index 73f5e952..52991678 100644 --- a/backend/README.md +++ b/backend/README.md @@ -71,6 +71,8 @@ DATABASE_URI="postgresql://username:password@localhost:5432/dbname" alembic upgrade head ``` +You can find more info about alembic [here](alembic/README.md). + #### Managing the database ```sh # Stop the database container diff --git a/backend/alembic/README b/backend/alembic/README deleted file mode 100644 index 2500aa1b..00000000 --- a/backend/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. diff --git a/backend/alembic/README.md b/backend/alembic/README.md new file mode 100644 index 00000000..66f22a7a --- /dev/null +++ b/backend/alembic/README.md @@ -0,0 +1,43 @@ +# Alembic + +From the docs: + +> [Alembic](https://alembic.sqlalchemy.org/en/latest/) is a lightweight database +migration tool for usage with the [SQLAlchemy](https://www.sqlalchemy.org/) +Database Toolkit for Python. + +It allows us to generate database schemas from Python SQLAlchemy code, found in each +`models.py` file. + +## Usage + +Here are some of the most commonly used commands you might need. + +#### Automatically generate a revision script after modifying database models in Python: + +```sh +alembic revision --autogenerate -m "my_revision_name" +``` + +Make sure to manually review the generated script in `alembic/versions` +and apply adjustments if needed. + +#### Run a migration: this will upgrade the database schema to the most recent revision. + +```sh +alembic upgrade head +``` + +#### Undo the most recent revision: + +```sh +alembic downgrade -1 +``` + +#### Reset the database to its initial (empty) state: + +```sh +alembic downgrade base +``` + +For more examples, see the [official alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html). From 22e08d5b788204a7078a915220bcb156f3476242 Mon Sep 17 00:00:00 2001 From: Pieter Janin Date: Fri, 3 May 2024 17:59:37 +0200 Subject: [PATCH 12/81] hoofdletter --- backend/alembic/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/alembic/README.md b/backend/alembic/README.md index 66f22a7a..5161364b 100644 --- a/backend/alembic/README.md +++ b/backend/alembic/README.md @@ -40,4 +40,4 @@ alembic downgrade -1 alembic downgrade base ``` -For more examples, see the [official alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html). +For more examples, see the [official Alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html). From a8da6a8935d8465b1968b2015829dcb89a2268ac Mon Sep 17 00:00:00 2001 From: Marieke Date: Sat, 4 May 2024 14:48:23 +0200 Subject: [PATCH 13/81] theme store works --- frontend/src/App.vue | 5 +++ frontend/src/components/ApolloHeader.vue | 11 ++++-- .../components/navigation/DropDownList.vue | 2 +- frontend/src/components/navigation/NavBar.vue | 2 +- .../{ => switcher}/LocaleSwitcher.vue | 1 - .../src/components/switcher/ThemeSwitcher.vue | 39 +++++++++++++++++++ frontend/src/stores/theme-store.ts | 16 ++++++++ frontend/src/views/LoginView.vue | 2 +- 8 files changed, 71 insertions(+), 7 deletions(-) rename frontend/src/components/{ => switcher}/LocaleSwitcher.vue (96%) create mode 100644 frontend/src/components/switcher/ThemeSwitcher.vue create mode 100644 frontend/src/stores/theme-store.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6fc4b7d4..b0d41a0c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -19,6 +19,8 @@ import NavBar from "@/components/navigation/NavBar.vue"; import { computed, onBeforeMount, ref } from "vue"; import { useI18n } from "vue-i18n"; import { useLocale } from "@/stores/locale-store"; +import { useThemeStore } from "@/stores/theme-store"; +import { useTheme } from 'vuetify'; const navBar = ref | null>(null); @@ -28,7 +30,10 @@ const hideHeader = computed(() => currentRoute.meta.hideHeader); onBeforeMount(() => { const { locale } = useI18n(); const { selectedLocale } = useLocale(); + const { storedTheme } = useThemeStore(); + const theme = useTheme(); locale.value = selectedLocale; + theme.global.name.value = storedTheme }); diff --git a/frontend/src/components/ApolloHeader.vue b/frontend/src/components/ApolloHeader.vue index 8c4387cf..8cfded1b 100644 --- a/frontend/src/components/ApolloHeader.vue +++ b/frontend/src/components/ApolloHeader.vue @@ -10,6 +10,7 @@
+
@@ -17,8 +18,9 @@ + + diff --git a/frontend/src/stores/theme-store.ts b/frontend/src/stores/theme-store.ts new file mode 100644 index 00000000..46eab399 --- /dev/null +++ b/frontend/src/stores/theme-store.ts @@ -0,0 +1,16 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; + +export const useThemeStore = defineStore("theme", () => { + const storedTheme = ref(localStorage.getItem("theme")); + + function setTheme(newTheme?: string | null) { + if (!newTheme) { + return; + } + storedTheme.value = newTheme; + localStorage.setItem("theme", newTheme); + } + + return { storedTheme, setTheme }; +}); diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index c68b9cbf..7644ffc3 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -21,7 +21,7 @@ From 346cfe871bd60e5d758b5fbff322c46aa4de5dfa Mon Sep 17 00:00:00 2001 From: Pieter Janin Date: Sat, 4 May 2024 15:14:21 +0200 Subject: [PATCH 14/81] beter zo --- backend/alembic/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/alembic/README.md b/backend/alembic/README.md index 5161364b..d5cbbb71 100644 --- a/backend/alembic/README.md +++ b/backend/alembic/README.md @@ -19,8 +19,8 @@ Here are some of the most commonly used commands you might need. alembic revision --autogenerate -m "my_revision_name" ``` -Make sure to manually review the generated script in `alembic/versions` -and apply adjustments if needed. +Make sure to review the generated script in `alembic/versions` +and make adjustments if needed. #### Run a migration: this will upgrade the database schema to the most recent revision. From bafd0f06fdd81a0f1b9ebba5dafd22f263bff60b Mon Sep 17 00:00:00 2001 From: Pieter Janin Date: Sat, 4 May 2024 15:33:50 +0200 Subject: [PATCH 15/81] main readme engels --- README.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fc1ef1c3..4489966d 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,38 @@ # Apollo -Apollo is een indieningsplatform waar lesgevers flexibel vereisten kunnen stellen waaraan -indieningen van studenten moeten voldoen. Deze vereisten kunnen in de vorm zijn -van checks op de ingediende bestandsstructuur en/of automatisch lopende tests -wanneer een indiening gemaakt wordt. +Apollo is an online submission platform where instructors can flexibly set requirements for +student submissions. These requirements can range from simple checks on the submitted +file structure to test scripts that run when a submission is made. -Studenten krijgen feedback te zien over hun indiening en weten zo of hun indiening -voldoet aan de projectvereisten. +Students quickly receive feedback on their submission, allowing them to know if it meets +the project requirements. + +This repository hosts the web application's source code. To use Apollo, visit https://sel2-5.ugent.be. ## Wiki -Informatie over de gebruikte technologieƫn, de gebruikershandleiding en meer kan je vinden in de [wiki](https://github.com/SELab-2/UGent-5/wiki). +Documentation, including a user manual for teachers, can be found in the +[Apollo wiki](https://github.com/SELab-2/UGent-5/wiki). + +## For Developers -## Voor ontwikkelaars +Instructions for setting up the frontend development environment can be found +[here](frontend/README.md). -De instructies voor het opzetten van de ontwikkelomgeving van de frontend kan je [hier](frontend/README.md) vinden. De instructies voor de backend staan [hier](backend/REAMDE.md). +Instructions for the backend are located [here](backend/REAMDE.md). ## API -Geautomatiseerde clients kunnen interageren met de webapplicatie via de [API](https://sel2-5.ugent.be/api/docs). +Automated clients can interact with the web application via the [API](https://sel2-5.ugent.be/api/docs). -## Het team +## The team | | | |------------------|---------------------------------------------------| -| Xander Bil | Systeembeheerder | -| Michaƫl Boelaert | Testbeheerder | -| Mattis Cauwel | Frontendbeheerder | -| Dries Huybens | Backendbeheerder | -| Pieter Janin | Customer Relations Officer, Documentatiebeheerder | -| Bram Reyniers | Technische lead | -| Marieke Sinnaeve | Team lead | +| Xander Bil | System Administrator | +| Michaƫl Boelaert | Test Manager | +| Mattis Cauwel | Frontend Manager | +| Dries Huybens | Backend Manager | +| Pieter Janin | Customer Relations Officer, Documentation Manager | +| Bram Reyniers | Technical Lead | +| Marieke Sinnaeve | Team Lead | From 6946511b1edba74a93d8caec4f88a0c69dcee4e1 Mon Sep 17 00:00:00 2001 From: Pieter Janin Date: Sat, 4 May 2024 16:22:43 +0200 Subject: [PATCH 16/81] submission requirements fix --- .../18fb90307213_project_requirements_fix.py | 35 ++++++ backend/src/project/models.py | 2 +- backend/tests/test_project.py | 3 +- backend/tests/test_submission.py | 102 ++++++++++++++++-- 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 backend/alembic/versions/18fb90307213_project_requirements_fix.py diff --git a/backend/alembic/versions/18fb90307213_project_requirements_fix.py b/backend/alembic/versions/18fb90307213_project_requirements_fix.py new file mode 100644 index 00000000..d0a94668 --- /dev/null +++ b/backend/alembic/versions/18fb90307213_project_requirements_fix.py @@ -0,0 +1,35 @@ +"""project_requirements_fix: fixes bug where project requirements would remain in the database after the parent +project would be deleted. + +Revision ID: 18fb90307213 +Revises: e0c97995e669 +Create Date: 2024-05-04 15:42:43.114843 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '18fb90307213' +down_revision: Union[str, None] = 'e0c97995e669' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('requirement', 'project_id', + existing_type=sa.INTEGER(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('requirement', 'project_id', + existing_type=sa.INTEGER(), + nullable=True) + # ### end Alembic commands ### diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 627bee4a..1efa8ab4 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -49,7 +49,7 @@ class Requirement(Base): id: Mapped[int] = mapped_column(primary_key=True) project_id: Mapped[int] = mapped_column(ForeignKey( - "project.id", ondelete="CASCADE"), nullable=True) + "project.id", ondelete="CASCADE"), nullable=False) project: Mapped["Project"] = relationship(back_populates="requirements") # True for mandatory False for prohibited diff --git a/backend/tests/test_project.py b/backend/tests/test_project.py index daedd7aa..277b68df 100644 --- a/backend/tests/test_project.py +++ b/backend/tests/test_project.py @@ -18,8 +18,7 @@ "enroll_deadline": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "is_visible": True, "capacity": 1, - "requirements": [], - "test_files": [], + "requirements": [{"mandatory": "false", "value": "*.pdf"}], } diff --git a/backend/tests/test_submission.py b/backend/tests/test_submission.py index 5c152e14..b2f9ed50 100644 --- a/backend/tests/test_submission.py +++ b/backend/tests/test_submission.py @@ -1,19 +1,57 @@ -import shutil +from datetime import datetime, timezone, timedelta import pytest +import pytest_asyncio from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + from src.auth.exceptions import NotAuthorized import os -from src.docker_tests.utils import submissions_path +from src.user.service import set_admin # Import Fixtures from tests.test_group import group_id from tests.test_project import project_id from tests.test_subject import subject_id +from tests.test_docker import cleanup_files + +subject = {"name": "test subject"} +future_date = datetime.now(timezone.utc) + timedelta(weeks=1) +project_with_reqs = { + "name": "test project", + "subject_id": 0, # temp needs to be filled in by actual subject id + "deadline": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "description": "test", + "enroll_deadline": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "is_visible": True, + "capacity": 1, + "requirements": [{"mandatory": "true", "value": "*.py"}, {"mandatory": "false", "value": "*.pdf"}], +} + +group_data = {"team_name": "test group", "project_id": 0} + + +@pytest_asyncio.fixture +async def project_with_reqs_id(client: AsyncClient, db: AsyncSession, subject_id: int) -> int: + """Create new project""" + project_with_reqs["subject_id"] = subject_id + await set_admin(db, "test", True) + response = await client.post("/api/projects/", json=project_with_reqs) + await set_admin(db, "test", False) + return response.json()["id"] + + +@pytest_asyncio.fixture +async def group_with_reqs_id(client: AsyncClient, db: AsyncSession, project_with_reqs_id: int): + group_data["project_id"] = project_with_reqs_id + await set_admin(db, "test", True) + response = await client.post("/api/groups/", json=group_data) + await set_admin(db, "test", False) + return response.json()["id"] @pytest.mark.asyncio -async def test_create_submission(client: AsyncClient, group_id: int): +async def test_create_submission(client: AsyncClient, group_id: int, cleanup_files): with open("testfile1.txt", "w") as f: f.write("content1") with open("testfile2.txt", "w") as f: @@ -29,7 +67,6 @@ async def test_create_submission(client: AsyncClient, group_id: int): # Submit await client.post(f"/api/groups/{group_id}") # Join group response = await client.post(f"/api/submissions/", params={"group_id": group_id}, files=files) - files_uuid = response.json()['files_uuid'] assert response.status_code == 201 # Leave group @@ -64,7 +101,60 @@ async def test_create_submission(client: AsyncClient, group_id: int): # cleanup files os.remove("testfile1.txt") os.remove("testfile2.txt") - shutil.rmtree(submissions_path(files_uuid)) + await cleanup_files(id) -# TODO: check submission with project requirements +@pytest.mark.asyncio +async def test_project_requirements(client: AsyncClient, group_with_reqs_id: int, cleanup_files): + mandatory_path = "mandatory.py" + forbidden_path = "forbidden.pdf" + optional_path = "whatever.txt" + with open(mandatory_path, "w") as f: + f.write("content1") + with open(forbidden_path, "w") as f: + f.write("content2") + with open(optional_path, "w") as f: + f.write("content2") + + mandatory = ('files', open(mandatory_path, 'rb')) + forbidden = ('files', open(forbidden_path, 'rb')) + optional = ('files', open(optional_path, 'rb')) + + await client.post(f"/api/groups/{group_with_reqs_id}") # Join group + + # Submit without mandatory + response = await client.post("/api/submissions/", params={"group_id": group_with_reqs_id}, files=[optional]) + assert response.status_code == 422 + assert len(response.json()["detail"]) == 1 + assert response.json()["detail"][0]["requirement"] == "*.py" + assert response.json()["detail"][0]["type"] == "mandatory" + + # Submit with forbidden + response = await client.post( + "/api/submissions/", params={"group_id": group_with_reqs_id}, files=[optional, forbidden] + ) + assert response.status_code == 422 + assert len(response.json()["detail"]) == 2 + reqs = [(req["requirement"], req["type"]) for req in response.json()["detail"]] + assert ("*.py", "mandatory") in reqs + assert ("*.pdf", "forbidden") in reqs + + # Submit with forbidden and mandatory + response = await client.post( + "/api/submissions/", params={"group_id": group_with_reqs_id}, files=[optional, forbidden, mandatory] + ) + assert response.status_code == 422 + assert len(response.json()["detail"]) == 1 + reqs = [(req["requirement"], req["type"]) for req in response.json()["detail"]] + assert ("*.pdf", "forbidden") == reqs[0] + + # Submit with mandatory + response = await client.post( + "/api/submissions/", params={"group_id": group_with_reqs_id}, files=[optional, mandatory] + ) + assert response.status_code == 201 + await cleanup_files(response.json()["id"]) + + # cleanup files + for path in [mandatory_path, forbidden_path, optional_path]: + os.remove(path) From a1a130aed129b3edcbf16c458504973c9c68cc82 Mon Sep 17 00:00:00 2001 From: Pieter Janin Date: Sat, 4 May 2024 16:44:48 +0200 Subject: [PATCH 17/81] fix #162 --- backend/src/project/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 1efa8ab4..175964f3 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -27,7 +27,7 @@ class Project(Base): ) requirements: Mapped[List["Requirement"]] = relationship( - back_populates="project", lazy="joined") + back_populates="project", lazy="joined", passive_deletes="all") # see submission/models/Submission test_files_uuid: Mapped[str | None] = mapped_column(nullable=True) From 9a09110c65b17bbf85fcba0c944fd6fd161b540c Mon Sep 17 00:00:00 2001 From: Pieter Janin Date: Sat, 4 May 2024 16:55:48 +0200 Subject: [PATCH 18/81] autopep --- .../versions/18fb90307213_project_requirements_fix.py | 8 ++++---- backend/src/project/models.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/alembic/versions/18fb90307213_project_requirements_fix.py b/backend/alembic/versions/18fb90307213_project_requirements_fix.py index d0a94668..b2c40390 100644 --- a/backend/alembic/versions/18fb90307213_project_requirements_fix.py +++ b/backend/alembic/versions/18fb90307213_project_requirements_fix.py @@ -22,14 +22,14 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.alter_column('requirement', 'project_id', - existing_type=sa.INTEGER(), - nullable=False) + existing_type=sa.INTEGER(), + nullable=False) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.alter_column('requirement', 'project_id', - existing_type=sa.INTEGER(), - nullable=True) + existing_type=sa.INTEGER(), + nullable=True) # ### end Alembic commands ### diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 175964f3..b0bb0a4d 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -27,7 +27,8 @@ class Project(Base): ) requirements: Mapped[List["Requirement"]] = relationship( - back_populates="project", lazy="joined", passive_deletes="all") # see submission/models/Submission + # see submission/models/Submission -> testresults + back_populates="project", lazy="joined", passive_deletes="all") test_files_uuid: Mapped[str | None] = mapped_column(nullable=True) From 41bc1999dd20270732f09e5399b67227f9153823 Mon Sep 17 00:00:00 2001 From: Marieke Date: Sat, 4 May 2024 17:34:40 +0200 Subject: [PATCH 19/81] dark theme colors --- frontend/src/components/ApolloHeader.vue | 4 ++-- .../components/home/listcontent/DeadlineItem.vue | 6 +++--- .../components/home/listcontent/SubjectItem.vue | 6 +++--- .../src/components/navigation/DropDownList.vue | 4 ++++ frontend/src/components/navigation/NavBar.vue | 1 - frontend/src/components/navigation/NavButton.vue | 4 +--- .../src/components/switcher/LocaleSwitcher.vue | 4 ++-- frontend/src/plugins/vuetify.ts | 14 ++++++++++---- 8 files changed, 25 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/ApolloHeader.vue b/frontend/src/components/ApolloHeader.vue index 8cfded1b..3d7945f5 100644 --- a/frontend/src/components/ApolloHeader.vue +++ b/frontend/src/components/ApolloHeader.vue @@ -45,7 +45,7 @@ const { isLoggedIn } = storeToRefs(useAuthStore()); } .v-app-bar-nav-icon{ - color: rgb(var(--v-theme-secondary)); + color: rgb(var(--v-theme-navtext)); } .leftContent { @@ -55,7 +55,7 @@ const { isLoggedIn } = storeToRefs(useAuthStore()); } .logout { - color: rgb(var(--v-theme-secondary)); + color: rgb(var(--v-theme-navtext)); } .switcher{ diff --git a/frontend/src/components/home/listcontent/DeadlineItem.vue b/frontend/src/components/home/listcontent/DeadlineItem.vue index ab9f7d88..61286694 100644 --- a/frontend/src/components/home/listcontent/DeadlineItem.vue +++ b/frontend/src/components/home/listcontent/DeadlineItem.vue @@ -51,7 +51,7 @@ const navigateToProject = () => { diff --git a/frontend/src/components/home/listcontent/SubjectItem.vue b/frontend/src/components/home/listcontent/SubjectItem.vue index 18445acc..0cf2fe63 100644 --- a/frontend/src/components/home/listcontent/SubjectItem.vue +++ b/frontend/src/components/home/listcontent/SubjectItem.vue @@ -41,12 +41,12 @@ const navigateToCourse = () => { align-items: center; transition: background-color 0.3s; cursor: pointer; - background-color: #ffffff; + background-color: rgb(var(--v-theme-background)); border-radius: 2px; } .coursebtn:hover { - background-color: #E9EDFA; + background-color: rgb(var(--v-theme-tertiary)); } .chevron { @@ -56,6 +56,6 @@ const navigateToCourse = () => { } .teacher { - color: lightslategrey; + color: rgb(var(--v-theme-textsecondary)) } diff --git a/frontend/src/components/navigation/DropDownList.vue b/frontend/src/components/navigation/DropDownList.vue index a75b0b47..ae6cc021 100644 --- a/frontend/src/components/navigation/DropDownList.vue +++ b/frontend/src/components/navigation/DropDownList.vue @@ -6,12 +6,16 @@ + + + diff --git a/frontend/src/assets/base.scss b/frontend/src/assets/base.scss index 84eca241..c6059b62 100644 --- a/frontend/src/assets/base.scss +++ b/frontend/src/assets/base.scss @@ -5,7 +5,7 @@ --color-primary-dark: color-mod(var(--color-primary) shade(15%)); --color-primary-bg: color-mod(var(--color-primary) alpha(30)); - --color-secondary: #EBEFFB; + --color-secondary: #ebeffb; --color-accent: #ffd200; --color-accent-light: color-mod(var(--color-accent) tint(15%)); diff --git a/frontend/src/components/ApolloHeader.vue b/frontend/src/components/ApolloHeader.vue index 3d7945f5..3882ce78 100644 --- a/frontend/src/components/ApolloHeader.vue +++ b/frontend/src/components/ApolloHeader.vue @@ -10,7 +10,7 @@
- +
@@ -20,7 +20,7 @@ import { RouterLink } from "vue-router"; import { useDisplay } from "vuetify"; import LocaleSwitcher from "./switcher/LocaleSwitcher.vue"; import DropDownMobile from "@/components/navigation/DropDownMobile.vue"; -import ThemeSwitcher from "@/components/switcher/ThemeSwitcher.vue" +import ThemeSwitcher from "@/components/switcher/ThemeSwitcher.vue"; import LogoutButton from "@/components/buttons/LogoutButton.vue"; import { useAuthStore } from "@/stores/auth-store"; import { storeToRefs } from "pinia"; @@ -44,7 +44,7 @@ const { isLoggedIn } = storeToRefs(useAuthStore()); padding: 5px 0; } -.v-app-bar-nav-icon{ +.v-app-bar-nav-icon { color: rgb(var(--v-theme-navtext)); } @@ -58,7 +58,7 @@ const { isLoggedIn } = storeToRefs(useAuthStore()); color: rgb(var(--v-theme-navtext)); } -.switcher{ +.switcher { margin-left: 20px; } diff --git a/frontend/src/components/home/listcontent/DeadlineItem.vue b/frontend/src/components/home/listcontent/DeadlineItem.vue index 61286694..743ff2d1 100644 --- a/frontend/src/components/home/listcontent/DeadlineItem.vue +++ b/frontend/src/components/home/listcontent/DeadlineItem.vue @@ -1,8 +1,6 @@ + diff --git a/frontend/src/components/project/ProjectMiniCard.vue b/frontend/src/components/project/ProjectMiniCard.vue index f6e1b66b..dd90fc78 100644 --- a/frontend/src/components/project/ProjectMiniCard.vue +++ b/frontend/src/components/project/ProjectMiniCard.vue @@ -1,5 +1,5 @@