From 5c90d5ede25d9886478ccb451d21878d939589a2 Mon Sep 17 00:00:00 2001 From: Tristan Verbeken Date: Mon, 20 May 2024 21:48:27 +0200 Subject: [PATCH 01/84] React readme removal --- frontend/README.md | 44 -------------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 frontend/README.md diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index cc946687..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Getting Started with React - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). From 877bf126d055bcd402fa51d2d67842740748a896 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Tue, 21 May 2024 00:28:58 +0200 Subject: [PATCH 02/84] Bug fixes --- frontend/index.html | 2 +- frontend/src/@types/requests.d.ts | 12 +- .../forms/projectFormTabs/DockerFormTab.tsx | 12 +- .../forms/projectFormTabs/GeneralFormTab.tsx | 1 + .../projectFormTabs/StructureFormTab.tsx | 2 +- frontend/src/i18n/en/translation.json | 4 +- frontend/src/i18n/nl/translation.json | 6 +- .../components/membersTab/MembersList.tsx | 16 ++- frontend/src/pages/editRole/EditRole.tsx | 2 +- frontend/src/pages/submit/Submit.tsx | 18 ++- .../submit/components/SubmitStructure.tsx | 117 +++++++++--------- 11 files changed, 110 insertions(+), 82 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 72c262f1..d0505398 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index a2f80805..477bfb83 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -32,21 +32,21 @@ export enum ApiRoutes { PROJECT_TESTS_UPLOAD = "api/projects/:id/tests/extrafiles", PROJECT_SUBMIT = "api/projects/:id/submit", PROJECT_DOWNLOAD_ALL_SUBMISSIONS = "api/projects/:id/submissions/files", - + SUBMISSION = "api/submissions/:id", SUBMISSION_FILE = "api/submissions/:id/file", SUBMISSION_STRUCTURE_FEEDBACK= "/api/submissions/:id/structurefeedback", SUBMISSION_DOCKER_FEEDBACK= "/api/submissions/:id/dockerfeedback", SUBMISSION_ARTIFACT="/api/submissions/:id/artifacts", - + CLUSTER = "api/clusters/:id", CLUSTER_FILL = "api/clusters/:id/fill", - GROUP = "api/groups/:id", - GROUP_MEMBERS = "api/groups/:id/members", - GROUP_MEMBER = "api/groups/:id/members/:userId", - GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", + GROUP = "api/groups/:id", + GROUP_MEMBERS = "api/groups/:id/members", + GROUP_MEMBER = "api/groups/:id/members/:userId", + GROUP_SUBMISSIONS = "api/projects/:id/groups/:id/submissions", USER = "api/users/:id", USERS = "api/users", diff --git a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx index 370ee2b3..6c7619bb 100644 --- a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx @@ -118,11 +118,11 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { style={{ fontFamily: "monospace", whiteSpace: "pre", overflowX: "auto" }} /> - + /> */} = ({ form }) => { style={{ fontFamily: "monospace", whiteSpace: "pre", overflowX: "auto" }} /> - + /> */} = ({ form }) => { disabled={dockerDisabled} accept="application/zip, application/x-zip-compressed, application/octet-stream, application/x-zip, *.zip" beforeUpload={ (file) => { - const isPNG = file.type === 'application/zip' - if (!isPNG) { + const isZIP = file.type.includes('zip') || file.name.includes('.zip') + if (!isZIP) { message.error(`${file.name} is not a zip file`); return Upload.LIST_IGNORE } diff --git a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx index f81fd4c7..c9ef712e 100644 --- a/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/GeneralFormTab.tsx @@ -35,6 +35,7 @@ const GeneralFormTab: FC<{ form: FormInstance }> = ({ form }) => { {!visible && ( = ({ form }) => { {t("project.change.fileStructurePreview")}: - + ) } diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 8e6a104e..340914a2 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -140,6 +140,7 @@ "successfullyDeleted": "Project deleted successfully", "deleteProject": "Delete project", "deleteProjectDescription": "Are you sure you want to delete this project? All submissions will be deleted, you cannot undo this action.", + "noStructure": "No specific file structure needed", "change": { "title": "Create project", "updateTitle": "Update {{name}}", @@ -150,7 +151,8 @@ "groupClusterId": "Groups", "groupClusterIdMessage": "Please enter the group cluster", "visible": "Make the project visible", - "visibleAfter": "Choose when the project will be made visible to students, leaving this empty will keep the project invisible", + "visibleAfter": "Make the project visible after", + "visibleAfterTooltip": "Choose when the project will be made visible to students, leaving this empty will keep the project invisible", "maxScore": "Maximum score", "maxScoreMessage": "Please enter the maximum score for the project", "maxScoreHelp": "What is the maximum achievable score for this project? Leaving it empty means the project won't be graded.", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 81b4afa0..7733b8a1 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -10,7 +10,7 @@ }, "courses":{ "courses": "Vakken", - "searchCourse": "Zoek ak", + "searchCourse": "Zoek vak", "member": "Lid", "members": "Leden", "archived": "Gearchiveerd", @@ -142,6 +142,7 @@ "successfullyDeleted": "Project succesvol verwijderd", "deleteProject": "Project verwijderen", "deleteProjectDescription": "Bent u zeker dat u dit project wilt verwijderen? Alle indieningen zullen ook verwijderd worden. Deze actie kan niet ongedaan gemaakt worden.", + "noStructure": "Er specifiek geen bestanden structuur nodig", "change": { "title": "Maak project aan", "name": "Naam", @@ -152,7 +153,8 @@ "groupClusterId": "Groepen", "groupClusterIdMessage": "Vul de Groep cluster in", "visible": "Project zichtbaar maken", - "visibleAfter": "Kies wanneer het project automatisch zichtbaar wordt voor studenten. Als je niets invult, blijft het project onzichtbaar", + "visibleAfter": "Zichtbaar maken na", + "visibleAfterTooltip": "Kies wanneer het project automatisch zichtbaar wordt voor studenten. Als je niets invult, blijft het project onzichtbaar", "maxScore": "Maximum score", "maxScoreMessage": "Vul de maximum score van het project in", "maxScoreHelp": "Wat is de maximale score die je kunt behalen voor dit project? Als je het leeg laat, wordt het project niet beoordeeld", diff --git a/frontend/src/pages/course/components/membersTab/MembersList.tsx b/frontend/src/pages/course/components/membersTab/MembersList.tsx index ab70c80e..398838f4 100644 --- a/frontend/src/pages/course/components/membersTab/MembersList.tsx +++ b/frontend/src/pages/course/components/membersTab/MembersList.tsx @@ -45,17 +45,23 @@ const MembersList: FC<{ members: CourseMemberType[] | null; onChange: (members: const removeUserFromCourse = async (userId: number) => { if (!courseId) return const req = await API.DELETE(ApiRoutes.COURSE_MEMBER, { pathValues: { userId, courseId } }, "message") - if(!req.success) return + if (!req.success) return const newMembers = members?.filter((m) => m.user.userId !== userId) onChange(newMembers ?? []) } const onRoleChange = async (userId: number, role: CourseRelation) => { - if(!courseId) return - const response = await API.PATCH(ApiRoutes.COURSE_MEMBER, { body: { relation: role },pathValues: { userId, courseId } }, "message") - if(!response.success) return - onChange(response.response.data) + if (!courseId) return + const response = await API.PATCH(ApiRoutes.COURSE_MEMBER, { body: { relation: role }, pathValues: { userId, courseId } }, "message") + if (!response.success) return + + const newMembers = members?.map((m) => { + if (m.user.userId === userId) return { user: m.user, relation: role } + return m + }) + + onChange(newMembers ?? []) } return ( diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index 3e215c5f..d72faa8f 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -25,7 +25,7 @@ const ProfileContent = () => { onSearch() }, [debouncedSearchValue]) - function updateRole(user: UsersListItem, role: UserRole) { + const updateRole = (user: UsersListItem, role: UserRole) => { console.log(user, role) apiCall.patch(ApiRoutes.USER, { role: role }, { id: user.id }).then((res) => { console.log(res.data) diff --git a/frontend/src/pages/submit/Submit.tsx b/frontend/src/pages/submit/Submit.tsx index 43f5f8af..c1f8848a 100644 --- a/frontend/src/pages/submit/Submit.tsx +++ b/frontend/src/pages/submit/Submit.tsx @@ -3,12 +3,13 @@ import {useTranslation} from "react-i18next" import SubmitForm from "./components/SubmitForm" import SubmitStructure from "./components/SubmitStructure" import {useNavigate, useParams} from "react-router-dom" -import {useState} from 'react'; +import {useEffect, useState} from 'react'; import apiCall from "../../util/apiFetch"; import {ApiRoutes} from "../../@types/requests.d"; import JSZip from 'jszip'; import {AppRoutes} from "../../@types/routes"; import useAppApi from "../../hooks/useAppApi" +import useApi from "../../hooks/useApi" const Submit = () => { const {t} = useTranslation() @@ -16,7 +17,20 @@ const Submit = () => { const {projectId, courseId} = useParams<{ projectId: string, courseId: string}>() const {message} = useAppApi() const [fileAdded, setFileAdded] = useState(false); + const [structure,setStructure] = useState(null); const navigate = useNavigate() + const API = useApi() + + useEffect(()=> { + if(!projectId) return; + API.GET(ApiRoutes.PROJECT_TESTS, {pathValues: {id: projectId}}).then((e)=> { + if(!e.success) return setStructure("") // if 404, it means there are no tests. + console.log(e.response.data); + setStructure(e.response.data.structureTest) + }) + // API.GET(ApiRoutes.STRC) + + },[projectId]) const onSubmit = async (values: any) => { console.log("Received values of form: ", values) @@ -85,7 +99,7 @@ const Submit = () => { style={{height: "100%"}} styles={{body: {display: "flex", justifyContent: "center"}}} > - + diff --git a/frontend/src/pages/submit/components/SubmitStructure.tsx b/frontend/src/pages/submit/components/SubmitStructure.tsx index c07966e5..0864455f 100644 --- a/frontend/src/pages/submit/components/SubmitStructure.tsx +++ b/frontend/src/pages/submit/components/SubmitStructure.tsx @@ -1,100 +1,103 @@ -import { Tree } from "antd" +import { Tree, Typography } from "antd" import type { TreeDataNode } from "antd" import { FC, memo, useMemo } from "react" +import { useTranslation } from "react-i18next" - -type TreeDataOutput = { tree: TreeDataNode[] | null; error: string | null } - +type TreeDataOutput = { tree: TreeDataNode[] | null; error: string | null, directoryIds: string[] } type TreeNode = { - title: string; - key: string; - isLeaf: boolean; - style?: { color: string }; - children?: TreeNode[]; -}; + title: string + key: string + isLeaf: boolean + style?: { color: string } + children?: TreeNode[] +} function getPrefixLength(line: string): number { - let prefixLength = 0; - while (prefixLength < line.length && (line[prefixLength] === ' ' || line[prefixLength] === '\t')) { - prefixLength++; + let prefixLength = 0 + while (prefixLength < line.length && (line[prefixLength] === " " || line[prefixLength] === "\t")) { + prefixLength++ } - return prefixLength; + return prefixLength } -function parseSubmissionTemplate(lines: string[], prefix="",key=""): TreeNode[] { - let children: TreeNode[] = []; - - while(lines.length > 0 ) { - const leadingWhitespaces = getPrefixLength(lines[0]) - if( leadingWhitespaces < prefix.length) break - - const line = lines.shift()?.trimEnd(); - let newKey = key + line - if (!line?.length) continue; // ignore empty lines - let node:TreeNode = { - title: line.trim(), - isLeaf: true, - key: newKey, - style: undefined, - children: [] - } - - if(line.trimStart().startsWith("-")) { - // ignore file - node.style = { color: "#F44336" } - node.title = node.title.substring(1) - } +function parseSubmissionTemplate(lines: string[], directoryIds: string[], prefix = "", key = ""): TreeNode[] { + let children: TreeNode[] = [] + + while (lines.length > 0) { + const leadingWhitespaces = getPrefixLength(lines[0]) + if (leadingWhitespaces < prefix.length) break + + const line = lines.shift()?.trimEnd() + let newKey = key + line + if (!line?.length) continue // ignore empty lines + let node: TreeNode = { + title: line.trim(), + isLeaf: true, + key: newKey, + style: undefined, + children: [], + } + if (line.trimStart().startsWith("-")) { + // ignore file + node.style = { color: "#F44336" } + node.title = node.title.substring(1) + } - if (line.endsWith('/')) { - node.title = node.title.substring(0,node.title.length-1) - // It's a directory - node.isLeaf = false; - if(lines[0]) { - const nextLineWhitespaces = getPrefixLength(lines[0]) - if(nextLineWhitespaces > leadingWhitespaces) { - node.children = parseSubmissionTemplate(lines,lines[0].substring(0,nextLineWhitespaces),newKey ); - } - } + if (line.endsWith("/")) { + node.title = node.title.substring(0, node.title.length - 1) + // It's a directory + node.isLeaf = false + directoryIds.push(newKey) + if (lines[0]) { + const nextLineWhitespaces = getPrefixLength(lines[0]) + if (nextLineWhitespaces > leadingWhitespaces) { + node.children = parseSubmissionTemplate(lines, directoryIds, lines[0].substring(0, nextLineWhitespaces), newKey) } - children.push(node); + } } + children.push(node) + } - return children; + return children } - export function generateTreeData(structure: string): TreeDataOutput { - if (!structure) return { tree: null, error: "No structure" } + if (!structure) return { tree: null, error: "No structure", directoryIds:[] } // Remove comments (lines that include # until end of the line) structure = structure.replace(/#.*?(?=\n)/g, "") // Split the string into lines const lines = structure.split("\n") let result: TreeNode[] = [] + let directoryIds: string[] = [] try { - result = parseSubmissionTemplate(lines) - + result = parseSubmissionTemplate(lines, directoryIds) } catch (error) { console.error(error) - return { tree: null, error: "Woops something went wrong while parsing!" } // If you get this, then there's a bug in the parser + return { tree: null, error: "Woops something went wrong while parsing!",directoryIds:[] } // If you get this, then there's a bug in the parser } return { tree: result, error: null, + directoryIds } } -const SubmitStructure: FC<{ structure: string }> = ({ structure }) => { - const treeData: { tree: TreeDataNode[] | null; error: string | null } = generateTreeData(structure) +const SubmitStructure: FC<{ structure: string | null; hideEmpty?: boolean }> = ({ structure, hideEmpty }) => { + const { t } = useTranslation() + const treeData: TreeDataOutput = structure === null ? { tree: [{ isLeaf: true, title: "Loading...", key: "loading" }], error: null,directoryIds:[] } : generateTreeData(structure) - + if (structure === "" && !hideEmpty) return {t("project.noStructure")} if (!treeData.tree) return null return ( From 31e3de22344a28202e0f1515c4ef6b488fd3eef0 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Tue, 21 May 2024 01:33:05 +0200 Subject: [PATCH 03/84] Added breadcrumbs --- .../forms/projectFormTabs/DockerFormTab.tsx | 1 - .../forms/projectFormTabs/GroupsFormTab.tsx | 1 - .../layout/breadcrumbs/ProjectBreadcrumbs.tsx | 83 +++++++++++++++++++ .../src/components/layout/nav/AuthNav.tsx | 4 +- .../components/other/GroupMembersTransfer.tsx | 2 - frontend/src/hooks/useApi.tsx | 1 - frontend/src/i18n/en/translation.json | 9 +- frontend/src/i18n/nl/translation.json | 11 ++- .../course/components/groupTab/GroupList.tsx | 1 - .../components/settingsTab/SettingsCard.tsx | 1 - .../src/pages/editProject/EditProject.tsx | 5 +- frontend/src/pages/editRole/EditRole.tsx | 6 +- .../pages/index/components/CourseSection.tsx | 1 - .../index/components/CreateCourseModal.tsx | 1 - frontend/src/pages/project/Project.tsx | 5 +- .../src/pages/project/components/GroupTab.tsx | 1 - .../components/SubmissionStatusTag.tsx | 1 - .../project/components/SubmissionTab.tsx | 3 - .../project/components/SubmissionsTab.tsx | 2 - .../project/components/SubmissionsTable.tsx | 1 - .../src/pages/project/components/createCsv.ts | 1 - .../src/pages/projectCreate/ProjectCreate.tsx | 1 - .../components/GroupClusterModalContent.tsx | 1 - .../components/ProjectCreateService.tsx | 2 - frontend/src/pages/submission/Submission.tsx | 2 +- .../submission/components/SubmissionCard.tsx | 1 - frontend/src/pages/submit/Submit.tsx | 11 +-- .../pages/submit/components/SubmitForm.tsx | 1 - frontend/src/providers/UserProvider.tsx | 5 +- frontend/src/router/CourseRoutes.tsx | 1 - frontend/src/router/ProjectRoutes.tsx | 5 +- 31 files changed, 114 insertions(+), 57 deletions(-) create mode 100644 frontend/src/components/layout/breadcrumbs/ProjectBreadcrumbs.tsx diff --git a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx index 6c7619bb..dfa4a579 100644 --- a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx @@ -12,7 +12,6 @@ const UploadBtn: React.FC<{ form: FormInstance; fieldName: string; textFieldProp const reader = new FileReader() reader.onload = (e) => { const contents = e.target?.result as string - console.log(contents) form.setFieldValue(fieldName, contents) } reader.readAsText(file) diff --git a/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx b/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx index 91efd873..a4d1881d 100644 --- a/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/GroupsFormTab.tsx @@ -41,7 +41,6 @@ const GroupsFormTab: FC<{ form: FormInstance }> = ({ form }) => { allowClear courseId={courseId!} onClusterCreated={(clusterId) => { - console.log("Setting clusterId:", clusterId) form.setFieldValue("groupClusterId", clusterId) }} /> diff --git a/frontend/src/components/layout/breadcrumbs/ProjectBreadcrumbs.tsx b/frontend/src/components/layout/breadcrumbs/ProjectBreadcrumbs.tsx new file mode 100644 index 00000000..5a6c7954 --- /dev/null +++ b/frontend/src/components/layout/breadcrumbs/ProjectBreadcrumbs.tsx @@ -0,0 +1,83 @@ +import { HomeFilled } from "@ant-design/icons" +import { Breadcrumb, BreadcrumbItemProps, BreadcrumbProps, Skeleton } from "antd" +import { FC, useContext, useMemo } from "react" +import { ProjectType } from "../../../pages/index/components/ProjectTableCourse" +import { Link, useLocation, useMatch, useResolvedPath } from "react-router-dom" +import useCourse from "../../../hooks/useCourse" +import { AppRoutes } from "../../../@types/routes" +import { useTranslation } from "react-i18next" +import useCourseUser from "../../../hooks/useCourseUser" +import { UserContext } from "../../../providers/UserProvider" + +const ProjectBreadcrumbs: FC<{ project: ProjectType | null }> = ({ project }) => { + const course = useCourse() + const { courses } = useContext(UserContext) + const { t } = useTranslation() + const matchProject = useMatch(AppRoutes.PROJECT) + const submitMatch = useMatch(AppRoutes.NEW_SUBMISSION) + const submissionMatch = useMatch(AppRoutes.SUBMISSION) + const editProjectMatch = useMatch(AppRoutes.EDIT_PROJECT) + + const items: BreadcrumbProps["items"] = useMemo(() => { + const menuItems: BreadcrumbItemProps["menu"] = { + items: + courses?.map((c) => ({ + key: c.courseId, + title: {c.name}, + })) ?? [], + } + + return [ + { + title: ( + + + + ), + }, + { + title: {course.name}, + menu: menuItems, + }, + ] + }, [courses, course]) + + let breadcrumbs = [...items] + if (breadcrumbs) { + if (matchProject && project) { + breadcrumbs.push({ + title: project.name, + }) + } else { + breadcrumbs.push({ + title: {project?.name || ""}, + }) + if (editProjectMatch) { + breadcrumbs.push({ + title: t("project.breadcrumbs.editPage"), + }) + } + + if (submitMatch) { + breadcrumbs.push({ + title: t("project.breadcrumbs.submit"), + }) + } + + if (submissionMatch) { + breadcrumbs.push({ + title: t("project.breadcrumbs.submission"), + }) + } + } + } + + return ( + + ) +} + +export default ProjectBreadcrumbs diff --git a/frontend/src/components/layout/nav/AuthNav.tsx b/frontend/src/components/layout/nav/AuthNav.tsx index a0d6df1f..4ce2cae4 100644 --- a/frontend/src/components/layout/nav/AuthNav.tsx +++ b/frontend/src/components/layout/nav/AuthNav.tsx @@ -1,5 +1,5 @@ import { useAccount } from "@azure/msal-react" -import { Dropdown, MenuProps, Typography } from "antd" +import { Breadcrumb, Dropdown, MenuProps, Typography } from "antd" import { useTranslation } from "react-i18next" import { UserOutlined, BgColorsOutlined, DownOutlined, LogoutOutlined, PlusOutlined } from "@ant-design/icons" import { msalInstance } from "../../../index" @@ -75,6 +75,8 @@ const AuthNav = () => { return ( <> + +
(null) const { t } = useTranslation() const API = useApi() - console.log(courseMembers, selectedGroup, groups, value); useEffect(()=> { if(courseMembers === null || !groups?.length) return @@ -135,7 +134,6 @@ const GroupMembersTransfer: FC<{ value?: GroupMembers,groups: GroupType[]; onCha // @ts-ignore //TODO: fix the types so i can remove the ts ignore randomGroups[group.name] = groupMembers.map((m) => m.user.userId) } - console.log(randomGroups); // setTargetKeys(randomGroups) if(onChange) onChange(randomGroups) } diff --git a/frontend/src/hooks/useApi.tsx b/frontend/src/hooks/useApi.tsx index da88570a..92602196 100644 --- a/frontend/src/hooks/useApi.tsx +++ b/frontend/src/hooks/useApi.tsx @@ -140,7 +140,6 @@ const useApi = ():UseApiType => { } else if (options.mode === "message") { message.error(errMessage) } else if (options.mode === "page") { - console.log("------"); setError({ status, message: errMessage, diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 340914a2..70c67e6c 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -168,7 +168,7 @@ "groupClusterCreated": "Group cluster created", "fileStructure": "File structure", "fileStructurePreview": "File structure preview", - "update": "Update project", + "update": "Save", "newGroupCluster": "Add a new group cluster", "makeCluster": "Create group cluster", "clusterName": "Cluster name", @@ -210,7 +210,12 @@ "noFeedback": "No feedback provided", "noScoreLabel": "No score", "noFeedbackLabel": "No feedback", - "noSubmissionDownload": "No submission found" + "noSubmissionDownload": "No submission found", + "breadcrumbs": { + "editPage": "Edit project", + "submit": "Submit", + "submission": "Submission" + } }, "group": { "removeUserFromGroup": "Remove {{name}} from group" diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 7733b8a1..fa449539 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -142,7 +142,7 @@ "successfullyDeleted": "Project succesvol verwijderd", "deleteProject": "Project verwijderen", "deleteProjectDescription": "Bent u zeker dat u dit project wilt verwijderen? Alle indieningen zullen ook verwijderd worden. Deze actie kan niet ongedaan gemaakt worden.", - "noStructure": "Er specifiek geen bestanden structuur nodig", + "noStructure": "Er is specifiek geen bestanden structuur nodig", "change": { "title": "Maak project aan", "name": "Naam", @@ -170,7 +170,7 @@ "groupClusterCreated": "Groep cluster aangemaakt", "fileStructure": "Bestandsstructuur", "fileStructurePreview": "Voorbeeld van bestandsstructuur", - "update": "Project aanpassen", + "update": "Bewaren", "newGroupCluster": "Voeg een nieuwe groep cluster toe", "makeCluster": "Groep cluster aanmaken", "clusterName": "Cluster naam", @@ -212,7 +212,12 @@ "noFeedback": "Geen feedback gegeven", "noScoreLabel": "Geen score", "noFeedbackLabel": "Geen feedback", - "noSubmissionDownload": "Geen indiening gevonden" + "noSubmissionDownload": "Geen indiening gevonden", + "breadcrumbs": { + "editPage": "project aanpassen", + "submit": "indienen", + "submission": "indiening" + } }, "group": { "removeUserFromGroup": "Verwijder {{name}} uit deze groep" diff --git a/frontend/src/pages/course/components/groupTab/GroupList.tsx b/frontend/src/pages/course/components/groupTab/GroupList.tsx index 0c26399e..9b2d95a4 100644 --- a/frontend/src/pages/course/components/groupTab/GroupList.tsx +++ b/frontend/src/pages/course/components/groupTab/GroupList.tsx @@ -140,7 +140,6 @@ const GroupList: FC<{ locked:ClusterType["lockGroupsAfter"] ,groups: GroupType[] setLoading(false) } - console.log("Group: ", groupId); return ( <> diff --git a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx index 3d1dd86c..70700a1d 100644 --- a/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx +++ b/frontend/src/pages/course/components/settingsTab/SettingsCard.tsx @@ -33,7 +33,6 @@ const SettingsCard: FC = () => { await form.validateFields() const values:{name:string, description:string} = form.getFieldsValue() - console.log(values); values.description ??= "" setLoading(true) const res = await API.PATCH(ApiRoutes.COURSE, { body: values, pathValues: { courseId: course.courseId } }, "message") diff --git a/frontend/src/pages/editProject/EditProject.tsx b/frontend/src/pages/editProject/EditProject.tsx index ef7c6b62..670d6e6f 100644 --- a/frontend/src/pages/editProject/EditProject.tsx +++ b/frontend/src/pages/editProject/EditProject.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate, useLocation } from "react-router-dom" import { Button, Form, UploadProps } from "antd" import { useTranslation } from "react-i18next" import ProjectForm from "../../components/forms/ProjectForm" -import { EditFilled } from "@ant-design/icons" +import { EditFilled, SaveFilled } from "@ant-design/icons" import { FormProps } from "antd/lib" import { ProjectFormData } from "../projectCreate/components/ProjectCreateService" import useProject from "../../hooks/useProject" @@ -25,7 +25,6 @@ const EditProject: React.FC = () => { const project = useProject() const { updateProject } = useContext(ProjectContext) const [initialDockerValues, setInitialDockerValues] = useState(null) - const location = useLocation() const updateDockerForm = async () => { if (!projectId) return @@ -155,7 +154,7 @@ const EditProject: React.FC = () => { diff --git a/frontend/src/pages/index/components/ProjectCard.tsx b/frontend/src/pages/index/components/ProjectCard.tsx index e5244e3c..6e8aa088 100644 --- a/frontend/src/pages/index/components/ProjectCard.tsx +++ b/frontend/src/pages/index/components/ProjectCard.tsx @@ -60,6 +60,7 @@ const ProjectCard: FC<{ courseId?: number }> = ({ courseId }) => { /> ) : ( diff --git a/frontend/src/pages/index/components/ProjectTable.tsx b/frontend/src/pages/index/components/ProjectTable.tsx index aeffe10e..69e63569 100644 --- a/frontend/src/pages/index/components/ProjectTable.tsx +++ b/frontend/src/pages/index/components/ProjectTable.tsx @@ -11,7 +11,7 @@ import { AppRoutes } from "../../../@types/routes" export type ProjectType = GET_Responses[ApiRoutes.PROJECT] -const ProjectTable: FC<{ projects: ProjectType[]|null,ignoreColumns?: string[] }> = ({ projects,ignoreColumns }) => { +const ProjectTable: FC<{ projects: ProjectType[]|null,ignoreColumns?: string[], noFilter?:boolean }> = ({ projects,ignoreColumns,noFilter }) => { const { t } = useTranslation() const { modal } = useAppApi() @@ -54,7 +54,7 @@ const ProjectTable: FC<{ projects: ProjectType[]|null,ignoreColumns?: string[] } const deadlineTimestamp = new Date(record.deadline).getTime(); return value === 'notPassed' ? deadlineTimestamp >= currentTimestamp : true; }, - defaultFilteredValue: ["notPassed"] , + defaultFilteredValue: noFilter ? [] : ["notPassed"] , render: (text: string) => new Date(text).toLocaleString(i18n.language, { year: "numeric", diff --git a/frontend/src/pages/index/components/ProjectTableCourse.tsx b/frontend/src/pages/index/components/ProjectTableCourse.tsx index d9ebaf0e..dbe49320 100644 --- a/frontend/src/pages/index/components/ProjectTableCourse.tsx +++ b/frontend/src/pages/index/components/ProjectTableCourse.tsx @@ -56,7 +56,6 @@ const ProjectTableCourse: FC<{ projects: ProjectType[] | null, ignoreColumns?: s const deadlineTimestamp = new Date(record.deadline).getTime(); return value === 'notPassed' ? deadlineTimestamp >= currentTimestamp : true; }, - defaultFilteredValue: ["notPassed"], render: (text: string) => new Date(text).toLocaleString(i18n.language, { year: "numeric", diff --git a/frontend/src/pages/project/components/SubmissionsTable.tsx b/frontend/src/pages/project/components/SubmissionsTable.tsx index 2e999657..20fe2931 100644 --- a/frontend/src/pages/project/components/SubmissionsTable.tsx +++ b/frontend/src/pages/project/components/SubmissionsTable.tsx @@ -22,7 +22,7 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha const { courseId, projectId } = useParams() const { message } = useAppApi() const API = useApi() - const updateTable = async (groupId: number, feedback: Partial, usePost: boolean) => { + const updateTable = async (groupId: number, feedback: Omit, usePost: boolean) => { if (!projectId || submissions === null || !groupId) return console.error("No projectId or submissions or groupId found") let res @@ -30,17 +30,13 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha res = await API.POST( ApiRoutes.PROJECT_SCORE, { - body: { - score: 0, - feedback: "", - ...feedback, - }, + body: feedback, pathValues: { id: projectId, groupId }, }, "message" ) } else { - res = await API.PATCH(ApiRoutes.PROJECT_SCORE, { body: feedback, pathValues: { id: projectId, groupId } }, "message") + res = await API.PUT(ApiRoutes.PROJECT_SCORE, { body: feedback, pathValues: { id: projectId, groupId } }, "message") } if (!res.success) return @@ -69,11 +65,11 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha else score = parseFloat(scoreStr) if (isNaN(score as number)) score = null if (score !== null && score > project.maxScore) return message.error(t("project.scoreTooHigh")) - await updateTable(s.group.groupId, { score }, s.feedback === null) + await updateTable(s.group.groupId, { score:score||null, feedback:s.feedback?.feedback??"" }, s.feedback === null) } const updateFeedback = async (s: ProjectSubmissionsType, feedback: string) => { - await updateTable(s.group.groupId, { feedback }, s.feedback === null) + await updateTable(s.group.groupId, { feedback, score: s.feedback?.score||null }, s.feedback === null) } const downloadFile = async (route: ApiRoutes.SUBMISSION_FILE | ApiRoutes.SUBMISSION_ARTIFACT, filename: string) => { @@ -147,12 +143,6 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha dataIndex: "submission", key: "submission", render: (time: ProjectSubmissionsType["submission"]) => time?.submissionTime && {new Date(time.submissionTime).toLocaleString()}, - sorter: (a: ProjectSubmissionsType, b: ProjectSubmissionsType) => { - // Implement sorting logic for submissionTime column - const timeA: any = a.submission?.submissionTime || 0 - const timeB: any = b.submission?.submissionTime || 0 - return timeA - timeB - }, }, ] From d1769298bb60462d5adfc54480db9c8facb5d77b Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Tue, 21 May 2024 13:47:41 +0200 Subject: [PATCH 08/84] Small fix to backend --- .../com/ugent/pidgeon/controllers/SubmissionController.java | 4 ---- .../com/ugent/pidgeon/model/json/GroupFeedbackJson.java | 6 +++--- .../ugent/pidgeon/postgre/models/GroupFeedbackEntity.java | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index cf07c916..9cb06685 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -124,7 +124,6 @@ private Map> getLatestSubmissionsForProject(lon @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/submissions") //Route to get all submissions for a project @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getSubmissions(@PathVariable("projectid") long projectid, Auth auth) { - try { CheckResult checkResult = projectUtil.isProjectAdmin(projectid, auth.getUserEntity()); if (!checkResult.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); @@ -154,9 +153,6 @@ public ResponseEntity getSubmissions(@PathVariable("projectid") long projecti } return ResponseEntity.ok(res); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); - } } /** diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJson.java index e9a9b58a..4510e860 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/GroupFeedbackJson.java @@ -2,7 +2,7 @@ public class GroupFeedbackJson { - private float score; + private Float score; private String feedback; private long groupId; @@ -11,7 +11,7 @@ public class GroupFeedbackJson { public GroupFeedbackJson() { } - public GroupFeedbackJson(float score, String feedback, long groupId, long projectId) { + public GroupFeedbackJson(Float score, String feedback, long groupId, long projectId) { this.score = score; this.feedback = feedback; this.groupId = groupId; @@ -19,7 +19,7 @@ public GroupFeedbackJson(float score, String feedback, long groupId, long projec } - public float getScore() { + public Float getScore() { return score; } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java index 05d30eea..20c479d0 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/GroupFeedbackEntity.java @@ -52,11 +52,11 @@ public String getFeedback() { } - public float getScore() { + public Float getScore() { return grade; } - public void setScore(float score) { + public void setScore(Float score) { this.grade = score; } From 2e9dbca120320585f7c611f2e3c1025c8cf783cf Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Tue, 21 May 2024 14:18:44 +0200 Subject: [PATCH 09/84] Even more bug fixes --- frontend/src/i18n/en/translation.json | 5 +++-- frontend/src/i18n/nl/translation.json | 5 +++-- .../pages/course/components/groupTab/GroupsCard.tsx | 8 +------- frontend/src/pages/editRole/EditRole.tsx | 10 +++------- frontend/src/pages/profile/Profile.tsx | 5 +---- .../pages/project/components/SubmissionsTable.tsx | 13 ++++++++----- frontend/src/pages/submission/Submission.tsx | 7 +------ .../pages/submission/components/SubmissionCard.tsx | 13 +------------ .../submission/components/SubmissionCardContent.tsx | 8 ++++---- 9 files changed, 25 insertions(+), 49 deletions(-) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 70c67e6c..5e7ca050 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -64,8 +64,8 @@ }, "groupProgress": "Group progress", "completeProgress": "{{count}} / {{total}} completed", - "activeProjects": "{{count}} active project", - "activeProjects_plural": "{{count}} active projects", + "activeProjects": "{{count}} project", + "activeProjects_plural": "{{count}} projects", "userCourseCount": "{{count}} user is in this course", "userCourseCount_plural": "{{count}} users are in this course" }, @@ -242,6 +242,7 @@ "failed": "failed", "expected": "Expected output:", "received": "Received output:", + "optional": "optional", "status": { "accepted": "All tests succeeded.", "failed": "Some tests failed." diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index fa449539..92ff23a3 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -63,8 +63,8 @@ }, "groupProgress": "Voortgang groep", "completeProgress": "{{count}} / {{total}} voltooid", - "activeProjects": "{{count}} actief project", - "activeProjects_plural": "{{count}} actieve projecten", + "activeProjects": "{{count}} project", + "activeProjects_plural": "{{count}} projecten", "submit": "Indienen", "userCourseCount_plural": "{{count}} gebruikers in dit vak", "userCourseCount": "{{count}} gebruiker in dit vak" @@ -244,6 +244,7 @@ "failed": "Niet geslaagd", "expected": "Vewachte output:", "received": "Ontvangen output:", + "optional": "optioneel", "status": { "accepted": "Alle testen zijn geslaagd.", "failed": "Sommige testen zijn niet geslaagd." diff --git a/frontend/src/pages/course/components/groupTab/GroupsCard.tsx b/frontend/src/pages/course/components/groupTab/GroupsCard.tsx index 3c8615a2..fe8be598 100644 --- a/frontend/src/pages/course/components/groupTab/GroupsCard.tsx +++ b/frontend/src/pages/course/components/groupTab/GroupsCard.tsx @@ -13,7 +13,6 @@ const GroupsCard: FC<{ courseId: number | null; cardProps?: CardProps }> = ({ co const { t } = useTranslation() const API = useApi() useEffect(() => { - // TODO: do the fetch (get all clusters from the course ) fetchGroups().catch(console.error) }, [courseId]) @@ -25,9 +24,6 @@ const GroupsCard: FC<{ courseId: number | null; cardProps?: CardProps }> = ({ co setGroups(res.response.data) } - // if(!groups) return
- // - //
const items: CollapseProps["items"] = useMemo(()=> groups?.map((cluster) => ({ key: cluster.clusterId.toString(), @@ -51,9 +47,7 @@ const GroupsCard: FC<{ courseId: number | null; cardProps?: CardProps }> = ({ co if (!items) return (
- - - +
) return ( diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index c3f51e3a..22d2cf79 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -1,12 +1,11 @@ -import { useEffect, useState, useRef } from "react" -import { Row, Col, Form, Input, Button, Spin, Select, Typography } from "antd" +import { useEffect, useState } from "react" +import { Form, Input, Spin, Select, Typography } from "antd" import UserList from "./components/UserList" import { ApiRoutes, GET_Responses, UserRole } from "../../@types/requests.d" import apiCall from "../../util/apiFetch" import { useTranslation } from "react-i18next" import { UsersListItem } from "./components/UserList" import { useDebounceValue } from "usehooks-ts" -import { User } from "../../providers/UserProvider" export type UsersType = GET_Responses[ApiRoutes.USERS] type SearchType = "name" | "surname" | "email" @@ -105,10 +104,7 @@ const ProfileContent = () => { <> {loading ? (
- +
) : ( { if (user === null) { return (
- +
) } diff --git a/frontend/src/pages/project/components/SubmissionsTable.tsx b/frontend/src/pages/project/components/SubmissionsTable.tsx index 20fe2931..fb78f5f0 100644 --- a/frontend/src/pages/project/components/SubmissionsTable.tsx +++ b/frontend/src/pages/project/components/SubmissionsTable.tsx @@ -12,8 +12,11 @@ import { ApiRoutes, PUT_Requests } from "../../../@types/requests.d" import useAppApi from "../../../hooks/useAppApi" import useApi from "../../../hooks/useApi" -const GroupMember = ({ name }: ProjectSubmissionsType["group"]["members"][number]) => { - return {name} +const GroupMember = ({ name,studentNumber }: ProjectSubmissionsType["group"]["members"][number]) => { + return + + + } const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onChange: (s: ProjectSubmissionsType[]) => void, withArtifacts?:boolean }> = ({ submissions, onChange,withArtifacts }) => { @@ -192,14 +195,14 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha {t("project.feedback")}:

+ updateFeedback(g, value), }} - type={g.feedback?.feedback ? undefined : "secondary"} - > - {g.feedback?.feedback || t('project.noFeedbackLabel')} + > + {g.feedback?.feedback??t('project.noFeedbackLabel')}
diff --git a/frontend/src/pages/submission/Submission.tsx b/frontend/src/pages/submission/Submission.tsx index 2f5ae94a..1e51a318 100644 --- a/frontend/src/pages/submission/Submission.tsx +++ b/frontend/src/pages/submission/Submission.tsx @@ -40,12 +40,7 @@ const Submission = () => { if (submission === null) { return (
- - - +
) } diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index ccbb2a9f..1f0a712e 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -60,18 +60,7 @@ const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({ submission } }, }} type="inner" - title={ - - - {t("submission.submission")} - - } + title= {t("submission.submission")} extra={ {submission.fileUrl && } diff --git a/frontend/src/pages/submission/components/SubmissionCardContent.tsx b/frontend/src/pages/submission/components/SubmissionCardContent.tsx index f3bcf3ed..0131cacc 100644 --- a/frontend/src/pages/submission/components/SubmissionCardContent.tsx +++ b/frontend/src/pages/submission/components/SubmissionCardContent.tsx @@ -1,4 +1,4 @@ -import { Collapse, Flex, Input, Spin, Typography } from "antd" +import { Collapse, Flex, Input, Spin, Tag, Typography } from "antd" import { useTranslation } from "react-i18next" import { SubTest } from "../../../@types/requests" import { FC } from "react" @@ -15,17 +15,17 @@ const SubmissionContent: FC<{ submission: SubmissionType }> = ({ submission }) = return ( {`${test.testName}: ${successText}`}} + header={<>{`${test.testName}: ${successText}`} {!test.required && ({t("submission.optional")})}} > {test.testDescription}
{t("submission.expected")} - +
{t("submission.received")} - +
From 4a8dd03899701f65227092cf363a29999992f4b1 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Tue, 21 May 2024 15:03:13 +0200 Subject: [PATCH 10/84] Updated submission page --- frontend/src/@types/requests.d.ts | 2 +- frontend/src/i18n/en/translation.json | 2 + frontend/src/i18n/nl/translation.json | 4 + .../submission/components/SubmissionCard.tsx | 1 - .../components/SubmissionCardContent.tsx | 109 +++++++++++------- 5 files changed, 73 insertions(+), 45 deletions(-) diff --git a/frontend/src/@types/requests.d.ts b/frontend/src/@types/requests.d.ts index 477bfb83..2de125e8 100644 --- a/frontend/src/@types/requests.d.ts +++ b/frontend/src/@types/requests.d.ts @@ -229,7 +229,7 @@ export type GET_Responses = { projectUrl: ApiRoutes.PROJECT groupUrl: ApiRoutes.GROUP | null fileUrl: ApiRoutes.SUBMISSION_FILE - structureFeedback: ApiRoutes.SUBMISSION_STRUCTURE_FEEDBACK + structureFeedback: string dockerFeedback: DockerFeedback, artifactUrl: ApiRoutes.SUBMISSION_ARTIFACT | null } diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 5e7ca050..3a48d192 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -243,6 +243,8 @@ "expected": "Expected output:", "received": "Received output:", "optional": "optional", + "structureTestSuccess": "Submission meets the structure requirements", + "structureTestFailed": "Submission does not meets the structure requirements", "status": { "accepted": "All tests succeeded.", "failed": "Some tests failed." diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 92ff23a3..4d959bc3 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -245,6 +245,10 @@ "expected": "Vewachte output:", "received": "Ontvangen output:", "optional": "optioneel", + "structureTestSuccess": "Indiening voldoet aan de structuur", + "structureTestFailed": "Indiening voldoet niet aan de structuur", + "submissionSuccess": "Indiening geslaagd", + "submissionFailed": "Indiening niet geslaagd", "status": { "accepted": "Alle testen zijn geslaagd.", "failed": "Sommige testen zijn niet geslaagd." diff --git a/frontend/src/pages/submission/components/SubmissionCard.tsx b/frontend/src/pages/submission/components/SubmissionCard.tsx index 1f0a712e..71895f17 100644 --- a/frontend/src/pages/submission/components/SubmissionCard.tsx +++ b/frontend/src/pages/submission/components/SubmissionCard.tsx @@ -13,7 +13,6 @@ export type SubmissionType = GET_Responses[ApiRoutes.SUBMISSION] const SubmissionCard: React.FC<{ submission: SubmissionType }> = ({ submission }) => { const { token } = theme.useToken() const { t } = useTranslation() - const navigate = useNavigate() const API = useApi() diff --git a/frontend/src/pages/submission/components/SubmissionCardContent.tsx b/frontend/src/pages/submission/components/SubmissionCardContent.tsx index 0131cacc..85ae6134 100644 --- a/frontend/src/pages/submission/components/SubmissionCardContent.tsx +++ b/frontend/src/pages/submission/components/SubmissionCardContent.tsx @@ -1,8 +1,9 @@ -import { Collapse, Flex, Input, Spin, Tag, Typography } from "antd" +import { Collapse, Flex, Input, Space, Spin, Tag, Typography } from "antd" import { useTranslation } from "react-i18next" import { SubTest } from "../../../@types/requests" import { FC } from "react" import { SubmissionType } from "./SubmissionCard" +import { CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons" const SubmissionContent: FC<{ submission: SubmissionType }> = ({ submission }) => { const { t } = useTranslation() @@ -15,18 +16,42 @@ const SubmissionContent: FC<{ submission: SubmissionType }> = ({ submission }) = return ( {`${test.testName}: ${successText}`} {!test.required && ({t("submission.optional")})}} + header={ + <> + {`${test.testName}: ${successText}`}{" "} + {!test.required && ( + + ({t("submission.optional")}) + + )} + + } > {test.testDescription} - -
+ +
{t("submission.expected")} - +
-
+
{t("submission.received")} - - +
@@ -49,49 +74,47 @@ const SubmissionContent: FC<{ submission: SubmissionType }> = ({ submission }) = ) return ( <> - {t("submission.structuretest")} - - {submission.dockerStatus === "no_test" &&
    -
  • - {submission.structureAccepted ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.structureAccepted ? null : ( -
    - {submission.structureFeedback === null ? ( - - ) : ( + {!submission.structureAccepted && ( + <> + {t("submission.structuretest")} +
  • + {submission.structureAccepted ? ( + {t("submission.structureTestSuccess")} + ) : ( + <> + {t("submission.structureTestFailed")} - )} -
- )} - - } + + )} + + + )} + + {submission.dockerStatus === "no_test" && submission.structureAccepted && {t("submission.submissionSuccess")}} {submission.dockerStatus === "finished" && ( -
    -
  • - <> - {submission.dockerFeedback.allowed ? t("submission.status.accepted") : t("submission.status.failed")} - {submission.dockerFeedback.type === "SIMPLE" ? ( -
    - -
    - ) : submission.dockerFeedback.type === "TEMPLATE" ? ( - TestResults(submission.dockerFeedback.feedback.subtests) - ) : null} - -
  • -
+
+ <> + {submission.dockerFeedback.allowed ? t("submission.status.accepted") : t("submission.status.failed")} + {submission.dockerFeedback.type === "SIMPLE" ? ( +
+ +
+ ) : submission.dockerFeedback.type === "TEMPLATE" ? ( + TestResults(submission.dockerFeedback.feedback.subtests) + ) : null} + +
)} ) From 6bf44c5c5c5832a630298c63b5300e0bb172e954 Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Tue, 21 May 2024 15:33:38 +0200 Subject: [PATCH 11/84] Improved submission page --- frontend/src/i18n/en/translation.json | 1 + frontend/src/i18n/nl/translation.json | 1 + .../src/pages/submission/components/SubmissionCardContent.tsx | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 3a48d192..c9fada70 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -245,6 +245,7 @@ "optional": "optional", "structureTestSuccess": "Submission meets the structure requirements", "structureTestFailed": "Submission does not meets the structure requirements", + "tests": "Automated tests:", "status": { "accepted": "All tests succeeded.", "failed": "Some tests failed." diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 4d959bc3..92e171fa 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -249,6 +249,7 @@ "structureTestFailed": "Indiening voldoet niet aan de structuur", "submissionSuccess": "Indiening geslaagd", "submissionFailed": "Indiening niet geslaagd", + "tests": "Automatische testen", "status": { "accepted": "Alle testen zijn geslaagd.", "failed": "Sommige testen zijn niet geslaagd." diff --git a/frontend/src/pages/submission/components/SubmissionCardContent.tsx b/frontend/src/pages/submission/components/SubmissionCardContent.tsx index 85ae6134..7fad8061 100644 --- a/frontend/src/pages/submission/components/SubmissionCardContent.tsx +++ b/frontend/src/pages/submission/components/SubmissionCardContent.tsx @@ -74,7 +74,6 @@ const SubmissionContent: FC<{ submission: SubmissionType }> = ({ submission }) = ) return ( <> - {!submission.structureAccepted && ( <> {t("submission.structuretest")}
  • @@ -93,12 +92,12 @@ const SubmissionContent: FC<{ submission: SubmissionType }> = ({ submission }) = )}
  • - )} {submission.dockerStatus === "no_test" && submission.structureAccepted && {t("submission.submissionSuccess")}} {submission.dockerStatus === "finished" && (
    + {t("submission.tests")}:
    <> {submission.dockerFeedback.allowed ? t("submission.status.accepted") : t("submission.status.failed")} {submission.dockerFeedback.type === "SIMPLE" ? ( From 0a1ed77623454abd8fc86bb2e04699b3ece57f2d Mon Sep 17 00:00:00 2001 From: usserwoutV2 Date: Tue, 21 May 2024 15:48:20 +0200 Subject: [PATCH 12/84] Update user object when role changes --- frontend/src/pages/editRole/EditRole.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/editRole/EditRole.tsx b/frontend/src/pages/editRole/EditRole.tsx index 22d2cf79..948b189f 100644 --- a/frontend/src/pages/editRole/EditRole.tsx +++ b/frontend/src/pages/editRole/EditRole.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +import { useContext, useEffect, useState } from "react" import { Form, Input, Spin, Select, Typography } from "antd" import UserList from "./components/UserList" import { ApiRoutes, GET_Responses, UserRole } from "../../@types/requests.d" @@ -6,12 +6,14 @@ import apiCall from "../../util/apiFetch" import { useTranslation } from "react-i18next" import { UsersListItem } from "./components/UserList" import { useDebounceValue } from "usehooks-ts" +import { UserContext } from "../../providers/UserProvider" +import useUser from "../../hooks/useUser" export type UsersType = GET_Responses[ApiRoutes.USERS] type SearchType = "name" | "surname" | "email" const ProfileContent = () => { const [users, setUsers] = useState(null) - + const myself = useUser() const [loading, setLoading] = useState(false) const [form] = Form.useForm() const searchValue = Form.useWatch("search", form) @@ -35,6 +37,9 @@ const ProfileContent = () => { return u; }); setUsers(updatedUsers?updatedUsers:null); + if(user.id === myself.user?.id){ + myself.updateUser() + } }) } From b34da36cfdf6623263392005800d6efcd9f05001 Mon Sep 17 00:00:00 2001 From: Arne Dierick Date: Tue, 21 May 2024 20:28:10 +0200 Subject: [PATCH 13/84] added more info to project page --- frontend/src/i18n/en/translation.json | 3 +- frontend/src/i18n/nl/translation.json | 3 +- .../index/components/ProjectTableCourse.tsx | 6 +- frontend/src/pages/project/Project.tsx | 184 ++++++++++-------- 4 files changed, 112 insertions(+), 84 deletions(-) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index c9fada70..62758fe7 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -42,13 +42,14 @@ "projects": { "noProjects": "No projects", "name": "Name", - "description": "Description", + "description": "Info", "course": "Course", "deadline": "Deadline", "deadlineNotPassed": "Only show active projects", "showMore": "Show more", "submit": "Submit", "projectStatus": "Status", + "maxScore": "Maximum score", "visibility": "Visibility", "visibleStatus": { "visible": "Visible", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 92e171fa..813fe8fb 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -42,12 +42,13 @@ "projects": { "noProjects": "Geen projecten", "name": "Naam", - "description": "Beschrijving", + "description": "Info", "course": "Vak", "deadline": "Deadline", "deadlineNotPassed": "Toon enkel actieve projecten", "showMore": "Toon meer", "projectStatus": "Status", + "maxScore": "Maximum score", "visibility": "Zichtbaarheid", "visibleStatus": { "visible": "Zichtbaar", diff --git a/frontend/src/pages/index/components/ProjectTableCourse.tsx b/frontend/src/pages/index/components/ProjectTableCourse.tsx index dbe49320..32f4b121 100644 --- a/frontend/src/pages/index/components/ProjectTableCourse.tsx +++ b/frontend/src/pages/index/components/ProjectTableCourse.tsx @@ -8,7 +8,7 @@ import ProjectStatusTag from "./ProjectStatusTag" import GroupProgress from "./GroupProgress" import { Link } from "react-router-dom" import { AppRoutes } from "../../../@types/routes" -import { ClockCircleOutlined } from "@ant-design/icons" +import {ClockCircleOutlined, EyeInvisibleOutlined, EyeOutlined} from "@ant-design/icons" import useIsCourseAdmin from "../../../hooks/useIsCourseAdmin"; export type ProjectType = GET_Responses[ApiRoutes.PROJECT] @@ -88,7 +88,7 @@ const ProjectTableCourse: FC<{ projects: ProjectType[] | null, ignoreColumns?: s key: "visible", render: (project: ProjectType) => { if (project.visible) { - return {t("home.projects.visibleStatus.visible")} + return }>{t("home.projects.visibleStatus.visible")} } else if (project.visibleAfter) { return ( ) } else { - return {t("home.projects.visibleStatus.invisible")} + return }>{t("home.projects.visibleStatus.invisible")} } } }) diff --git a/frontend/src/pages/project/Project.tsx b/frontend/src/pages/project/Project.tsx index 0f9bdbd9..179aa044 100644 --- a/frontend/src/pages/project/Project.tsx +++ b/frontend/src/pages/project/Project.tsx @@ -1,4 +1,4 @@ -import { Breadcrumb, Button, Card, Popconfirm, Tabs, TabsProps, Tooltip, theme } from "antd" +import {Button, Card, Popconfirm, Tabs, TabsProps, Tooltip, theme, Tag} from "antd" import { ApiRoutes, GET_Responses } from "../../@types/requests.d" import { useTranslation } from "react-i18next" import { Link, useLocation, useNavigate, useParams } from "react-router-dom" @@ -6,7 +6,16 @@ import SubmissionCard from "./components/SubmissionTab" import useCourse from "../../hooks/useCourse" import useProject from "../../hooks/useProject" import ScoreCard from "./components/ScoreTab" -import { DeleteOutlined, FileDoneOutlined, InfoCircleOutlined, PlusOutlined, SendOutlined, SettingFilled, TeamOutlined } from "@ant-design/icons" +import { + ClockCircleOutlined, + DeleteOutlined, EyeInvisibleOutlined, EyeOutlined, + FileDoneOutlined, + InfoCircleOutlined, + PlusOutlined, + SendOutlined, + SettingFilled, StarOutlined, + TeamOutlined +} from "@ant-design/icons" import { useMemo, useState } from "react" import useIsCourseAdmin from "../../hooks/useIsCourseAdmin" import GroupTab from "./components/GroupTab" @@ -14,6 +23,7 @@ import { AppRoutes } from "../../@types/routes" import SubmissionsTab from "./components/SubmissionsTab" import MarkdownTextfield from "../../components/input/MarkdownTextfield" import useApi from "../../hooks/useApi" +import i18n from "i18next"; // dracula, darcula,oneDark,vscDarkPlus | prism, base16AteliersulphurpoolLight, oneLight @@ -43,6 +53,14 @@ const Project = () => { children: project && (
    + }> {new Date(project.deadline).toLocaleString(i18n.language, { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + })} + }> {t("home.projects.maxScore")}: {project.maxScore}
    @@ -131,86 +149,94 @@ const Project = () => { navigate(AppRoutes.COURSE.replace(":courseId", course.courseId + "")) } - return ( -
    - - + return ( +
    + + + : project?.visibleAfter ? : } + color={project?.visible ? token.colorSuccess : project?.visibleAfter ? "default" : token.colorHighlight} /> + + + + + + + + + - - - - - - + + + ) + } + > + - - - ) : ( - deadline ? t("project.deadlinePassed") : ""}> - - - - - ) - } - > - - -
    - ) -} -export default Project +
    +
    + )} + export default Project; From 19058de8962060288a4dbbd26ef2d2851fde5269 Mon Sep 17 00:00:00 2001 From: Arne Dierick Date: Tue, 21 May 2024 21:18:49 +0200 Subject: [PATCH 14/84] small changes to project info and added homepage title logic based on role --- frontend/src/i18n/en/translation.json | 3 +- frontend/src/i18n/nl/translation.json | 3 +- .../pages/index/components/CourseSection.tsx | 129 ++++++++++-------- .../index/components/ProjectTableCourse.tsx | 2 +- frontend/src/pages/project/Project.tsx | 25 ++-- 5 files changed, 89 insertions(+), 73 deletions(-) diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 62758fe7..e63229f6 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -33,7 +33,8 @@ "docs": "Documentation", "calendar": "Calendar", "academicYearRequired": "Academic year is required", - "noCourses": "You're not in any courses yet, you can join a course by opening a course join link.", + "noCoursesStudent": "You're not in any courses yet, you can join a course by opening a course join link", + "noCoursesTeacher": "You're not in any courses yet you can create a course by clicking the button above", "courseNameRequired": "Course name is required", "courseNameMaxLength": "Course name must be less than 50 characters", "courseNameMinLength": "Course name must be at least 3 characters", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 813fe8fb..becb9d18 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -33,7 +33,8 @@ "table": "Tabel", "docs": "Documentatie", "calendar": "Kalender", - "noCourses": "Je bent nog niet ingeschreven in een vak. Je kan je inschrijven door een vak join link te openen.", + "noCoursesStudent": "Je bent nog niet ingeschreven in een vak. Je kan je inschrijven door een vak join link te openen", + "noCoursesTeacher": "Je bent nog niet verbonden aan een vak. Je kan zelf een vak maken door op de knop hierboven te klikken", "courseNameMaxLength": "Vak naam mag maximaal 50 karakters bevatten", "courseNameRequired": "Vak naam is verplicht", "courseNameMinLength": "Vak naam moet minimaal 3 karakters bevatten", diff --git a/frontend/src/pages/index/components/CourseSection.tsx b/frontend/src/pages/index/components/CourseSection.tsx index 1ff4155a..ea639bd7 100644 --- a/frontend/src/pages/index/components/CourseSection.tsx +++ b/frontend/src/pages/index/components/CourseSection.tsx @@ -19,6 +19,7 @@ export type CourseProjectList = CourseProjectsType[string][] | null const CourseSection: FC<{ projects: ProjectsType | null; onOpenNew: () => void }> = ({ projects, onOpenNew }) => { const { courses } = useUser() + const user = useUser().user const [courseProjects, setCourseProjects] = useState(null) const [adminCourseProjects, setAdminCourseProjects] = useState(null) const [archivedCourses, setArchivedCourses] = useState(false) @@ -67,7 +68,6 @@ const CourseSection: FC<{ projects: ProjectsType | null; onOpenNew: () => void } return () => (ignore = true) }, [courses, projects]) - const [filteredCourseProjects, filteredAdminCourseProjects, courseProjectsList, adminCourseProjectsList, yearOptions]: [CourseProjectList, CourseProjectList, CourseProjectList, CourseProjectList, number[] | null] = useMemo(() => { // Filter courses based on selected year if (courseProjects === null || adminCourseProjects === null) return [null, null, [], [], null] @@ -83,68 +83,79 @@ const CourseSection: FC<{ projects: ProjectsType | null; onOpenNew: () => void } }, [courseProjects, adminCourseProjects, selectedYear]) const YearDropdown = () => ( - <> - {yearOptions && yearOptions.length > 1 && ( -
    - -
    - )} - + <> + {yearOptions && yearOptions.length > 1 && ( +
    + +
    + )} + ) const showYourCourses = !!filteredCourseProjects?.length || !filteredAdminCourseProjects?.length return ( - <> - {/* Dropdown for selecting year */} - - {!!showYourCourses && 2} - showPlus={!filteredAdminCourseProjects?.length} - extra={YearDropdown} - allOptions={showYourCourses} - type="enrolled" - />} - - - { !!filteredAdminCourseProjects?.length && 2} - extra={YearDropdown} - showPlus={!!filteredAdminCourseProjects?.length} - allOptions={!!filteredAdminCourseProjects?.length && !filteredCourseProjects?.length} - type="admin" - />} - - - - {filteredCourseProjects !== null && courseProjectsList.length === 0 && adminCourseProjectsList.length === 0 && ( - - {t("home.noCourses")} - - )} - + <> + {/* Dropdown for selecting year */} + + {showYourCourses && ( + 2} + showPlus={!filteredAdminCourseProjects?.length} + extra={YearDropdown} + allOptions={showYourCourses} + type="enrolled" + /> + )} + + { !!filteredAdminCourseProjects?.length && 2} + extra={YearDropdown} + showPlus={!!filteredAdminCourseProjects?.length} + allOptions={!!filteredAdminCourseProjects?.length && !filteredCourseProjects?.length} + type="admin" + />} + + {/* No courses messages */} + {filteredCourseProjects !== null && courseProjectsList.length === 0 && adminCourseProjectsList.length === 0 && + user?.role === "student" && ( + + {t("home.noCoursesStudent")} + + )} + {filteredCourseProjects !== null && courseProjectsList.length === 0 && adminCourseProjectsList.length === 0 && + user?.role !== "student" && ( + + {t("home.noCoursesTeacher")} + + )} + ) } -export default CourseSection +export default CourseSection \ No newline at end of file diff --git a/frontend/src/pages/index/components/ProjectTableCourse.tsx b/frontend/src/pages/index/components/ProjectTableCourse.tsx index 32f4b121..2c77b203 100644 --- a/frontend/src/pages/index/components/ProjectTableCourse.tsx +++ b/frontend/src/pages/index/components/ProjectTableCourse.tsx @@ -98,7 +98,7 @@ const ProjectTableCourse: FC<{ projects: ProjectType[] | null, ignoreColumns?: s hour: "2-digit", minute: "2-digit", })}`}> - } color="default">{t("home.projects.visibleStatus.scheduled")} + } color="default">{t("home.projects.visibleStatus.scheduled")}
    ) } else { diff --git a/frontend/src/pages/project/Project.tsx b/frontend/src/pages/project/Project.tsx index 179aa044..1e9c0e1f 100644 --- a/frontend/src/pages/project/Project.tsx +++ b/frontend/src/pages/project/Project.tsx @@ -7,6 +7,7 @@ import useCourse from "../../hooks/useCourse" import useProject from "../../hooks/useProject" import ScoreCard from "./components/ScoreTab" import { + CalendarOutlined, ClockCircleOutlined, DeleteOutlined, EyeInvisibleOutlined, EyeOutlined, FileDoneOutlined, @@ -24,6 +25,7 @@ import SubmissionsTab from "./components/SubmissionsTab" import MarkdownTextfield from "../../components/input/MarkdownTextfield" import useApi from "../../hooks/useApi" import i18n from "i18next"; +import useUser from "../../hooks/useUser"; // dracula, darcula,oneDark,vscDarkPlus | prism, base16AteliersulphurpoolLight, oneLight @@ -61,6 +63,18 @@ const Project = () => { minute: "2-digit", })} }> {t("home.projects.maxScore")}: {project.maxScore} + {courseAdmin && ( + + : } + color="default"/> + + )}
    @@ -170,17 +184,6 @@ const Project = () => { extra={ courseAdmin ? ( <> - - : project?.visibleAfter ? : } - color={project?.visible ? token.colorSuccess : project?.visibleAfter ? "default" : token.colorHighlight} /> - - + ), }, @@ -31,7 +30,7 @@ const SubmissionList: FC<{ submissions: GroupSubmissionType[] | null }> = ({ sub title: t("project.submissionTime"), dataIndex: "submissionTime", key: "submissionTime", - + sorter: (a: GroupSubmissionType, b: GroupSubmissionType) => new Date(a.submissionTime).getTime() - new Date(b.submissionTime).getTime(), render: (submission: GroupSubmissionType["submissionTime"]) => ( {new Date(submission).toLocaleString()} ), diff --git a/frontend/src/pages/project/components/SubmissionTab.tsx b/frontend/src/pages/project/components/SubmissionTab.tsx index 37045570..3ef49e3b 100644 --- a/frontend/src/pages/project/components/SubmissionTab.tsx +++ b/frontend/src/pages/project/components/SubmissionTab.tsx @@ -8,6 +8,7 @@ export type GroupSubmissionType = GET_Responses[ApiRoutes.PROJECT_GROUP_SUBMISSI const SubmissionTab: FC<{ projectId: number; courseId: number; testSubmissions?: boolean }> = ({ projectId, courseId, testSubmissions }) => { const [submissions, setSubmissions] = useState(null) + const [indices, setIndices] = useState>(new Map()) const project = useProject() const API = useApi() @@ -18,7 +19,16 @@ const SubmissionTab: FC<{ projectId: number; courseId: number; testSubmissions?: let ignore = false API.GET(testSubmissions ? ApiRoutes.PROJECT_TEST_SUBMISSIONS : ApiRoutes.PROJECT_GROUP_SUBMISSIONS, { pathValues: { projectId: project.projectId, groupId: project.groupId ?? "" } }).then((res) => { if (!res.success || ignore) return - setSubmissions(res.response.data.sort((a, b) => b.submissionId - a.submissionId)) + console.log(res.response.data) + //this is sorts the submissions by submission time, with the oldest submission first + const ascending = res.response.data.sort((a, b) => new Date(a.submissionTime).getTime() - new Date(b.submissionTime).getTime()) + const tmp = new Map() + ascending.forEach((submission, index) => { + tmp.set(submission.submissionId, index+1) + }) + setIndices(tmp) + //we need descending order, so we reverse the array + setSubmissions(ascending.reverse()) }) return () => { @@ -30,7 +40,7 @@ const SubmissionTab: FC<{ projectId: number; courseId: number; testSubmissions?: <> - + ) } From 3d43dd1b3fc50a3d1fcc178680a93d0746381273 Mon Sep 17 00:00:00 2001 From: Tristan Verbeken Date: Wed, 22 May 2024 00:37:35 +0200 Subject: [PATCH 16/84] Updated tooltips and made changes to docker tests page so it is as clear as possible. --- .../forms/projectFormTabs/DockerFormTab.tsx | 81 ++++++++++++------- .../projectFormTabs/StructureFormTab.tsx | 12 ++- frontend/src/i18n/en/translation.json | 11 ++- frontend/src/i18n/nl/translation.json | 11 ++- 4 files changed, 80 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx index 370ee2b3..f1e45d85 100644 --- a/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/DockerFormTab.tsx @@ -1,8 +1,8 @@ import { InboxOutlined, UploadOutlined } from "@ant-design/icons" -import { Button, Form, Input, Upload } from "antd" +import {Button, Form, Input, Switch, Upload} from "antd" import { TextAreaProps } from "antd/es/input" import { FormInstance } from "antd/lib" -import { FC } from "react" +import {FC, useState} from "react" import { useTranslation } from "react-i18next" import { ApiRoutes } from "../../../@types/requests" import useAppApi from "../../../hooks/useAppApi" @@ -79,6 +79,7 @@ function isValidTemplate(template: string): string { const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { const { t } = useTranslation() const {message} = useAppApi() + const [withArtifacts, setWithArtifacts] = useState(true) const dockerImage = Form.useWatch("dockerImage", form) const dockerDisabled = !dockerImage?.length @@ -97,8 +98,7 @@ const DockerFormTab: FC<{ form: FormInstance }> = ({ form }) => { + tooltip={t("project.tests.dockerImageTooltip")} > = ({ form }) => { rules={[{ required: !dockerDisabled, message: "Docker script is required" }]} label="Docker start script" name="dockerScript" - tooltip="TODO write docs for this" + tooltip={t("project.tests.dockerScriptTooltip")} > = ({ form }) => { disabled={dockerDisabled} fieldName="dockerScript" /> - - { - const errorMessage = isValidTemplate(value) - return errorMessage === "" ? Promise.resolve() : Promise.reject(new Error(errorMessage)) - }, - }, - ]} - > - + - - + + + {withArtifacts ? +
    + {t("project.tests.templateModeInfo")} +
    +
    +
    + + { + const errorMessage = isValidTemplate(value) + return errorMessage === "" ? Promise.resolve() : Promise.reject(new Error(errorMessage)) + }, + }, + ]} + > + + required\n>description=\"This is a test\"\nExpected output 1\n@helloUGent\n>optional\nExpected output 2\n"} + /> + +
    : {t("project.tests.simpleModeInfo")}} + rules={[{ required: true}]} + />} diff --git a/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx b/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx index 6cfe2af4..747cb70d 100644 --- a/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx +++ b/frontend/src/components/forms/projectFormTabs/StructureFormTab.tsx @@ -15,11 +15,17 @@ const StructureFormTab: FC<{ form: FormInstance }> = ({ form }) => { + tooltip={t("project.tests.fileStructureTooltip")}> { if (e.key === "Tab") { e.preventDefault() diff --git a/frontend/src/i18n/en/translation.json b/frontend/src/i18n/en/translation.json index 8e6a104e..727a5149 100644 --- a/frontend/src/i18n/en/translation.json +++ b/frontend/src/i18n/en/translation.json @@ -202,7 +202,16 @@ "dockerScriptHeader": "Docker script", "modeHeader": "Template", "fileStructure": "File structure", - "fileStructurePreview": "File structure preview" + "fileStructurePreview": "File structure preview", + "simpleMode": "Simple mode", + "templateMode": "Template mode", + "fileStructureTooltip": "Describe the project structure with a simple template, which is indentation-sensitive. Each filename is in regex, where the dot is converted to a (non-)escaped dot. Under each directory, describe the files in that directory by listing them on the following lines with the same indentation. To specify that it is a directory, end the filename with a '/'. You can also blacklist files by prefixing the filename with a '-'.", + "dockerImageTooltip": "Enter the Docker image that will be used to run the container. This must be a valid image available on Docker Hub.", + "dockerScriptTooltip": "The Docker script is the script that will be executed when starting a container with the above image. This script is always executed in bash on the container. You can also upload a script. This script has access to the files in /shared/input, where the student's files are located.", + "dockerTemplateTooltip": "To specify specific tests, you need to provide a template. First, enter the test name with '@{test}'. Below this, you can use '>' to provide options such as ('>required', '>optional', '>description'). Everything under these options until the next test or the end of the file is the expected output.", + "dockerTestDirTooltip": "Here you can upload additional test utility files that will be available in the container. They will be located in the /shared/extra folder.", + "simpleModeInfo": "In simple mode, the container will execute the Docker script. Everything logged to the console will be visible to the student as feedback. This allows you to provide feedback to the student using print statements. To specify whether the submission was successful or not, you must write 'Push allowed/denied' to the file /shared/output/dockerOutput.", + "templateModeInfo": "In template mode, the student receives feedback in a more structured format. You can specify multiple small subtests, and the student will see the differences between their output and the expected output in these tests." }, "noScore": "No score available", "noFeedback": "No feedback provided", diff --git a/frontend/src/i18n/nl/translation.json b/frontend/src/i18n/nl/translation.json index 81b4afa0..db720766 100644 --- a/frontend/src/i18n/nl/translation.json +++ b/frontend/src/i18n/nl/translation.json @@ -204,7 +204,16 @@ "dockerScriptHeader": "Docker script", "modeHeader": "Sjabloon", "fileStructure": "Bestandsstructuur", - "fileStructurePreview": "Voorbeeld van bestandsstructuur" + "simpleMode": "Simpele modus", + "templateMode": "Template modus", + "fileStructurePreview": "Voorbeeld van bestandsstructuur", + "fileStructureTooltip": "Beschrijf de projectstructuur met een eenvoudig sjabloon, dat gevoelig is voor inspringing. Elke bestandsnaam is in regex, waarbij de punt wordt omgezet naar een (niet)geëscapete punt. Onder elke map beschrijf je de bestanden in deze map, door ze op de volgende regels te zetten met dezelfde inspringing. Om aan te geven dat het een map is, eindig je de bestandsnaam met een '/'. Je kunt ook bestanden uitsluiten door een '-' voor de bestandsnaam te zetten.", + "dockerImageTooltip": "Voer de Docker image in die zal worden gebruikt om de container uit te voeren. Dit moet een geldige image zijn die beschikbaar is op Docker Hub.", + "dockerScriptTooltip": "Het Docker script is het script dat wordt uitgevoerd bij het starten van een container met bovenstaande image. Dit script wordt altijd uitgevoerd in bash op de container. Je kunt ook een script uploaden. Dit script heeft toegang tot de bestanden in /shared/input, waar de student zijn bestanden kan plaatsen.", + "dockerTemplateTooltip": "Om specifieke tests te definiëren, moet je een sjabloon invoeren. Geef eerst de naam van de test in met '@{test}'. Hieronder kun je met een '>' opties geven zoals ('>required', '>optional', '>description'). Alles onder de opties tot de volgende test of het einde van het bestand is de verwachte output.", + "dockerTestDirTooltip": "Hier kun je extra test utility bestanden uploaden die beschikbaar zullen zijn in de container. Ze zullen worden geplaatst in de map /shared/extra.", + "simpleModeInfo": "In de eenvoudige modus zal de container het Docker script uitvoeren. Alles wat naar de console wordt gelogd, is zichtbaar voor de student als feedback. Zo kun je zelf met printstatements feedback geven aan de student. Om aan te geven of de indiening is geslaagd, moet je 'Push allowed/denied' schrijven naar het bestand /shared/output/dockerOutput.", + "templateModeInfo": "In de sjabloonmodus krijgt de student gestructureerde feedback te zien. Je kunt meerdere kleine subtests opgeven, en de student ziet wat het verschil is met het verwachtte resultaat in deze tests." }, "noScore": "Nog geen score beschikbaar", "noFeedback": "Geen feedback gegeven", From 063ff744202a9e607486388bb95bd1f1561fd822 Mon Sep 17 00:00:00 2001 From: Floris Kornelis van Dijken Date: Wed, 22 May 2024 02:45:18 +0200 Subject: [PATCH 17/84] done --- frontend/src/pages/index/components/CourseCard.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/index/components/CourseCard.tsx b/frontend/src/pages/index/components/CourseCard.tsx index 3c509b16..86b1e6e1 100644 --- a/frontend/src/pages/index/components/CourseCard.tsx +++ b/frontend/src/pages/index/components/CourseCard.tsx @@ -7,6 +7,7 @@ import { useNavigate } from "react-router-dom" import { AppRoutes } from "../../../@types/routes" import GroupProgress from "./GroupProgress" import { CourseProjectsType } from "./CourseSection" +import { Link } from "react-router-dom" const CourseCard: FC<{ courseProjects: CourseProjectsType[string], adminView?:boolean }> = ({ courseProjects,adminView }) => { const { t } = useTranslation() @@ -74,10 +75,16 @@ const CourseCard: FC<{ courseProjects: CourseProjectsType[string], adminView?:bo />, ]} > - {project.name}} /> + event.stopPropagation()}> + {project.name} + }/> )} - > + > + {courseProjects.projects.length > 0 && ...} + ) } From cea07e94dcf7ea2f992c4ab63545c18ba63366bb Mon Sep 17 00:00:00 2001 From: Floris Kornelis van Dijken Date: Wed, 22 May 2024 02:46:15 +0200 Subject: [PATCH 18/84] debug dingen veranderd --- frontend/src/pages/index/components/CourseCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/index/components/CourseCard.tsx b/frontend/src/pages/index/components/CourseCard.tsx index 86b1e6e1..7cbda045 100644 --- a/frontend/src/pages/index/components/CourseCard.tsx +++ b/frontend/src/pages/index/components/CourseCard.tsx @@ -83,7 +83,7 @@ const CourseCard: FC<{ courseProjects: CourseProjectsType[string], adminView?:bo )} > - {courseProjects.projects.length > 0 && ...} + {courseProjects.projects.length > 3 && ...} ) From 7bd211c4890f4cfca6fcf39de995e8e7fce89f5c Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Wed, 22 May 2024 09:01:03 +0200 Subject: [PATCH 19/84] Fix for tests --- .gitignore | 1 + .../pidgeon/controllers/GroupFeedbackControllerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9a182e36..87feac74 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ docker.env startBackend.sh /.env +backend/web-bff/App/.env diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java index df28a51d..dce45290 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java @@ -281,7 +281,7 @@ public void testAddGroupScore() throws Exception { when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), HttpMethod.POST)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( - json -> json.getScore() == groupFeedbackEntity.getScore() && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) + json -> Objects.equals(json.getScore(), groupFeedbackEntity.getScore()) && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupFeedbackRepository.save(any())).thenReturn(groupFeedbackEntity); when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); @@ -292,7 +292,7 @@ public void testAddGroupScore() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); verify(groupFeedbackRepository, times(1)).save(argThat( - groupFeedback -> groupFeedback.getScore() == groupFeedbackEntity.getScore() && + groupFeedback -> Objects.equals(groupFeedback.getScore(), groupFeedbackEntity.getScore()) && groupFeedback.getFeedback().equals(groupFeedbackEntity.getFeedback()) && groupFeedback.getGroupId() == groupFeedbackEntity.getGroupId() && groupFeedback.getProjectId() == groupFeedbackEntity.getProjectId())); From 1ae569ad25f974157910ea7031289e8816d680b8 Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Wed, 22 May 2024 09:29:50 +0200 Subject: [PATCH 20/84] Fix for sturcturefeedback + admin submit after deadline --- .gitignore | 1 + .../SubmissionTemplateModel.java | 12 ++++++------ .../com/ugent/pidgeon/util/SubmissionUtil.java | 2 +- .../GroupFeedbackControllerTest.java | 4 ++-- .../ugent/pidgeon/util/SubmissionUtilTest.java | 6 ++++++ .../DockerSubmissionTestTest/d__test.zip | Bin 162 -> 162 bytes 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 9a182e36..87feac74 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ docker.env startBackend.sh /.env +backend/web-bff/App/.env diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java index 13bdd40b..7f7bd347 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/SubmissionTemplateModel.java @@ -151,16 +151,16 @@ public SubmissionResult checkSubmission(ZipFile file) throws IOException { boolean passed = (filesMissing.size() + filesUnrequested.size() + filesDenied.size()) == 0; String feedback = passed ? "File structure is correct" : "File structure failed to pass the template, because: \n "; if (!filesMissing.isEmpty()) { - feedback += " -The following files are required from the template and are not found in the project: \n -"; - feedback += String.join("\n -", filesMissing); + feedback += "- The following files are required from the template and are not found in the project: \n - "; + feedback += String.join("\n - ", filesMissing); } if (!filesUnrequested.isEmpty()) { - feedback += "\n -The following files are not requested in the template: \n -"; - feedback += String.join("\n -", filesUnrequested); + feedback += "\n - The following files are not requested in the template: \n - "; + feedback += String.join("\n - ", filesUnrequested); } if (!filesDenied.isEmpty()) { - feedback += "\n -The following files are not allowed in the project: \n -"; - feedback += String.join("\n -", filesDenied); + feedback += "\n - The following files are not allowed in the project: \n - "; + feedback += String.join("\n - ", filesDenied); } return new SubmissionResult(passed, feedback); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java index 866eed19..b7d996ee 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/SubmissionUtil.java @@ -105,7 +105,7 @@ public CheckResult checkOnSubmit(long projectId, UserEntity user) { OffsetDateTime time = OffsetDateTime.now(); Logger.getGlobal().info("Time: " + time + " Deadline: " + project.getDeadline()); - if (time.isAfter(project.getDeadline())) { + if (time.isAfter(project.getDeadline()) && groupId != null) { return new CheckResult<>(HttpStatus.FORBIDDEN, "Project deadline has passed", null); } return new CheckResult<>(HttpStatus.OK, "", groupId); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java index df28a51d..dce45290 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java @@ -281,7 +281,7 @@ public void testAddGroupScore() throws Exception { when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), HttpMethod.POST)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( - json -> json.getScore() == groupFeedbackEntity.getScore() && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) + json -> Objects.equals(json.getScore(), groupFeedbackEntity.getScore()) && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupFeedbackRepository.save(any())).thenReturn(groupFeedbackEntity); when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); @@ -292,7 +292,7 @@ public void testAddGroupScore() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); verify(groupFeedbackRepository, times(1)).save(argThat( - groupFeedback -> groupFeedback.getScore() == groupFeedbackEntity.getScore() && + groupFeedback -> Objects.equals(groupFeedback.getScore(), groupFeedbackEntity.getScore()) && groupFeedback.getFeedback().equals(groupFeedbackEntity.getFeedback()) && groupFeedback.getGroupId() == groupFeedbackEntity.getGroupId() && groupFeedback.getProjectId() == groupFeedbackEntity.getProjectId())); diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java index a30b71a6..1ca4fe30 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/SubmissionUtilTest.java @@ -157,6 +157,11 @@ public void testCheckOnSubmit() { result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); assertEquals(HttpStatus.OK, result.getStatus()); assertNull(result.getData()); + + /* Deadline passed when user is admin, should still be allowed */ + projectEntity.setDeadline(OffsetDateTime.now().minusDays(1)); + result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); + assertEquals(HttpStatus.OK, result.getStatus()); /* User not part of group and not admin */ when(projectUtil.isProjectAdmin(projectEntity.getId(), userEntity)) @@ -171,6 +176,7 @@ public void testCheckOnSubmit() { result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); assertEquals(HttpStatus.FORBIDDEN, result.getStatus()); + /* GroupCluster in archived course */ when(groupClusterRepository.inArchivedCourse(groupEntity.getClusterId())).thenReturn(true); result = submissionUtil.checkOnSubmit(projectEntity.getId(), userEntity); diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip index 00b2b6bfdef119624eab2ce0a67bdbe128332895..b95a278278710c6170339ed58ff8cb9f4adfd3ed 100644 GIT binary patch delta 28 hcmZ3)xQLNAz?+#xgn@&DgW;>!wu!v{%pfY>831Ag2rmEt delta 28 hcmZ3)xQLNAz?+#xgn@&DgJDkd=83%i%pfY>8311e2k8I+ From f4adc8d9f23e55f35134b2e5c277432fbb807c5d Mon Sep 17 00:00:00 2001 From: Aqua-sc <108478185+Aqua-sc@users.noreply.github.com> Date: Wed, 22 May 2024 10:01:12 +0200 Subject: [PATCH 21/84] Fix test --- .gitignore | 1 + .../pidgeon/controllers/GroupFeedbackControllerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9a182e36..87feac74 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ docker.env startBackend.sh /.env +backend/web-bff/App/.env diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java index df28a51d..dce45290 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/GroupFeedbackControllerTest.java @@ -281,7 +281,7 @@ public void testAddGroupScore() throws Exception { when(groupFeedbackUtil.checkGroupFeedbackUpdate(groupFeedbackEntity.getGroupId(), groupFeedbackEntity.getProjectId(), getMockUser(), HttpMethod.POST)) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupFeedbackUtil.checkGroupFeedbackUpdateJson(argThat( - json -> json.getScore() == groupFeedbackEntity.getScore() && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) + json -> Objects.equals(json.getScore(), groupFeedbackEntity.getScore()) && json.getFeedback().equals(groupFeedbackEntity.getFeedback())), eq(groupFeedbackEntity.getProjectId()))) .thenReturn(new CheckResult<>(HttpStatus.OK, "", null)); when(groupFeedbackRepository.save(any())).thenReturn(groupFeedbackEntity); when(entityToJsonConverter.groupFeedbackEntityToJson(groupFeedbackEntity)).thenReturn(groupFeedbackJson); @@ -292,7 +292,7 @@ public void testAddGroupScore() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(groupFeedbackJson))); verify(groupFeedbackRepository, times(1)).save(argThat( - groupFeedback -> groupFeedback.getScore() == groupFeedbackEntity.getScore() && + groupFeedback -> Objects.equals(groupFeedback.getScore(), groupFeedbackEntity.getScore()) && groupFeedback.getFeedback().equals(groupFeedbackEntity.getFeedback()) && groupFeedback.getGroupId() == groupFeedbackEntity.getGroupId() && groupFeedback.getProjectId() == groupFeedbackEntity.getProjectId())); From 9f12dde2c553881e4968e1dd8370505a1ce66064 Mon Sep 17 00:00:00 2001 From: Arne Dierick Date: Wed, 22 May 2024 10:44:34 +0200 Subject: [PATCH 22/84] fixed multiline feedback input and rendering --- .../components/gradesTab/GradesList.tsx | 1 + .../src/pages/project/components/ScoreTab.tsx | 2 +- .../project/components/SubmissionsTable.tsx | 341 ++++++++++-------- 3 files changed, 190 insertions(+), 154 deletions(-) diff --git a/frontend/src/pages/course/components/gradesTab/GradesList.tsx b/frontend/src/pages/course/components/gradesTab/GradesList.tsx index 93b1a95e..c691d96d 100644 --- a/frontend/src/pages/course/components/gradesTab/GradesList.tsx +++ b/frontend/src/pages/course/components/gradesTab/GradesList.tsx @@ -47,6 +47,7 @@ const GradesList: FC<{ feedback: CourseGradesType[]; courseId: number }> = ({ fe } description={score.groupFeedback!.feedback} + style={{whiteSpace: "pre-wrap"}} /> )} diff --git a/frontend/src/pages/project/components/ScoreTab.tsx b/frontend/src/pages/project/components/ScoreTab.tsx index 593f4b9c..7a51eea0 100644 --- a/frontend/src/pages/project/components/ScoreTab.tsx +++ b/frontend/src/pages/project/components/ScoreTab.tsx @@ -61,7 +61,7 @@ const ScoreCard = () => { ), ]} > - {score.feedback?.length ? {score.feedback} : ({t("project.noFeedback")})} + {score.feedback?.length ? {score.feedback} : ({t("project.noFeedback")})} ) } diff --git a/frontend/src/pages/project/components/SubmissionsTable.tsx b/frontend/src/pages/project/components/SubmissionsTable.tsx index fb78f5f0..fec9f862 100644 --- a/frontend/src/pages/project/components/SubmissionsTable.tsx +++ b/frontend/src/pages/project/components/SubmissionsTable.tsx @@ -1,82 +1,95 @@ -import { Button, Input, List, Table, Tooltip, Typography } from "antd" -import { FC, useMemo } from "react" -import { ProjectSubmissionsType } from "./SubmissionsTab" -import { TableProps } from "antd/lib" -import { useTranslation } from "react-i18next" -import { DownloadOutlined } from "@ant-design/icons" -import useProject from "../../../hooks/useProject" -import SubmissionStatusTag, { createStatusBitVector } from "./SubmissionStatusTag" -import { Link, useParams } from "react-router-dom" -import { AppRoutes } from "../../../@types/routes" -import { ApiRoutes, PUT_Requests } from "../../../@types/requests.d" -import useAppApi from "../../../hooks/useAppApi" -import useApi from "../../../hooks/useApi" - -const GroupMember = ({ name,studentNumber }: ProjectSubmissionsType["group"]["members"][number]) => { - return - - - -} - -const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onChange: (s: ProjectSubmissionsType[]) => void, withArtifacts?:boolean }> = ({ submissions, onChange,withArtifacts }) => { - const { t } = useTranslation() - const project = useProject() - const { courseId, projectId } = useParams() - const { message } = useAppApi() - const API = useApi() +import { Button, Input, List, Table, Tooltip, Typography } from "antd"; +import { FC, useMemo, useState } from "react"; +import { ProjectSubmissionsType } from "./SubmissionsTab"; +import { TableProps } from "antd/lib"; +import { useTranslation } from "react-i18next"; +import {DownloadOutlined, SaveOutlined} from "@ant-design/icons"; +import useProject from "../../../hooks/useProject"; +import SubmissionStatusTag, { createStatusBitVector } from "./SubmissionStatusTag"; +import { Link, useParams } from "react-router-dom"; +import { AppRoutes } from "../../../@types/routes"; +import { ApiRoutes, PUT_Requests } from "../../../@types/requests.d"; +import useAppApi from "../../../hooks/useAppApi"; +import useApi from "../../../hooks/useApi"; + +const GroupMember = ({ name, studentNumber }: ProjectSubmissionsType["group"]["members"][number]) => { + return ( + + + + ); +}; + +const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onChange: (s: ProjectSubmissionsType[]) => void; withArtifacts?: boolean }> = ({ submissions, onChange, withArtifacts }) => { + const { t } = useTranslation(); + const project = useProject(); + const { courseId, projectId } = useParams(); + const { message } = useAppApi(); + const API = useApi(); + const [editingFeedback, setEditingFeedback] = useState<{ [key: number]: string }>({}); + const [isEditing, setIsEditing] = useState<{ [key: number]: boolean }>({}); + const updateTable = async (groupId: number, feedback: Omit, usePost: boolean) => { - if (!projectId || submissions === null || !groupId) return console.error("No projectId or submissions or groupId found") + if (!projectId || submissions === null || !groupId) return console.error("No projectId or submissions or groupId found"); - let res + let res; if (usePost) { res = await API.POST( - ApiRoutes.PROJECT_SCORE, - { - body: feedback, - pathValues: { id: projectId, groupId }, - }, - "message" - ) + ApiRoutes.PROJECT_SCORE, + { + body: feedback, + pathValues: { id: projectId, groupId }, + }, + "message" + ); } else { - res = await API.PUT(ApiRoutes.PROJECT_SCORE, { body: feedback, pathValues: { id: projectId, groupId } }, "message") + res = await API.PUT(ApiRoutes.PROJECT_SCORE, { body: feedback, pathValues: { id: projectId, groupId } }, "message"); } - if (!res.success) return + if (!res.success) return; - const data = res.response.data + const data = res.response.data; const newSubmissions: ProjectSubmissionsType[] = submissions.map((s) => { - if (s.group.groupId !== groupId) return s + if (s.group.groupId !== groupId) return s; return { ...s, feedback: { ...s.feedback, ...data, }, - } - }) + }; + }); - onChange(newSubmissions) - } + onChange(newSubmissions); + }; const updateScore = async (s: ProjectSubmissionsType, scoreStr: string) => { - if (!projectId || !project) return console.error("No projectId or project found") - if (!project.maxScore) return console.error("Scoring not available for this project") - scoreStr = scoreStr.trim() - let score: number | null - if (scoreStr === "") score = null - else score = parseFloat(scoreStr) - if (isNaN(score as number)) score = null - if (score !== null && score > project.maxScore) return message.error(t("project.scoreTooHigh")) - await updateTable(s.group.groupId, { score:score||null, feedback:s.feedback?.feedback??"" }, s.feedback === null) - } - - const updateFeedback = async (s: ProjectSubmissionsType, feedback: string) => { - await updateTable(s.group.groupId, { feedback, score: s.feedback?.score||null }, s.feedback === null) - } + if (!projectId || !project) return console.error("No projectId or project found"); + if (!project.maxScore) return console.error("Scoring not available for this project"); + scoreStr = scoreStr.trim(); + let score: number | null; + if (scoreStr === "") score = null; + else score = parseFloat(scoreStr); + if (isNaN(score as number)) score = null; + if (score !== null && score > project.maxScore) return message.error(t("project.scoreTooHigh")); + await updateTable(s.group.groupId, { score: score || null, feedback: s.feedback?.feedback ?? "" }, s.feedback === null); + }; + + const updateFeedback = async (groupId: number) => { + const feedback = editingFeedback[groupId]; + if (feedback !== undefined) { + await updateTable(groupId, { feedback, score: submissions?.find(s => s.group.groupId === groupId)?.feedback?.score || null }, false); + setEditingFeedback((prev) => { + const newState = { ...prev }; + delete newState[groupId]; + return newState; + }); + setIsEditing((prev) => ({ ...prev, [groupId]: false })); + } + }; const downloadFile = async (route: ApiRoutes.SUBMISSION_FILE | ApiRoutes.SUBMISSION_ARTIFACT, filename: string) => { - const response = await API.GET( + const response = await API.GET( route, { config: { @@ -85,27 +98,29 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha }, }, "message" - ) - if (!response.success) return - const url = window.URL.createObjectURL(new Blob([response.response.data])) - const link = document.createElement("a") - link.href = url - let fileName = filename+".zip" // default filename - link.setAttribute("download", fileName) - document.body.appendChild(link) - link.click() - link.parentNode!.removeChild(link) - - } - + ); + if (!response.success) return; + const url = window.URL.createObjectURL(new Blob([response.response.data])); + const link = document.createElement("a"); + link.href = url; + let fileName = filename + ".zip"; // default filename + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + link.parentNode!.removeChild(link); + }; const downloadSubmission = async (submission: ProjectSubmissionsType) => { - if (!submission.submission) return console.error("No submission found") - downloadFile(submission.submission.fileUrl, submission.group.name+".zip") - if(withArtifacts && submission.submission.artifactUrl) { - downloadFile(submission.submission.artifactUrl, submission.group.name+"-artifacts.zip") + if (!submission.submission) return console.error("No submission found"); + downloadFile(submission.submission.fileUrl, submission.group.name + ".zip"); + if (withArtifacts && submission.submission.artifactUrl) { + downloadFile(submission.submission.artifactUrl, submission.group.name + "-artifacts.zip"); } - } + }; + + const handleEditFeedback = (groupId: number) => { + setIsEditing((prev) => ({ ...prev, [groupId]: true })); + }; const columns: TableProps["columns"] = useMemo(() => { const cols: TableProps["columns"] = [ @@ -115,30 +130,31 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha key: "group", render: (g) => {g.name}, sorter: (a: ProjectSubmissionsType, b: ProjectSubmissionsType) => { - return a.group.groupId - b.group.groupId + return a.group.groupId - b.group.groupId; }, }, { title: t("project.submission"), key: "submissionId", - render: (s: ProjectSubmissionsType) => s.submission ? ( - - - - ) : null, + render: (s: ProjectSubmissionsType) => + s.submission ? ( + + + + ) : null, }, { title: t("project.status"), dataIndex: "submission", key: "submissionStatus", render: (s) => ( - - {" "} - + + {" "} + ), }, { @@ -147,83 +163,102 @@ const SubmissionsTable: FC<{ submissions: ProjectSubmissionsType[] | null; onCha key: "submission", render: (time: ProjectSubmissionsType["submission"]) => time?.submissionTime && {new Date(time.submissionTime).toLocaleString()}, }, - ] + ]; if (!project || project.maxScore) { cols.push({ title: `Score (/${project?.maxScore ?? ""})`, key: "score", render: (s: ProjectSubmissionsType) => ( - updateScore(s, e), maxLength: 10 }} - > - {s.feedback?.score ?? t("project.noScoreLabel")} - + updateScore(s, e), maxLength: 10 }} + > + {s.feedback?.score ?? t("project.noScoreLabel")} + ), - }) + }); } cols.push({ title: "Download", key: "download", render: (s: ProjectSubmissionsType) => ( - -