diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index c411cfc9..8f606cab 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -1,3 +1,4 @@ +import zipfile from os.path import basename, realpath from django.db import transaction @@ -15,7 +16,7 @@ from backend.pigeonhole.apps.groups.models import GroupSerializer from backend.pigeonhole.apps.submissions.models import ( Submissions, - SubmissionsSerializer, submission_folder_path, + SubmissionsSerializer, ) from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.filters import ( @@ -25,7 +26,6 @@ ) from .models import Project, ProjectSerializer from .permissions import CanAccessProject -from ..submissions.views import ZipUtilities class CsrfExemptSessionAuthentication(SessionAuthentication): @@ -253,19 +253,22 @@ def download_submissions(self, request, *args, **kwargs): if len(submissions) == 0: return Response(status=status.HTTP_400_BAD_REQUEST) - path = 'backend/downloads/submissions.zip' - submission_folders = [] + path = "" - for submission in submissions: - submission_folders.append( - submission_folder_path( - submission.group_id.group_id, submission.submission_id + if len(submissions) == 1: + path = submissions[0].file.path + + else: + path = "backend/downloads/submissions.zip" + zipf = zipfile.ZipFile(file=path, mode="w", compression=zipfile.ZIP_STORED) + + for submission in submissions: + zipf.write( + filename=submission.file.path, + arcname=basename(submission.file.path), ) - ) - utilities = ZipUtilities() - filename = path - utilities.toZip(submission_folders, filename) + zipf.close() path = realpath(path) response = FileResponse( diff --git a/backend/pigeonhole/apps/submissions/views.py b/backend/pigeonhole/apps/submissions/views.py index 7710bc5c..a4c23f60 100644 --- a/backend/pigeonhole/apps/submissions/views.py +++ b/backend/pigeonhole/apps/submissions/views.py @@ -206,26 +206,40 @@ def download_selection(self, request, *args, **kwargs): if not ids: return Response(status=status.HTTP_400_BAD_REQUEST) - path = 'backend/downloads/submissions.zip' - submission_folders = [] + path = "" - for sid in ids: - submission = Submissions.objects.get(submission_id=sid) + if len(ids) == 1: + submission = Submissions.objects.get(submission_id=ids[0]) if submission is None: return Response( - {"message": f"Submission with id {id} not found", + {"message": f"Submission with id {ids[0]} not found", "errorcode": "ERROR_SUBMISSION_NOT_FOUND"}, - status=status.HTTP_404_NOT_FOUND + status=status.HTTP_404_NOT_FOUND, ) - submission_folders.append( - submission_folder_path( - submission.group_id.group_id, submission.submission_id + + path = submission.file.path + + else: + path = 'backend/downloads/submissions.zip' + submission_folders = [] + + for sid in ids: + submission = Submissions.objects.get(submission_id=sid) + if submission is None: + return Response( + {"message": f"Submission with id {id} not found", + "errorcode": "ERROR_SUBMISSION_NOT_FOUND"}, + status=status.HTTP_404_NOT_FOUND + ) + submission_folders.append( + submission_folder_path( + submission.group_id.group_id, submission.submission_id + ) ) - ) - utilities = ZipUtilities() - filename = path - utilities.toZip(submission_folders, filename) + utilities = ZipUtilities() + filename = path + utilities.toZip(submission_folders, filename) path = realpath(path) response = FileResponse( diff --git a/frontend/__test__/AccountMenu.test.tsx b/frontend/__test__/AccountMenu.test.tsx index 7e605243..104a73f6 100644 --- a/frontend/__test__/AccountMenu.test.tsx +++ b/frontend/__test__/AccountMenu.test.tsx @@ -43,6 +43,8 @@ describe('AccountMenu', () => { fireEvent.click(screen.getByRole('button')); const menu = screen.getByRole('menu'); expect(menu).toBeVisible(); + fireEvent.click(screen.getByRole('menuitem', {name: 'settings'})); + expect(menu).not.toBeVisible(); }); diff --git a/frontend/__test__/course_components/CancelButton.test.tsx b/frontend/__test__/course_components/CancelButton.test.tsx new file mode 100644 index 00000000..e134106e --- /dev/null +++ b/frontend/__test__/course_components/CancelButton.test.tsx @@ -0,0 +1,10 @@ +import {render, screen} from "@testing-library/react"; +import React from "react"; +import CancelButton from "@app/[locale]/components/course_components/CancelButton"; + +describe("CancelButton", () => { + it("renders cancel button and click", async () => { + render(); + screen.getByText(/cancel/i).click(); + }); +}); diff --git a/frontend/app/[locale]/components/AccountMenu.tsx b/frontend/app/[locale]/components/AccountMenu.tsx index e58ab51f..a9efdd43 100644 --- a/frontend/app/[locale]/components/AccountMenu.tsx +++ b/frontend/app/[locale]/components/AccountMenu.tsx @@ -20,16 +20,13 @@ import {useEffect, useState} from "react"; const backend_url = process.env['NEXT_PUBLIC_BACKEND_URL']; export default function AccountMenu() { - /* - * Account menu component, used to display the user's name and a dropdown menu with options to go to the profile page and logout(right side of the navbar) - * */ const [user, setUser] = useState(null); const [error, setError] = useState(null); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); const { t } = useTranslation() useEffect(() => { + + const fetchCourses = async () => { try{ setUser(await getUserData()); @@ -42,23 +39,26 @@ export default function AccountMenu() { fetchCourses(); }, []); - //Utility & navigation functions + + + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { + //TODO: Handle settings and My profile actions!! setAnchorEl(null); }; const toProfile = () => { window.location.href = '/profile' - }; + } const handleLogout = () => { setAnchorEl(null); window.location.href = backend_url + "/auth/logout"; }; - return ( @@ -108,6 +108,12 @@ export default function AccountMenu() { {t('my_profile')} + + + + + {t('settings')} + diff --git a/frontend/app/[locale]/components/AddButton.tsx b/frontend/app/[locale]/components/AddButton.tsx index 2f8485f3..ca45fe66 100644 --- a/frontend/app/[locale]/components/AddButton.tsx +++ b/frontend/app/[locale]/components/AddButton.tsx @@ -3,12 +3,8 @@ import { Button } from '@mui/material'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import {useTranslation} from "react-i18next"; +//TODO: route to add project page function AddButton({translationkey, href} : {translationkey: string, href : string|undefined}){ - /* - * General add button component - * @param translationkey: The key of the translation in the i18n file - * @param href: The href of the button - * */ const { t } = useTranslation() return( diff --git a/frontend/app/[locale]/components/AddProjectButton.tsx b/frontend/app/[locale]/components/AddProjectButton.tsx index 3bdc91a0..0b17a986 100644 --- a/frontend/app/[locale]/components/AddProjectButton.tsx +++ b/frontend/app/[locale]/components/AddProjectButton.tsx @@ -9,10 +9,6 @@ interface EditCourseButtonProps{ } const AddProjectButton = ({course_id}: EditCourseButtonProps) => { - /* - * Specific add project button component - * @param course_id: The id of the course to which the project will be added - * */ const {t} = useTranslation(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); @@ -42,7 +38,7 @@ const AddProjectButton = ({course_id}: EditCourseButtonProps) => { }} /> : <> - {user?.role !== 3 && ( // If the user is not a student + {user?.role !== 3 && ( + + + ) +} + +export default CancelButton \ No newline at end of file diff --git a/frontend/app/[locale]/components/course_components/DeleteButton.tsx b/frontend/app/[locale]/components/course_components/DeleteButton.tsx index e3d1ca27..67094e37 100644 --- a/frontend/app/[locale]/components/course_components/DeleteButton.tsx +++ b/frontend/app/[locale]/components/course_components/DeleteButton.tsx @@ -1,67 +1,63 @@ -'use client'; - -import React, { useState } from 'react'; -import { useTranslation } from "react-i18next"; -import { deleteCourse } from "@lib/api"; -import { Button, Dialog, DialogActions, DialogTitle } from '@mui/material'; - -interface DeleteButtonProps { - courseId: number -} - -const DeleteButton = ({ courseId }: DeleteButtonProps) => { - /* - * This component displays the delete button for a course. - * @param courseId: The id of the course - */ - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - - const handleOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - const handleDelete = async () => { - await deleteCourse(courseId); - window.location.href = "/home"; - }; - - return ( - <> - - - {t("Are you sure you want to delete this course?")} - - - - - - - ); -}; - -export default DeleteButton; +'use client'; + +import React, { useState } from 'react'; +import { useTranslation } from "react-i18next"; +import { deleteCourse } from "@lib/api"; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Typography } from '@mui/material'; + +interface DeleteButtonProps { + courseId: number +} + +const DeleteButton = ({ courseId }: DeleteButtonProps) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleDelete = async () => { + await deleteCourse(courseId); + window.location.href = "/home"; + }; + + return ( + <> + + + {t("Are you sure you want to delete this course?")} + + + + + + + ); +}; + +export default DeleteButton; diff --git a/frontend/app/[locale]/components/general/ItemsList.tsx b/frontend/app/[locale]/components/general/ItemsList.tsx index a353e112..e2564ed1 100644 --- a/frontend/app/[locale]/components/general/ItemsList.tsx +++ b/frontend/app/[locale]/components/general/ItemsList.tsx @@ -14,14 +14,6 @@ interface ItemsListProps { } const ItemsList = ({items, setItems, input_placeholder, empty_list_placeholder, button_text}: ItemsListProps) => { - /* - * This component displays a list of items and allows the user to add and delete items. - * @param items: The list of items - * @param setItems: The function to set the list of items - * @param input_placeholder: The placeholder for the input field - * @param empty_list_placeholder: The placeholder for the list when it is empty - * @param button_text: The text for the button - */ const [newItem, setNewItem] = useState('') const [noInput, setNoInput] = useState(false) diff --git a/frontend/app/[locale]/components/general/RequiredFilesList.tsx b/frontend/app/[locale]/components/general/RequiredFilesList.tsx index 983b09b8..5cd34792 100644 --- a/frontend/app/[locale]/components/general/RequiredFilesList.tsx +++ b/frontend/app/[locale]/components/general/RequiredFilesList.tsx @@ -26,18 +26,6 @@ const ItemsList = ({ items_status, setItemsStatus }: ItemsListProps) => { - /* - * This component is a variation of the ItemsList that works specifically for the required files of a project. - * It displays a list of files and allows the user to add and delete files. - * It also allows the user to set the status of the files. - * @param items: The list of files - * @param setItems: The function to set the list of files - * @param input_placeholder: The placeholder for the input field - * @param empty_list_placeholder: The placeholder for the list when it is empty - * @param button_text: The text for the button - * @param items_status: The list of statuses for the files - * @param setItemsStatus: The function to set the list of statuses for the files - */ const [newItem, setNewItem] = useState('') const [noInput, setNoInput] = useState(false) const {t} = useTranslation(); diff --git a/frontend/app/[locale]/components/project_components/testfiles.tsx b/frontend/app/[locale]/components/project_components/testfiles.tsx index a98ff9b8..c3bf135e 100644 --- a/frontend/app/[locale]/components/project_components/testfiles.tsx +++ b/frontend/app/[locale]/components/project_components/testfiles.tsx @@ -14,14 +14,6 @@ interface TestFilesProps { } function TestFiles({testfilesName, setTestfilesName, testfilesData, setTestfilesData}: TestFilesProps) { - /* - * This component displays the list of test files for a project. - * It allows the user to delete test files. - * @param testfilesName: The list of names of the test files - * @param setTestfilesName: The function to set the list of names of the test files - * @param testfilesData: The list of data of the test files - * @param setTestfilesData: The function to set the list of data of the test files - */ const {t} = useTranslation(); return
diff --git a/frontend/app/[locale]/components/project_components/title.tsx b/frontend/app/[locale]/components/project_components/title.tsx index 2862f183..2c4f24fe 100644 --- a/frontend/app/[locale]/components/project_components/title.tsx +++ b/frontend/app/[locale]/components/project_components/title.tsx @@ -13,15 +13,6 @@ interface TitleProps { } function Title({isTitleEmpty, setTitle, title, score, isScoreEmpty, setScore}: TitleProps) { - /* - * This component displays the title and the maximum score of a project. - * @param isTitleEmpty: A boolean that indicates if the title is empty - * @param setTitle: The function to set the title - * @param title: The title of the project - * @param score: The maximum score of the project - * @param isScoreEmpty: A boolean that indicates if the score is empty - * @param setScore: The function to set the score - */ const {t} = useTranslation(); const handleScoreChange = (event: any) => { diff --git a/frontend/app/[locale]/components/project_components/uploadButton.tsx b/frontend/app/[locale]/components/project_components/uploadButton.tsx index 71258d7b..b4644d0b 100644 --- a/frontend/app/[locale]/components/project_components/uploadButton.tsx +++ b/frontend/app/[locale]/components/project_components/uploadButton.tsx @@ -11,19 +11,14 @@ interface UploadTestFileProps { setTestfilesData: (value: (((prevState: JSZipObject[]) => JSZipObject[]) | JSZipObject[])) => void, } -function UploadTestFile({ +function UploadTestFile( + { testfilesName, setTestfilesName, testfilesData, setTestfilesData, - }: UploadTestFileProps) { - /* - * This component allows the user to upload test files for a project. - * @param testfilesName: The list of names of the test files - * @param setTestfilesName: The function to set the list of names of the test files - * @param testfilesData: The list of data of the test files - * @param setTestfilesData: The function to set the list of data of the test files - */ + }: UploadTestFileProps +) { const {t} = useTranslation(); const handleFileChange = async (event: any) => { let zip = new JSZip(); diff --git a/frontend/app/[locale]/components/user_components/CancelButton.tsx b/frontend/app/[locale]/components/user_components/CancelButton.tsx index acb17c78..b9817030 100644 --- a/frontend/app/[locale]/components/user_components/CancelButton.tsx +++ b/frontend/app/[locale]/components/user_components/CancelButton.tsx @@ -4,9 +4,6 @@ import React from 'react' import {useTranslation} from "react-i18next"; const CancelButton = () => { - /* - * This component displays the cancel button. - */ const {t} = useTranslation() const handleCancel = () => { diff --git a/frontend/app/[locale]/components/user_components/DeleteButton.tsx b/frontend/app/[locale]/components/user_components/DeleteButton.tsx index 03eceb3b..390c9f59 100644 --- a/frontend/app/[locale]/components/user_components/DeleteButton.tsx +++ b/frontend/app/[locale]/components/user_components/DeleteButton.tsx @@ -3,17 +3,13 @@ import React, { useState } from 'react'; import { useTranslation } from "react-i18next"; import { deleteUser } from "@lib/api"; -import { Button, Dialog, DialogActions, DialogTitle, Typography } from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Typography } from '@mui/material'; interface DeleteButtonProps { userId: number } const DeleteButton = ({ userId }: DeleteButtonProps) => { - /* - * This component displays the delete button for a user. - * @param userId: The id of the user - */ const { t } = useTranslation(); const [open, setOpen] = useState(false); diff --git a/frontend/app/[locale]/project/[project_id]/edit/projectEditForm.tsx b/frontend/app/[locale]/project/[project_id]/edit/projectEditForm.tsx index ef3100f9..a7a9ee17 100644 --- a/frontend/app/[locale]/project/[project_id]/edit/projectEditForm.tsx +++ b/frontend/app/[locale]/project/[project_id]/edit/projectEditForm.tsx @@ -89,13 +89,13 @@ function ProjectEditForm({project_id, add_course_id}: ProjectEditFormProps) { if (project.deadline !== null) setDeadline(dayjs(project["deadline"])); setDescription(project.description) if (project.file_structure !== null && project.file_structure !== "") { + console.log(project.file_structure) const file_structure = project.file_structure.split(",").map((item: string) => item.trim().replace(/"/g, '')); const file_structure_status = file_structure.map((item: string) => item[0]); const file_structure_name = file_structure.map((item: string) => item.substring(1)); setFiles(file_structure_name); setStatusFiles(file_structure_status); } - setDockerImage(project["test_docker_image"]) setGroupSize(project["group_size"]) setTitle(project["name"]) setGroupAmount(project["number_of_groups"])