diff --git a/backend/project/endpoints/projects/project_submissions_download.py b/backend/project/endpoints/projects/project_submissions_download.py index 8e6ec83e..6ba93e93 100644 --- a/backend/project/endpoints/projects/project_submissions_download.py +++ b/backend/project/endpoints/projects/project_submissions_download.py @@ -51,9 +51,6 @@ def get_last_submissions_per_user(project_id): (Submission.submission_time == latest_submissions.c.max_time) ).all() - if not submissions: - return {"message": "No submissions found", "url": BASE_URL}, 404 - return {"message": "Resource fetched succesfully", "data": submissions}, 200 class SubmissionDownload(Resource): diff --git a/backend/project/endpoints/users.py b/backend/project/endpoints/users.py index 34e65817..cf5e054a 100644 --- a/backend/project/endpoints/users.py +++ b/backend/project/endpoints/users.py @@ -34,6 +34,10 @@ def get(self): role = Role[role.upper()] query = query.filter(userModel.role == role) + uid = request.args.getlist("uid") + if len(uid) > 0: + query = query.filter(userModel.uid.in_(uid)) + users = query.all() users = [user.to_dict() for user in users] diff --git a/documentation/documentation/.gitignore b/documentation/.gitignore similarity index 93% rename from documentation/documentation/.gitignore rename to documentation/.gitignore index b2d6de30..e6e9ac43 100644 --- a/documentation/documentation/.gitignore +++ b/documentation/.gitignore @@ -17,4 +17,4 @@ npm-debug.log* yarn-debug.log* -yarn-error.log* +yarn-error.log* \ No newline at end of file diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 00000000..a0daa9a7 --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,27 @@ +# UGent-3 project peristerĂ³nas user guide + +## Introduction +Project peristerĂ³nas has a lot of features, therefore a detailed user guide is available to consult. + +## Usage +### Development +If you want to develop on the site, run the following command: + ```sh + npm run start + ``` +This creates a lightweight version of the site, if you want to test a certain language version of the site you can use the command: + ```sh + npm run start -- --locale [language] + ``` + + ### Deployment + When you're ready to deploy, run the following command to run the proper version of the site: + ```sh + npm run build + ``` + + A static version will be built that you can access in the directory `build/`, you can then run the static site by using the command: + + ```sh + npm run serve + ``` diff --git a/documentation/documentation/babel.config.js b/documentation/babel.config.js similarity index 100% rename from documentation/documentation/babel.config.js rename to documentation/babel.config.js diff --git a/documentation/documentation/docs/evaluators/_category_.json b/documentation/docs/evaluators/_category_.json similarity index 100% rename from documentation/documentation/docs/evaluators/_category_.json rename to documentation/docs/evaluators/_category_.json diff --git a/documentation/documentation/docs/evaluators/custom_evaluator.md b/documentation/docs/evaluators/custom_evaluator.md similarity index 100% rename from documentation/documentation/docs/evaluators/custom_evaluator.md rename to documentation/docs/evaluators/custom_evaluator.md diff --git a/documentation/documentation/docs/evaluators/general_evaluator.md b/documentation/docs/evaluators/general_evaluator.md similarity index 100% rename from documentation/documentation/docs/evaluators/general_evaluator.md rename to documentation/docs/evaluators/general_evaluator.md diff --git a/documentation/documentation/docs/evaluators/python_evaluator.md b/documentation/docs/evaluators/python_evaluator.md similarity index 100% rename from documentation/documentation/docs/evaluators/python_evaluator.md rename to documentation/docs/evaluators/python_evaluator.md diff --git a/documentation/documentation/docs/intro.md b/documentation/docs/intro.md similarity index 100% rename from documentation/documentation/docs/intro.md rename to documentation/docs/intro.md diff --git a/documentation/documentation/docs/projectform/_category_.json b/documentation/docs/projectform/_category_.json similarity index 100% rename from documentation/documentation/docs/projectform/_category_.json rename to documentation/docs/projectform/_category_.json diff --git a/documentation/documentation/docs/projectform/image.png b/documentation/docs/projectform/image.png similarity index 100% rename from documentation/documentation/docs/projectform/image.png rename to documentation/docs/projectform/image.png diff --git a/documentation/documentation/docs/projectform/project_upload_form.md b/documentation/docs/projectform/project_upload_form.md similarity index 100% rename from documentation/documentation/docs/projectform/project_upload_form.md rename to documentation/docs/projectform/project_upload_form.md diff --git a/documentation/documentation/README.md b/documentation/documentation/README.md deleted file mode 100644 index 0c6c2c27..00000000 --- a/documentation/documentation/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Website - -This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. - -### Installation - -``` -$ yarn -``` - -### Local Development - -``` -$ yarn start -``` - -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. - -### Build - -``` -$ yarn build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -Using SSH: - -``` -$ USE_SSH=true yarn deploy -``` - -Not using SSH: - -``` -$ GIT_USER= yarn deploy -``` - -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/python_evaluator.md b/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/python_evaluator.md deleted file mode 100644 index c50653c8..00000000 --- a/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/python_evaluator.md +++ /dev/null @@ -1,13 +0,0 @@ -# Python evaluator -## General usage -This evaluator is responsible for running and executing tests on a student's Python code. - -## Structure -When submitting the project a teacher can add a requirements manifest `req-manifest.txt`, this way only the packages in the requirements file are usable on the evaluator. - -When no manifest is present, students are able to install their own depedencies with a `requirements.txt` and a `dev-requirements.txt`. -Or the teacher can add a `requirements.txt` if they want to pre install dependencies that a are present for testing the project. - -## Running tests -When a `run_tests.sh` is present in the project assignment files, it will be run when the student is submitting their code. -When running tests, it's important to note that the root of the student's submission will be `/submission`. diff --git a/documentation/documentation/docusaurus.config.ts b/documentation/docusaurus.config.ts similarity index 100% rename from documentation/documentation/docusaurus.config.ts rename to documentation/docusaurus.config.ts diff --git a/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/_category_.json b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/_category_.json similarity index 100% rename from documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/_category_.json rename to documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/_category_.json diff --git a/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md similarity index 100% rename from documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md rename to documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/custom_evaluator.md diff --git a/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md similarity index 100% rename from documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md rename to documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/general_evaluator.md diff --git a/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/python_evaluator.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/python_evaluator.md new file mode 100644 index 00000000..4bdf1d92 --- /dev/null +++ b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/evaluators/python_evaluator.md @@ -0,0 +1,13 @@ +# Python evaluator +## Algemeen gebruik +Deze evaluator is verantwoordelijk voor het uitvoeren en testen van de Python-code van een student. + +## Structuur +Bij het indienen van het project kan een leraar vereisten toevoegen via het bestand `req-manifest.txt`. Op deze manier zijn alleen de pakketten in het vereistenbestand bruikbaar op de evaluator. + +Wanneer er geen manifest aanwezig is, kunnen studenten hun eigen paketten installeren met een `requirements.txt` en een `dev-requirements.txt`. +Of de leraar kan een `requirements.txt` toevoegen als ze paketten vooraf willen installeren die aanwezig moeten zijn voor het testen van het project. + +## Tests uitvoeren +Als er een `run_tests.sh` aanwezig is in de projectopdrachtbestanden, wordt dit uitgevoerd wanneer de student zijn code indient. +Bij het uitvoeren van tests is het belangrijk op te merken dat de map van de inzending van de student `/submission` zal zijn. diff --git a/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md similarity index 100% rename from documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md rename to documentation/i18n/nl/docusaurus-plugin-content-docs/current/intro.md diff --git a/documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/projectform/project_upload_form.md b/documentation/i18n/nl/docusaurus-plugin-content-docs/current/projectform/project_upload_form.md similarity index 100% rename from documentation/documentation/i18n/nl/docusaurus-plugin-content-docs/current/projectform/project_upload_form.md rename to documentation/i18n/nl/docusaurus-plugin-content-docs/current/projectform/project_upload_form.md diff --git a/documentation/documentation/package-lock.json b/documentation/package-lock.json similarity index 100% rename from documentation/documentation/package-lock.json rename to documentation/package-lock.json diff --git a/documentation/documentation/package.json b/documentation/package.json similarity index 100% rename from documentation/documentation/package.json rename to documentation/package.json diff --git a/documentation/documentation/sidebars.ts b/documentation/sidebars.ts similarity index 100% rename from documentation/documentation/sidebars.ts rename to documentation/sidebars.ts diff --git a/documentation/documentation/src/components/HomepageFeatures/index.tsx b/documentation/src/components/HomepageFeatures/index.tsx similarity index 100% rename from documentation/documentation/src/components/HomepageFeatures/index.tsx rename to documentation/src/components/HomepageFeatures/index.tsx diff --git a/documentation/documentation/src/components/HomepageFeatures/styles.module.css b/documentation/src/components/HomepageFeatures/styles.module.css similarity index 100% rename from documentation/documentation/src/components/HomepageFeatures/styles.module.css rename to documentation/src/components/HomepageFeatures/styles.module.css diff --git a/documentation/documentation/src/css/custom.css b/documentation/src/css/custom.css similarity index 100% rename from documentation/documentation/src/css/custom.css rename to documentation/src/css/custom.css diff --git a/documentation/documentation/src/pages/index.module.css b/documentation/src/pages/index.module.css similarity index 100% rename from documentation/documentation/src/pages/index.module.css rename to documentation/src/pages/index.module.css diff --git a/documentation/documentation/src/pages/index.tsx b/documentation/src/pages/index.tsx similarity index 100% rename from documentation/documentation/src/pages/index.tsx rename to documentation/src/pages/index.tsx diff --git a/documentation/documentation/static/.nojekyll b/documentation/static/.nojekyll similarity index 100% rename from documentation/documentation/static/.nojekyll rename to documentation/static/.nojekyll diff --git a/documentation/documentation/static/img/logo_app.png b/documentation/static/img/logo_app.png similarity index 100% rename from documentation/documentation/static/img/logo_app.png rename to documentation/static/img/logo_app.png diff --git a/documentation/documentation/static/img/logo_ugent.png b/documentation/static/img/logo_ugent.png similarity index 100% rename from documentation/documentation/static/img/logo_ugent.png rename to documentation/static/img/logo_ugent.png diff --git a/documentation/documentation/static/img/project_form_1.png b/documentation/static/img/project_form_1.png similarity index 100% rename from documentation/documentation/static/img/project_form_1.png rename to documentation/static/img/project_form_1.png diff --git a/documentation/documentation/static/img/project_form_2.png b/documentation/static/img/project_form_2.png similarity index 100% rename from documentation/documentation/static/img/project_form_2.png rename to documentation/static/img/project_form_2.png diff --git a/documentation/documentation/static/img/project_upload_form_3.png b/documentation/static/img/project_upload_form_3.png similarity index 100% rename from documentation/documentation/static/img/project_upload_form_3.png rename to documentation/static/img/project_upload_form_3.png diff --git a/documentation/documentation/static/img/project_upload_form_4.png b/documentation/static/img/project_upload_form_4.png similarity index 100% rename from documentation/documentation/static/img/project_upload_form_4.png rename to documentation/static/img/project_upload_form_4.png diff --git a/documentation/documentation/static/img/project_upload_form_5.png b/documentation/static/img/project_upload_form_5.png similarity index 100% rename from documentation/documentation/static/img/project_upload_form_5.png rename to documentation/static/img/project_upload_form_5.png diff --git a/documentation/documentation/static/img/project_upload_form_6.png b/documentation/static/img/project_upload_form_6.png similarity index 100% rename from documentation/documentation/static/img/project_upload_form_6.png rename to documentation/static/img/project_upload_form_6.png diff --git a/documentation/documentation/static/img/project_upload_form_7.png b/documentation/static/img/project_upload_form_7.png similarity index 100% rename from documentation/documentation/static/img/project_upload_form_7.png rename to documentation/static/img/project_upload_form_7.png diff --git a/documentation/documentation/static/img/undraw_docusaurus_mountain.svg b/documentation/static/img/undraw_docusaurus_mountain.svg similarity index 100% rename from documentation/documentation/static/img/undraw_docusaurus_mountain.svg rename to documentation/static/img/undraw_docusaurus_mountain.svg diff --git a/documentation/documentation/static/img/undraw_docusaurus_react.svg b/documentation/static/img/undraw_docusaurus_react.svg similarity index 100% rename from documentation/documentation/static/img/undraw_docusaurus_react.svg rename to documentation/static/img/undraw_docusaurus_react.svg diff --git a/documentation/documentation/static/img/undraw_docusaurus_tree.svg b/documentation/static/img/undraw_docusaurus_tree.svg similarity index 100% rename from documentation/documentation/static/img/undraw_docusaurus_tree.svg rename to documentation/static/img/undraw_docusaurus_tree.svg diff --git a/documentation/documentation/tsconfig.json b/documentation/tsconfig.json similarity index 100% rename from documentation/documentation/tsconfig.json rename to documentation/tsconfig.json diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 94a5cbcf..0b693055 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -93,7 +93,8 @@ "noFilesPlaceholder": "Nog geen opgave bestanden geupload", "noRegexPlaceholder": "Nog geen regex toegevoegd", "unauthorized": "U heeft niet de juiste rechten om een project aan te maken voor dit vak", - "submissionError": "Er is een fout opgetreden bij het indienen van uw project, probeer het later opnieuw." + "submissionError": "Er is een fout opgetreden bij het indienen van uw project, probeer het later opnieuw.", + "clearSelected": "Deselecteer keuze" }, "projectView": { "submitNetworkError": "Er is iets mislopen bij het opslaan van uw indiening. Probeer het later opnieuw.", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a1402c22..c3a967a0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,7 @@ import ProjectOverView from "./pages/project/projectOverview.tsx"; import { synchronizeJoinCode } from "./loaders/join-code.ts"; import { fetchMe } from "./utils/fetches/FetchMe.ts"; import {fetchProjectForm} from "./components/ProjectForm/project-form.ts"; +import loadSubmissionOverview from "./loaders/submission-overview-loader.ts"; const router = createBrowserRouter( createRoutesFromElements( @@ -34,10 +35,6 @@ const router = createBrowserRouter( } loader={fetchProjectPage} /> }> } loader={fetchProjectPage} /> - } - /> } loader={dataLoaderCourses}/> @@ -49,6 +46,11 @@ const router = createBrowserRouter( element={} loader={fetchProjectPage} /> + } + /> }> } loader={fetchProjectForm}/> diff --git a/frontend/src/components/Courses/AllCoursesTeacher.tsx b/frontend/src/components/Courses/AllCoursesTeacher.tsx index facfa9c2..12b736a8 100644 --- a/frontend/src/components/Courses/AllCoursesTeacher.tsx +++ b/frontend/src/components/Courses/AllCoursesTeacher.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { SideScrollableCourses } from "./CourseUtilComponents"; -import { Course, callToApiToCreateCourse } from "./CourseUtils"; +import {Course, callToApiToCreateCourse, ProjectDetail} from "./CourseUtils"; import { Title } from "../Header/Title"; import { useLoaderData } from "react-router-dom"; @@ -12,7 +12,12 @@ import { useLoaderData } from "react-router-dom"; */ export function AllCoursesTeacher(): JSX.Element { const [open, setOpen] = useState(false); - const courses = (useLoaderData() as Course[]); + const loader = useLoaderData() as { + courses: Course[]; + projects: { [courseId: string]: ProjectDetail[] } + }; + const courses = loader.courses; + const projects = loader.projects; const [courseName, setCourseName] = useState(''); const [error, setError] = useState(''); @@ -49,7 +54,7 @@ export function AllCoursesTeacher(): JSX.Element { <> - + {t('courseForm')}
diff --git a/frontend/src/components/Courses/CourseDetailTeacher.tsx b/frontend/src/components/Courses/CourseDetailTeacher.tsx index df712e43..24d816ee 100644 --- a/frontend/src/components/Courses/CourseDetailTeacher.tsx +++ b/frontend/src/components/Courses/CourseDetailTeacher.tsx @@ -14,6 +14,7 @@ import { Menu, MenuItem, Paper, + Tooltip, Typography, } from "@mui/material"; import { ChangeEvent, useCallback, useEffect, useState } from "react"; @@ -24,7 +25,6 @@ import { getIdFromLink, getNearestFutureDate, getUser, - appHost, ProjectDetail, } from "./CourseUtils"; import { @@ -39,6 +39,7 @@ import { timeDifference } from "../../utils/date-utils"; import { authenticatedFetch } from "../../utils/authenticated-fetch"; import i18next from "i18next"; import { Me } from "../../types/me"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; interface UserUid { uid: string; @@ -179,9 +180,17 @@ export function CourseDetailTeacher(): JSX.Element { return ( <> - - - + + +
{t("projects")}: @@ -191,19 +200,12 @@ export function CourseDetailTeacher(): JSX.Element { - - + + - - - handleDeleteStudent( - navigate, - course.course_id, - selectedStudents - ) - } - > - - {t("deleteSelected")} - - - @@ -291,6 +277,20 @@ export function CourseDetailTeacher(): JSX.Element { + + + handleDeleteStudent( + navigate, + course.course_id, + selectedStudents + ) + } + > + + {t("deleteSelected")} + @@ -486,7 +486,10 @@ function JoinCodeMenu({ }; const handleCopyToClipboard = (join_code: string) => { - navigator.clipboard.writeText(`${appHost}/join-course?code=${join_code}`); + const host = window.location.host; + navigator.clipboard.writeText( + `${host}/${i18next.language}/courses/join?code=${join_code}` + ); }; const getCodes = useCallback(() => { @@ -560,6 +563,9 @@ function JoinCodeMenu({ vertical: "bottom", horizontal: "center", }} + style={{ + width: "25vw", + }} > {t("joinCodes")} @@ -568,25 +574,33 @@ function JoinCodeMenu({ elevation={0} style={{ margin: "1rem", + width: "100%", + maxHeight: "20vh", + height: "20vh", + overflowY: "auto", }} > {codes.map((code: JoinCode) => ( - handleCopyToClipboard(code.join_code)} - key={code.join_code} - > + - - - {code.expiry_time - ? timeDifference(code.expiry_time) - : t("noExpiryDate")} - - - - - {code.for_admins ? t("forAdmins") : t("forStudents")} - + + + + {code.expiry_time + ? timeDifference(code.expiry_time) + : t("noExpiryDate")} + + + {code.for_admins ? t("forAdmins") : t("forStudents")} + + + handleCopyToClipboard(code.join_code)} + > + + + + handleDeleteCode(code.join_code)}> diff --git a/frontend/src/components/Courses/CourseUtilComponents.tsx b/frontend/src/components/Courses/CourseUtilComponents.tsx index 58d01874..08495726 100644 --- a/frontend/src/components/Courses/CourseUtilComponents.tsx +++ b/frontend/src/components/Courses/CourseUtilComponents.tsx @@ -12,9 +12,7 @@ import { } from "@mui/material"; import { Course, - Project, ProjectDetail, - apiHost, getIdFromLink, getNearestFutureDate, } from "./CourseUtils"; @@ -22,7 +20,6 @@ import { Link, useNavigate, useLocation } from "react-router-dom"; import { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import debounce from "debounce"; -import { authenticatedFetch } from "../../utils/authenticated-fetch"; /** * @param text - The text to be displayed @@ -79,9 +76,10 @@ export function SearchBox({ * @returns A component to display courses in horizontal scroller where each course is a card containing its name. */ export function SideScrollableCourses({ - courses, + courses,projects }: { courses: Course[]; + projects: {[courseId: string]: ProjectDetail[];}; }): JSX.Element { //const navigate = useNavigate(); const location = useLocation(); @@ -101,9 +99,6 @@ export function SideScrollableCourses({ const [teacherNameFilter, setTeacherNameFilter] = useState( initialTeacherNameFilter ); - const [projects, setProjects] = useState<{ [courseId: string]: ProjectDetail[] }>( - {} - ); const debouncedHandleSearchChange = useMemo( () => @@ -150,51 +145,6 @@ export function SideScrollableCourses({ setTeacherNameFilter(newTeacherNameFilter); }; - useEffect(() => { - // Fetch projects for each course - const fetchProjects = async () => { - const projectPromises = courses.map((course) => - authenticatedFetch( - `${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}` - ).then((response) => response.json()) - ); - - const projectResults = await Promise.all(projectPromises); - const projectsMap: { [courseId: string]: ProjectDetail[] } = {}; - - projectResults.forEach((result, index) => { - const detailProjectPromises = result.data.map(async (item: Project) => { - const projectRes = await authenticatedFetch(item.project_id); - if (projectRes.status !== 200) { - throw new Response("Failed to fetch project data", { - status: projectRes.status, - }); - } - const projectJson = await projectRes.json(); - const projectData = projectJson.data; - const project: ProjectDetail = { - ...item, - deadlines: projectData.deadlines.map( - ([description, dateString]: [string, string]) => ({ - description, - date: new Date(dateString), - }) - ), - }; - return project; - }); - Promise.all(detailProjectPromises).then((projects) => { - projectsMap[getIdFromLink(courses[index].course_id)] = projects; - setProjects({ ...projectsMap }); - }); - }); - - setProjects(projectsMap); - }; - - fetchProjects(); - }, [courses]); - const filteredCourses = courses.filter( (course) => course.name.toLowerCase().includes(searchTerm.toLowerCase()) && @@ -313,6 +263,8 @@ function EmptyOrNotProjects({ projects: ProjectDetail[]; noProjectsText: string; }): JSX.Element { + const { i18n } = useTranslation(); + const lang = i18n.language; if (projects === undefined || projects.length === 0) { return ( {projects.slice(0, 3).map((project) => { let timeLeft = ""; - if (project.deadlines != undefined) { + if (project.deadlines.length > 0) { const deadline = getNearestFutureDate(project.deadlines); - if(deadline !== null){ + if (deadline !== null) { const deadlineDate = deadline.date; const diffTime = Math.abs(deadlineDate.getTime() - now.getTime()); const diffHours = Math.ceil(diffTime / (1000 * 60 * 60)); const diffDays = Math.ceil(diffHours * 24); - timeLeft = diffDays > 1 ? `${diffDays} days` : `${diffHours} hours`; + timeLeft = + diffDays > 1 ? `${diffDays} days` : `${diffHours} hours`; } } return ( - response.json()) .then((data) => { //But first also make sure that teacher is in the course admins list - authenticatedFetch(`${apiHost}/courses/${getIdFromLink(data.url)}/admins`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ admin_uid: loggedInUid() }), - }); + authenticatedFetch( + `${apiHost}/courses/${getIdFromLink(data.url)}/admins`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ admin_uid: loggedInUid() }), + } + ); navigate(getIdFromLink(data.url)); // navigate to data.url }); } @@ -131,9 +133,59 @@ const fetchData = async (url: string, params?: URLSearchParams) => { export const dataLoaderCourses = async () => { //const params = new URLSearchParams({ 'teacher': loggedInUid() }); - return fetchData(`courses`); + + const courses = await fetchData(`courses`); + const projects = await fetchProjectsCourse(courses); + for( const c of courses){ + const teacher = await fetchData(`users/${c.teacher}`) + c.teacher = teacher.display_name + } + return {courses, projects} }; +/** + * Fetch the projects for the Course component + * @param courses - All the courses + * @returns the projects + */ +export async function fetchProjectsCourse (courses:Course[]) { + const projectPromises = courses.map((course) => + authenticatedFetch( + `${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}` + ).then((response) => response.json()) + ); + + const projectResults = await Promise.all(projectPromises); + const projectsMap: { [courseId: string]: ProjectDetail[] } = {}; + for await (const [index, result] of projectResults.entries()) { + projectsMap[getIdFromLink(courses[index].course_id)] = await Promise.all(result.data.map(async (item: Project) => { + const projectRes = await authenticatedFetch(item.project_id); + if (projectRes.status !== 200) { + throw new Response("Failed to fetch project data", { + status: projectRes.status, + }); + } + const projectJson = await projectRes.json(); + const projectData = projectJson.data; + let projectDeadlines = []; + if (projectData.deadlines) { + projectDeadlines = projectData.deadlines.map( + ([description, dateString]: [string, string]) => ({ + description, + date: new Date(dateString), + }) + ); + } + const project: ProjectDetail = { + ...item, + deadlines: projectDeadlines, + }; + return project; + })); + } + return { ...projectsMap }; +} + const dataLoaderCourse = async (courseId: string) => { return fetchData(`courses/${courseId}`); }; @@ -147,22 +199,30 @@ const dataLoaderProjects = async (courseId: string) => { throw new Response("Failed to fetch data", { status: res.status }); } const jsonResult = await res.json(); - const projects: ProjectDetail[] = jsonResult.data.map(async (item: Project) => { - const projectRes = await authenticatedFetch(item.project_id); - if (projectRes.status !== 200) { - throw new Response("Failed to fetch project data", { status: projectRes.status }); + const projects: ProjectDetail[] = jsonResult.data.map( + async (item: Project) => { + const projectRes = await authenticatedFetch(item.project_id); + if (projectRes.status !== 200) { + throw new Response("Failed to fetch project data", { + status: projectRes.status, + }); + } + const projectJson = await projectRes.json(); + const projectData = projectJson.data; + let projectDeadlines = []; + if (projectData.deadlines) { + projectDeadlines = projectData.deadlines.map((deadline: Deadline) => ({ + description: deadline.description, + date: new Date(deadline.date), + })); + } + const project: ProjectDetail = { + ...item, + deadlines: projectDeadlines, + }; + return project; } - const projectJson = await projectRes.json(); - const projectData = projectJson.data; - const project: ProjectDetail = { - ...item, - deadlines: projectData.deadlines.map((deadline: Deadline) => ({ - description: deadline.description, - date: new Date(deadline.date), - })), - }; - return project; - }); + ); return Promise.all(projects); }; @@ -184,7 +244,6 @@ export const dataLoaderCourseDetail = async ({ if (!courseId) { throw new Error("Course ID is undefined."); } - const course = await dataLoaderCourse(courseId); const projects = await dataLoaderProjects(courseId); const admins = await dataLoaderAdmins(courseId); diff --git a/frontend/src/components/ProjectForm/ProjectForm.tsx b/frontend/src/components/ProjectForm/ProjectForm.tsx index f2273a87..12eb81ff 100644 --- a/frontend/src/components/ProjectForm/ProjectForm.tsx +++ b/frontend/src/components/ProjectForm/ProjectForm.tsx @@ -15,7 +15,7 @@ import { TableBody, Paper, Tooltip, IconButton, Tabs, Tab, } from "@mui/material"; -import React, {useEffect, useState} from "react"; +import React, {useEffect, useState, useTransition} from "react"; import JSZip from 'jszip'; import {useTranslation} from "react-i18next"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -55,6 +55,7 @@ export default function ProjectForm() { // all the stuff needed for submitting a project const [title, setTitle] = useState(''); + const [, setTransition] = useTransition(); const [titleError, setTitleError] = useState(false); const [description, setDescription] = useState(''); @@ -150,7 +151,10 @@ export default function ProjectForm() { setErrorMessage(t("faultySubmission")); } setValidSubmission(constainsDocker); - } else { + } else if(runner === ''){ + setValidRunner(true); + } + else { setValidRunner(containsRuntest); if(!containsRuntest) { setErrorMessage(t("faultySubmission")); @@ -280,7 +284,11 @@ export default function ProjectForm() { label={t("projectTitle")} placeholder={t("projectTitle")} error={titleError} - onChange={event => setTitle(event.target.value)} + onChange={event => { + setTransition(() => { + setTitle(event.target.value); + }) + }} /> @@ -293,7 +301,11 @@ export default function ProjectForm() { multiline rows={4} error={descriptionError} - onChange={event => setDescription(event.target.value)} + onChange={event => { + setTransition(() => { + setDescription(event.target.value); + }) + }} /> diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx index 2e26bb07..956d2f51 100644 --- a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx +++ b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx @@ -1,42 +1,39 @@ -import {Box, Button, Typography} from "@mui/material"; -import {useEffect, useState} from "react"; -import {useParams} from "react-router-dom"; +import { Box, Button, Typography } from "@mui/material"; +import { useLoaderData, useParams } from "react-router-dom"; import ProjectSubmissionsOverviewDatagrid from "./ProjectSubmissionOverviewDatagrid.tsx"; -import download from 'downloadjs'; -import {useTranslation} from "react-i18next"; +import download from "downloadjs"; +import { useTranslation } from "react-i18next"; import { authenticatedFetch } from "../../utils/authenticated-fetch.ts"; -const apiUrl = import.meta.env.VITE_API_HOST +import { Project } from "../Courses/CourseUtils.tsx"; +import { Submission } from "../../types/submission.ts"; + +const APIURL = import.meta.env.VITE_APP_API_HOST; /** * @returns Overview page for submissions */ export default function ProjectSubmissionOverview() { - - const { t } = useTranslation('submissionOverview', { keyPrefix: 'submissionOverview' }); - - useEffect(() => { - fetchProject(); + const { t } = useTranslation("submissionOverview", { + keyPrefix: "submissionOverview", }); - const fetchProject = async () => { - const response = await authenticatedFetch(`${apiUrl}/projects/${projectId}`) - const jsonData = await response.json(); - setProjectTitle(jsonData["data"].title); - - } + const { projectId } = useParams<{ projectId: string }>(); + const { projectData, submissionsWithUsers } = useLoaderData() as { + projectData: Project; + submissionsWithUsers: Submission[]; + }; const downloadProjectSubmissions = async () => { - await authenticatedFetch(`${apiUrl}/projects/${projectId}/submissions-download`) - .then(res => { + await authenticatedFetch( + `${APIURL}/projects/${projectId}/submissions-download` + ) + .then((res) => { return res.blob(); }) - .then(blob => { - download(blob, 'submissions.zip'); + .then((blob) => { + download(blob, "submissions.zip"); }); - } - - const [projectTitle, setProjectTitle] = useState("") - const { projectId } = useParams<{ projectId: string }>(); + }; return ( - {projectTitle} - + + {projectData["title"]} + + - + - ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx index a617d47e..171c7976 100644 --- a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx +++ b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx @@ -1,25 +1,14 @@ -import {useParams} from "react-router-dom"; -import {useEffect, useState} from "react"; -import {DataGrid, GridColDef, GridRenderCellParams} from "@mui/x-data-grid"; -import {Box, IconButton} from "@mui/material"; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import { green, red } from '@mui/material/colors'; -import CancelIcon from '@mui/icons-material/Cancel'; -import DownloadIcon from '@mui/icons-material/Download'; +import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; +import { Box, IconButton } from "@mui/material"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import { green, red } from "@mui/material/colors"; +import CancelIcon from "@mui/icons-material/Cancel"; +import DownloadIcon from "@mui/icons-material/Download"; import download from "downloadjs"; import { authenticatedFetch } from "../../utils/authenticated-fetch"; +import { Submission } from "../../types/submission"; -const apiUrl = import.meta.env.VITE_API_HOST - - interface Submission { - grading: string; - project_id: string; - submission_id: string; - submission_path: string; - submission_status: string; - submission_time: string; - uid: string; - } +const APIURL = import.meta.env.VITE_APP_API_HOST; /** * @returns unique id for datarows @@ -29,68 +18,60 @@ function getRowId(row: Submission) { } const fetchSubmissionsFromUser = async (submission_id: string) => { - await authenticatedFetch(`${apiUrl}/submissions/${submission_id}/download`) - .then(res => { + await authenticatedFetch(`${APIURL}/submissions/${submission_id}/download`) + .then((res) => { return res.blob(); }) - .then(blob => { + .then((blob) => { download(blob, `submissions_${submission_id}.zip`); }); -} +}; const columns: GridColDef[] = [ - { field: 'submission_id', headerName: 'Submission ID', flex: 0.4 }, - { field: 'uid', headerName: 'Student ID', width: 160, flex: 0.4 }, + { field: "submission_id", headerName: "Submission ID", flex: 0.4 }, + { field: "display_name", headerName: "Student", width: 160, flex: 0.4 }, { - field: 'grading', - headerName: 'Grading', + field: "grading", + headerName: "Grading", editable: true, - flex: 0.2 + flex: 0.2, }, { - field: 'submission_status', - headerName: 'Status', + field: "submission_status", + headerName: "Status", renderCell: (params: GridRenderCellParams) => ( <> - { - params.row.submission_status === "SUCCESS" ? ( - - ) : - } + {params.row.submission_status === "SUCCESS" ? ( + + ) : ( + + )} - ) + ), }, { - field: 'submission_path', - headerName: 'Download', + field: "submission_path", + headerName: "Download", renderCell: (params: GridRenderCellParams) => ( - fetchSubmissionsFromUser(params.row.submission_id)}> + fetchSubmissionsFromUser(params.row.submission_id)} + > - ) - }]; + ), + }, +]; /** * @returns the datagrid for displaying submissiosn */ -export default function ProjectSubmissionsOverviewDatagrid() { - const { projectId } = useParams<{ projectId: string }>(); - const [submissions, setSubmissions] = useState([]) - - useEffect(() => { - fetchLastSubmissionsByUser(); - }); - - const fetchLastSubmissionsByUser = async () => { - const response = await authenticatedFetch(`${apiUrl}/projects/${projectId}/latest-per-user`) - const jsonData = await response.json(); - setSubmissions(jsonData.data); - } - +export default function ProjectSubmissionsOverviewDatagrid({ + submissions, +}: { + submissions: Submission[]; +}) { return ( - + - ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/loaders/join-code.ts b/frontend/src/loaders/join-code.ts index 3e693d8c..f559723b 100644 --- a/frontend/src/loaders/join-code.ts +++ b/frontend/src/loaders/join-code.ts @@ -13,13 +13,22 @@ export async function synchronizeJoinCode() { const joinCode = queryParams.get("code"); if (joinCode) { - const response = await authenticatedFetch(new URL("/courses/join", API_URL)); + const response = await authenticatedFetch( + new URL("/courses/join", API_URL), + { + method: "POST", + body: JSON.stringify({ join_code: joinCode }), + headers: { "Content-Type": "application/json" }, + } + ); - if (response.ok) { + if (response.ok || response.status === 409) { const responseData = await response.json(); return redirect( `/${i18next.language}/courses/${responseData.data.course_id}` ); + } else { + throw new Error("Invalid join code"); } } else { throw new Error("No join code provided"); diff --git a/frontend/src/loaders/submission-overview-loader.ts b/frontend/src/loaders/submission-overview-loader.ts new file mode 100644 index 00000000..2ee5ab7c --- /dev/null +++ b/frontend/src/loaders/submission-overview-loader.ts @@ -0,0 +1,57 @@ +import { Params } from "react-router-dom"; +import { Me } from "../types/me"; +import { Submission } from "../types/submission"; +import { authenticatedFetch } from "../utils/authenticated-fetch"; + +const APIURL = import.meta.env.VITE_APP_API_HOST; + +const fetchDisplaynameByUid = async (uids: [string]) => { + const uidParams = new URLSearchParams(); + for (const uid of uids) { + uidParams.append("uid", uid); + } + const uidUrl = `${APIURL}/users?` + uidParams; + const response = await authenticatedFetch(uidUrl); + const jsonData = await response.json(); + + return jsonData.data; +}; + +/** + * + * @param param0 - projectId + * @returns - projectData and submissionsWithUsers + */ +export default async function loadSubmissionOverview({ + params, +}: { + params: Params; +}) { + const projectId = params.projectId; + const projectResponse = await authenticatedFetch( + `${APIURL}/projects/${projectId}` + ); + const projectData = (await projectResponse.json())["data"]; + + const overviewResponse = await authenticatedFetch( + `${APIURL}/projects/${projectId}/latest-per-user` + ); + const jsonData = await overviewResponse.json(); + const uids = jsonData.data.map((submission: Submission) => submission.uid); + const users = await fetchDisplaynameByUid(uids); + + const submissionsWithUsers = jsonData.data.map((submission: Submission) => { + // Find the corresponding user for this submission's UID + const user = users.find((user: Me) => user.uid === submission.uid); + // Add user information to the submission + return { + ...submission, + display_name: user.display_name, + }; + }); + + return { + projectData, + submissionsWithUsers, + }; +} diff --git a/frontend/src/pages/project/projectView/ProjectView.tsx b/frontend/src/pages/project/projectView/ProjectView.tsx index 5d93c41a..dca59a50 100644 --- a/frontend/src/pages/project/projectView/ProjectView.tsx +++ b/frontend/src/pages/project/projectView/ProjectView.tsx @@ -17,7 +17,7 @@ import { Title } from "../../../components/Header/Title"; import { authenticatedFetch } from "../../../utils/authenticated-fetch"; import i18next from "i18next"; -const API_URL = import.meta.env.VITE_API_HOST; +const API_URL = import.meta.env.VITE_APP_API_HOST; interface Project { title: string; @@ -88,7 +88,7 @@ export default function ProjectView() { {projectData.description} {courseData && ( - + {courseData.name} )} diff --git a/frontend/src/pages/project/projectView/SubmissionCard.tsx b/frontend/src/pages/project/projectView/SubmissionCard.tsx index d9d8a898..afd9faa5 100644 --- a/frontend/src/pages/project/projectView/SubmissionCard.tsx +++ b/frontend/src/pages/project/projectView/SubmissionCard.tsx @@ -1,12 +1,13 @@ import { Alert, - Button, Card, CardContent, CardHeader, Grid, IconButton, LinearProgress, + Tab, + Tabs, Typography, } from "@mui/material"; import SendIcon from "@mui/icons-material/Send"; @@ -25,7 +26,7 @@ interface SubmissionCardProps { } /** - * + * * @param params - regexRequirements, submissionUrl, projectId * @returns - SubmissionCard component which allows the user to submit files * and view previous submissions @@ -35,26 +36,29 @@ export default function SubmissionCard({ submissionUrl, projectId, }: SubmissionCardProps) { - const { t } = useTranslation('translation', { keyPrefix: 'projectView' }); + const { t } = useTranslation("translation", { keyPrefix: "projectView" }); const [activeTab, setActiveTab] = useState("submit"); const [selectedFile, setSelectedFile] = useState(null); const [uploadProgress, setUploadProgress] = useState(null); const [errorMessage, setErrorMessage] = useState(null); - const [previousSubmissions, setPreviousSubmissions] = useState([]); + const [previousSubmissions, setPreviousSubmissions] = useState( + [] + ); const handleFileDrop = (file: File) => { setSelectedFile(file); }; useEffect(() => { - - authenticatedFetch(`${submissionUrl}?project_id=${projectId}`).then((response) => { - if (response.ok) { - response.json().then((data) => { - setPreviousSubmissions(data["data"]); - }); + authenticatedFetch(`${submissionUrl}?project_id=${projectId}`).then( + (response) => { + if (response.ok) { + response.json().then((data) => { + setPreviousSubmissions(data["data"]); + }); + } } - }) + ); }, [projectId, submissionUrl]); const handleSubmit = async () => { @@ -99,12 +103,15 @@ export default function SubmissionCard({ - - - + { + setActiveTab(newValue); + }} + > + + + } /> @@ -141,7 +148,10 @@ export default function SubmissionCard({ ) : ( - + )} diff --git a/frontend/src/types/submission.ts b/frontend/src/types/submission.ts index 4522dbac..4eab356f 100644 --- a/frontend/src/types/submission.ts +++ b/frontend/src/types/submission.ts @@ -2,4 +2,5 @@ export interface Submission { submission_id: string; submission_time: string; submission_status: string; + uid: string; }