From 4bfa40f822900da4e2547212d297f7b7c0216de7 Mon Sep 17 00:00:00 2001 From: Gerwoud Van den Eynden <62761483+Gerwoud@users.noreply.github.com> Date: Thu, 9 May 2024 18:00:49 +0200 Subject: [PATCH] uses display name instead of uid (#328) * uses display name instead of uid * re added authentication * fix failing test * changed APP var * removed console log * replaced path and merge conflicts * removed 404 for submissions * fixed page spamming backend with requests * type error --------- Co-authored-by: Aron Buzogany --- .../projects/project_submissions_download.py | 3 - backend/project/endpoints/users.py | 4 + frontend/src/App.tsx | 10 +- .../ProjectSubmissionOverview.tsx | 63 +++++------ .../ProjectSubmissionOverviewDatagrid.tsx | 101 +++++++----------- .../src/loaders/submission-overview-loader.ts | 57 ++++++++++ frontend/src/types/submission.ts | 1 + 7 files changed, 142 insertions(+), 97 deletions(-) create mode 100644 frontend/src/loaders/submission-overview-loader.ts 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/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/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx index 0df85b87..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_APP_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 34f59ef8..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_APP_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/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/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; }