From 5b684e759622012de3b1af71c87506c8c21160b7 Mon Sep 17 00:00:00 2001 From: warre Date: Fri, 29 Mar 2024 09:48:43 +0100 Subject: [PATCH 01/29] start homepage --- frontend/src/App.tsx | 4 ++-- frontend/src/pages/home/Home_student.tsx | 28 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/home/Home_student.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 22932a0e..e4aee95b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; -import Home from "./pages/home/Home"; +import HomeStudent from "./pages/home/Home_student.tsx"; import { Header } from "./components/Header/Header"; /** @@ -11,7 +11,7 @@ function App(): JSX.Element {
- } /> + } /> ); diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx new file mode 100644 index 00000000..1d0b94b3 --- /dev/null +++ b/frontend/src/pages/home/Home_student.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from "react-i18next"; +import { Card, CardContent, Typography, Grid , Container} from '@mui/material'; +import {useEffect } from 'react'; + +/** + * This component is the home page component that will be rendered when on the index route. + * @returns - The home page component + */ +export default function HomeStudent() { + const { t } = useTranslation(); + useEffect(() => { + fetch("http://127.0.0.1:5000/project?uid=123") + .then(response => response.json()) + }, []); + return ( + + + + + + {t('myProjects')} + + + + + + ); +} From 326a82a2e0f82d9a04e6bcc62304d4f831de089f Mon Sep 17 00:00:00 2001 From: warre Date: Tue, 2 Apr 2024 22:38:58 +0200 Subject: [PATCH 02/29] added calender functions and titlecard --- frontend/package-lock.json | 189 ++++++++++++++---- frontend/package.json | 3 + frontend/public/locales/en/translation.json | 7 +- frontend/public/locales/nl/translation.json | 7 +- frontend/src/pages/home/Home_student.tsx | 201 ++++++++++++++++++-- 5 files changed, 352 insertions(+), 55 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8467e285..c6155381 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,8 +11,11 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.10", + "@mui/lab": "^5.0.0-alpha.169", "@mui/material": "^5.15.10", "@mui/styled-engine-sc": "^6.0.0-alpha.16", + "@mui/x-date-pickers": "^7.1.0", + "dayjs": "^1.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.1", @@ -352,9 +355,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1266,14 +1269,14 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.36.tgz", - "integrity": "sha512-6A8fYiXgjqTO6pgj31Hc8wm1M3rFYCxDRh09dBVk0L0W4cb2lnurRJa3cAyic6hHY+we1S58OdGYRbKmOsDpGQ==", + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", "dependencies": { "@babel/runtime": "^7.23.9", "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@popperjs/core": "^2.11.8", "clsx": "^2.1.0", "prop-types": "^15.8.1" @@ -1297,9 +1300,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.10", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.10.tgz", - "integrity": "sha512-qPv7B+LeMatYuzRjB3hlZUHqinHx/fX4YFBiaS19oC02A1e9JFuDKDvlyRQQ5oRSbJJt0QlaLTlr0IcauVcJRQ==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz", + "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -1330,17 +1333,57 @@ } } }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.169", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.169.tgz", + "integrity": "sha512-h6xe1K6ISKUbyxTDgdvql4qoDP6+q8ad5fg9nXQxGLUrIeT2jVrBuT/jRECSTufbnhzP+V5kulvYxaMfM8rEdA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.15.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { - "version": "5.15.10", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.10.tgz", - "integrity": "sha512-YJJGHjwDOucecjDEV5l9ISTCo+l9YeWrho623UajzoHRYxuKUmwrGVYOW4PKwGvCx9SU9oklZnbbi2Clc5XZHw==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", + "integrity": "sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.36", - "@mui/core-downloads-tracker": "^5.15.10", - "@mui/system": "^5.15.9", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.14", + "@mui/system": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", @@ -1375,12 +1418,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.9.tgz", - "integrity": "sha512-/aMJlDOxOTAXyp4F2rIukW1O0anodAMCkv1DfBh/z9vaKHY3bd5fFf42wmP+0GRmwMinC5aWPpNfHXOED1fEtg==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", + "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.9", + "@mui/utils": "^5.15.14", "prop-types": "^15.8.1" }, "engines": { @@ -1401,9 +1444,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.9.tgz", - "integrity": "sha512-NRKtYkL5PZDH7dEmaLEIiipd3mxNnQSO+Yo8rFNBNptY8wzQnQ+VjayTq39qH7Sast5cwHKYFusUrQyD+SS4Og==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -1453,15 +1496,15 @@ } }, "node_modules/@mui/system": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.9.tgz", - "integrity": "sha512-SxkaaZ8jsnIJ77bBXttfG//LUf6nTfOcaOuIgItqfHv60ZCQy/Hu7moaob35kBb+guxVJnoSZ+7vQJrA/E7pKg==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz", + "integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.9", - "@mui/styled-engine": "^5.15.9", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.9", + "@mui/private-theming": "^5.15.14", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1492,9 +1535,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.13", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", - "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -1505,9 +1548,9 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.9.tgz", - "integrity": "sha512-yDYfr61bCYUz1QtwvpqYy/3687Z8/nS4zv7lv/ih/6ZFGMl1iolEvxRmR84v2lOYxlds+kq1IVYbXxDKh8Z9sg==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", + "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", "dependencies": { "@babel/runtime": "^7.23.9", "@types/prop-types": "^15.7.11", @@ -1531,6 +1574,71 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.1.0.tgz", + "integrity": "sha512-1ufsYdbaOW0KJriAcu8NSwXRLVnzVgf8fvxPDbJU7Y981doNBPz02nwF8P2Fsza4aVgHNXnEl6ZzSzndxCbL8w==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2938,8 +3046,7 @@ "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", - "dev": true + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debug": { "version": "4.3.4", diff --git a/frontend/package.json b/frontend/package.json index dfb8c6fa..56773c6b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,8 +15,11 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.10", + "@mui/lab": "^5.0.0-alpha.169", "@mui/material": "^5.15.10", "@mui/styled-engine-sc": "^6.0.0-alpha.16", + "@mui/x-date-pickers": "^7.1.0", + "dayjs": "^1.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.1", diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 1447580c..f74a71d6 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -3,5 +3,10 @@ "myProjects": "My Projects", "myCourses": "My Courses", "login": "Login", - "home": "Home" + "home": "Home", + "deadlines":"Past deadlines", + "course": "Course", + "last_submission": "Last submission", + "SUCCESS": "Success", + "FAIL" : "Fail" } \ No newline at end of file diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index c852df96..a8c6b1d9 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -3,5 +3,10 @@ "myProjects": "Mijn Projecten", "myCourses": "Mijn Vakken", "login": "Login", - "home": "Home" + "home": "Home", + "deadlines": "Verlopen Deadlines", + "course": "Vak", + "last_submission" : "Laatste indiening", + "SUCCESS": "Geslaagd", + "FAIL": "Gefaald" } \ No newline at end of file diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index 1d0b94b3..a3704cc8 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -1,27 +1,204 @@ import { useTranslation } from "react-i18next"; -import { Card, CardContent, Typography, Grid , Container} from '@mui/material'; -import {useEffect } from 'react'; +import {Card, CardContent, Typography, Grid, Container, Badge} from '@mui/material'; +import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; +import {DayCalendarSkeleton, LocalizationProvider} from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { CardActionArea } from '@mui/material'; +import {Link } from "react-router-dom"; + +import React, { useState } from 'react'; +import dayjs, {Dayjs} from "dayjs"; +import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; + + +function fakeFetch(date: Dayjs, { signal }: { signal: AbortSignal }) { + return new Promise<{ daysToHighlight: number[] }>((resolve, reject) => { + const timeout = setTimeout(() => { + const daysInMonth = date.daysInMonth(); + const daysToHighlight: number[] = [5, 12, 15]; + + resolve({ daysToHighlight }); + }, 500); + + signal.onabort = () => { + clearTimeout(timeout); + reject(new DOMException('aborted', 'AbortError')); + }; + }); +} +const initialValue = dayjs(Date.now()); + +function ServerDay(props: PickersDayProps & { highlightedDays?: number[] }) { + const { highlightedDays = [], day, outsideCurrentMonth, ...other } = props; + + const isSelected = + !props.outsideCurrentMonth && highlightedDays.indexOf(props.day.date()) >= 0; + + return ( + + + + ); +} /** * This component is the home page component that will be rendered when on the index route. * @returns - The home page component */ export default function HomeStudent() { + const list_of_projects = [ + {"deadline": "2024-05-01", "title": "Python lists", "course_id":"123", "project_id": "111"}, + {"deadline": "2024-06-05", "title": "Prolog intro", "course_id":"333", "project_id": "222"}, + {"deadline": "2024-04-01", "title": "Verlopen", "course_id":"333", "project_id": "222"} + ] + // get the corresponding course and latest submission, order is important needs to be the same for matching + const latest_submissions = [{"project_id": "111", "submission_status": "FAIL", + "submission_id":"111"}, + {"project_id": "333", "submission_status": "SUCCESS", "submission_id": "232"}, + {"project_id": "333", "submission_status": "SUCCESS", "submission_id": "444"} + + ] + const courses = [{"course_id": "123", "name": "Programmeren"}, + {"course_id": "222", "name": "LOGPROG"}, + {"course_id": "222", "name": "FUNPROG"} + ] const { t } = useTranslation(); - useEffect(() => { - fetch("http://127.0.0.1:5000/project?uid=123") + //const [value, setValue] = useState(new Date()); + /*useEffect(() => { + fetch("http://172.17.0.2:5000/project?uid=123") .then(response => response.json()) + }, []);*/ + const requestAbortController = React.useRef(null); + const [isLoading, setIsLoading] = React.useState(false); + const [highlightedDays, setHighlightedDays] = React.useState([]); + const fetchHighlightedDays = (date: Dayjs) => { + const controller = new AbortController(); + + fakeFetch(date, { + signal: controller.signal, + }) + .then(({ daysToHighlight }) => { + setHighlightedDays(daysToHighlight); + setIsLoading(false); + }) + .catch((error) => { + // ignore the error if it's caused by `controller.abort` + if (error.name !== 'AbortError') { + throw error; + } + }); + + requestAbortController.current = controller; + }; + + React.useEffect(() => { + fetchHighlightedDays(initialValue); + // abort request on unmount + return () => requestAbortController.current?.abort(); }, []); + + const handleMonthChange = (date: Dayjs) => { + if (requestAbortController.current) { + // make sure that you are aborting useless requests + // because it is possible to switch between months pretty quickly + requestAbortController.current.abort(); + } + + setIsLoading(true); + setHighlightedDays([]); + fetchHighlightedDays(date); + }; + const [selectedDay, setSelectedDay] = useState(null); + + // Update selectedDay state when a day is selected + const handleDaySelect = (day: Dayjs) => { + console.log(day.get('day')); + setSelectedDay(day); + }; + return ( - - - - - {t('myProjects')} - - - + + + + {t('myProjects')} + + {latest_submissions.map((submission, index) => ( + (new Date(list_of_projects[index].deadline).getTime() > Date.now()) && ( + + + + + {list_of_projects[index].title} + + + {t('course')}: {courses[index].name} + + + {t('last_submission')}: {submission.submission_status} + + + + + + ) + + ))} + + + + } + slots={{ + day: ServerDay, + }} + slotProps={{ + day: { + highlightedDays, + } as any, + }} + /> + + + + + {t('deadlines')} + + {latest_submissions.map((submission, index) => ( + (new Date(list_of_projects[index].deadline).getTime() <= Date.now()) && ( + + + + + {list_of_projects[index].title} + + + {t('course')}: {courses[index].name} + + + {t('last_submission')}: {submission.submission_status} + + + + + ) + ))} + ); From 7455a330202ff95d0cb09a3e0c6167c3392a3520 Mon Sep 17 00:00:00 2001 From: warre Date: Sun, 7 Apr 2024 19:40:05 +0200 Subject: [PATCH 03/29] homepage changes --- frontend/package-lock.json | 7 + frontend/public/locales/en/translation.json | 9 +- frontend/public/locales/nl/translation.json | 8 +- frontend/src/pages/home/Home.tsx | 28 +- frontend/src/pages/home/Home_student.tsx | 306 ++++++++++++-------- 5 files changed, 224 insertions(+), 134 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e43063d3..63ccb886 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,6 +39,7 @@ "i18next": "^23.10.1", "i18next-browser-languagedetector": "^7.2.1", "i18next-http-backend": "^2.5.0", + "react-error-overlay": "^6.0.9", "react-i18next": "^14.1.0", "typescript": "^5.2.2", "vite": "^5.1.7" @@ -5379,6 +5380,12 @@ "react": "^18.2.0" } }, + "node_modules/react-error-overlay": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", + "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==", + "dev": true + }, "node_modules/react-i18next": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz", diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 8a0c03b6..2a4ffcfd 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -9,7 +9,8 @@ }, "home": { "homepage": "Homepage", - "welcomeDescription": "Welcome to Peristeronas the online submission platform of UGent" + "welcomeDescription": "Welcome to Perister贸nas the online submission platform of UGent", + "login": "Login" }, "courseForm": { "courseName": "Course Name", @@ -19,10 +20,12 @@ "student" : { "myProjects": "My Projects", "myCourses": "My Courses", - "deadlines": "Past deadline", + "deadlines": "Past deadlines", "last_submission" : "Last submission", "course": "Course", "SUCCESS": "Success", - "FAIL": "Fail" + "FAIL": "Fail", + "deadlinesOnDay": "Deadlines on: ", + "noDeadline": "No deadlines" } } diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 9cc94701..61d8d1b9 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -9,7 +9,8 @@ }, "home": { "homepage": "Homepage", - "welcomeDescription": "Welkom bij Peristeronas het online indieningsplatform van UGent" + "welcomeDescription": "Welkom bij Perister贸nas het online indieningsplatform van UGent", + "login": "Aanmelden" }, "courseForm": { "courseName": "Vak Naam", @@ -23,6 +24,9 @@ "course": "Vak", "last_submission": "Laatste indiening", "SUCCESS": "Geslaagd", - "FAIL": "Gefaald" + "FAIL": "Gefaald", + "deadlinesOnDay": "Deadlines op: ", + "noDeadline": "Geen deadlines" + } } \ No newline at end of file diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index f962af82..be629648 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { Button, Container, Typography, Box } from "@mui/material"; - +import {Link } from "react-router-dom"; /** * This component is the home page component that will be rendered when on the index route. @@ -8,9 +8,8 @@ import { Button, Container, Typography, Box } from "@mui/material"; */ export default function Home() { const { t } = useTranslation('translation', { keyPrefix: 'home' }); - const handleLoginClick = () => { - // Handle login button click here - }; + //console.log("log env", process.env.REACT_APP_LOGIN_LINK) + const login_redirect:string =import.meta.env.VITE_LOGIN_LINK return ( - - - Peristeronas + + + Perister贸nas - + {t('welcomeDescription', 'Welcome to Peristeronas.')} - diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index f92b0618..42a93bde 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -1,33 +1,105 @@ import { useTranslation } from "react-i18next"; -import {Card, CardContent, Typography, Grid, Container, Badge} from '@mui/material'; +import {Card, CardContent, Typography, Grid, Container, Badge, Box} from '@mui/material'; import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; import {DayCalendarSkeleton, LocalizationProvider} from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { CardActionArea } from '@mui/material'; import {Link } from "react-router-dom"; -import React, { useState } from 'react'; +import React, {useEffect, useState} from 'react'; import dayjs, {Dayjs} from "dayjs"; import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; +interface ShortSubmission { + submission_id:number, + submission_time:Date, + submission_status:string +} +interface Project { + project_id:number , + title :string, + description:string, + assignment_file:string, + deadline:Date, + course_id:number, + visible_for_students:boolean, + archived:boolean, + test_path:string, + script_name:string, + regex_expressions:string[], + short_submission: ShortSubmission -function fakeFetch(date: Dayjs, { signal }: { signal: AbortSignal }) { - return new Promise<{ daysToHighlight: number[] }>((resolve, reject) => { - const timeout = setTimeout(() => { - const daysInMonth = date.daysInMonth(); - const daysToHighlight: number[] = [5, 12, 15]; - - resolve({ daysToHighlight }); - }, 500); - - signal.onabort = () => { - clearTimeout(timeout); - reject(new DOMException('aborted', 'AbortError')); - }; - }); } +interface Course { + course_id: string; + name: string; + teacher: string; + ufora_id: string; +} +const apiUrl = import.meta.env.VITE_APP_API_URL const initialValue = dayjs(Date.now()); +interface DeadlineInfoProps { + selectedDay: Dayjs; + deadlines: Project[]; +} +interface ProjectCardProps{ + deadlines:Project[] +} + +const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) => { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + const deadlinesOnSelectedDay = deadlines.filter( + deadline => dayjs(deadline.deadline).isSame(selectedDay, 'day') + ); + //list of the corresponding assignment + return ( +
+ {deadlinesOnSelectedDay.length === 0 ? ( + + + + {t('noDeadline')} + + + + ) : } +
+ ); +}; +const ProjectCard: React.FC = ({ deadlines }) => { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + //list of the corresponding assignment + return ( + + {deadlines.map((project, index) => ( + + + + + {project.title} + + + {t('course')}: {"placeholder name"} + + + {t('last_submission')}: {project.short_submission.submission_status} + + + Deadline: {dayjs(project.deadline).format('MMMM D, YYYY')} + + + + + ))} + + + ); +}; + +/** + * + */ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] }) { const { highlightedDays = [], day, outsideCurrentMonth, ...other } = props; @@ -41,9 +113,9 @@ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] badgeContent={isSelected ? '馃敶' : undefined} sx={{ '.MuiBadge-badge': { - fontSize: '0.5em', // Adjust as needed - top: 8, // Adjust as needed - right: 8, // Adjust as needed + fontSize: '0.5em', + top: 8, + right: 8, }, }} > @@ -51,80 +123,96 @@ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] ); } +const changeMonth = ( + date:Dayjs, + projects:Project[], + setHighlightedDays:React.Dispatch>, +) =>{ + const month = date.month() + const year = date.year() + const hDays:number[] = [] + projects.map((project, ) => { + if(project.deadline.getMonth() == month && project.deadline.getFullYear() == year){ + hDays.push(project.deadline.getDate()) + } + } + ); + setHighlightedDays(hDays) +} +const handleMonthChange =( + date: Dayjs, + projects:Project[], + setHighlightedDays: React.Dispatch>, +) => { + + setHighlightedDays([]); + // projects are now only fetched on page load + changeMonth(date, projects, setHighlightedDays) + +}; +const fetchProjects = async (setProjects: React.Dispatch>) => { + const response = await fetch(`${apiUrl}/projects`, { + headers: { + "Authorization": "teacher2" // todo add true authorization + }, + }) + const jsonData = await response.json(); + const formattedData: Project[] = await Promise.all( jsonData.data.map(async (item:Project) => { + const uid:string = "Bart" // todo check if we can fecth it so and get the uid of the logged in user + const project_id:number = 94 // todo make this item.project_id when fixed + const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${project_id}`), { + headers: { + "Authorization": "teacher2" // todo add true authorization + }, + })).json() + //get the latest submission + const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ + submission_id: submission.submission_id,//todo convert this into a number after bugfix + submission_time: new Date(submission.submission_time), + submission_status: submission.submission_status + } + )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; + return { + project_id: item.project_id, // todo convert this into a number after bug fix "project_id" + title: item.title, + description: item.description, + assignment_file: item.assignment_file, + deadline: new Date(item.deadline), + course_id: Number(item.course_id), + visible_for_students: Boolean(item.visible_for_students), + archived: Boolean(item.archived), + test_path: item.test_path, + script_name: item.script_name, + regex_expressions: item.regex_expressions, + short_submission: latest_submission + }})); + setProjects(formattedData); + return formattedData +} /** * This component is the home page component that will be rendered when on the index route. * @returns - The home page component */ export default function HomeStudent() { - const list_of_projects = [ - {"deadline": "2024-05-01", "title": "Python lists", "course_id":"123", "project_id": "111"}, - {"deadline": "2024-06-05", "title": "Prolog intro", "course_id":"333", "project_id": "222"}, - {"deadline": "2024-04-01", "title": "Verlopen", "course_id":"333", "project_id": "222"} - ] - // get the corresponding course and latest submission, order is important needs to be the same for matching - const latest_submissions = [{"project_id": "111", "submission_status": "FAIL", - "submission_id":"111"}, - {"project_id": "333", "submission_status": "SUCCESS", "submission_id": "232"}, - {"project_id": "333", "submission_status": "SUCCESS", "submission_id": "444"} - - ] - const courses = [{"course_id": "123", "name": "Programmeren"}, - {"course_id": "222", "name": "LOGPROG"}, - {"course_id": "222", "name": "FUNPROG"} - ] const { t } = useTranslation('translation', { keyPrefix: 'student' }); - //const [value, setValue] = useState(new Date()); - /*useEffect(() => { - fetch("http://172.17.0.2:5000/project?uid=123") - .then(response => response.json()) - }, []);*/ - const requestAbortController = React.useRef(null); - const [isLoading, setIsLoading] = React.useState(false); + + const [projects, setProjects] = useState([]); + const [highlightedDays, setHighlightedDays] = React.useState([]); - const fetchHighlightedDays = (date: Dayjs) => { - const controller = new AbortController(); - fakeFetch(date, { - signal: controller.signal, + useEffect(() => { + fetchProjects(setProjects).then(p => { + handleMonthChange(initialValue, p,setHighlightedDays) }) - .then(({ daysToHighlight }) => { - setHighlightedDays(daysToHighlight); - setIsLoading(false); - }) - .catch((error) => { - // ignore the error if it's caused by `controller.abort` - if (error.name !== 'AbortError') { - throw error; - } - }); - - requestAbortController.current = controller; - }; - - React.useEffect(() => { - fetchHighlightedDays(initialValue); - // abort request on unmount - return () => requestAbortController.current?.abort(); }, []); - const handleMonthChange = (date: Dayjs) => { - if (requestAbortController.current) { - // make sure that you are aborting useless requests - // because it is possible to switch between months pretty quickly - requestAbortController.current.abort(); - } - - setIsLoading(true); - setHighlightedDays([]); - fetchHighlightedDays(date); - }; - const [selectedDay, setSelectedDay] = useState(null); + const [selectedDay, setSelectedDay] = useState(dayjs(Date.now())); // Update selectedDay state when a day is selected const handleDaySelect = (day: Dayjs) => { - console.log(day.get('day')); setSelectedDay(day); + }; return ( @@ -134,34 +222,20 @@ export default function HomeStudent() { {t('myProjects')} - {latest_submissions.map((submission, index) => ( - (new Date(list_of_projects[index].deadline).getTime() > Date.now()) && ( - - - - - {list_of_projects[index].title} - - - {t('course')}: {courses[index].name} - - - {t('last_submission')}: {submission.submission_status} - - - - - - ) - - ))} + + dayjs(dayjs()).isBefore(project.deadline)) + .sort((a, b) => dayjs(a.deadline).isBefore(dayjs(b.deadline)) ? -1 : 1) + .slice(0, 3) + } /> + {handleMonthChange(date, projects, + setHighlightedDays)}} onChange={handleDaySelect} renderLoading={() => } slots={{ @@ -179,25 +253,17 @@ export default function HomeStudent() { {t('deadlines')} - {latest_submissions.map((submission, index) => ( - (new Date(list_of_projects[index].deadline).getTime() <= Date.now()) && ( - - - - - {list_of_projects[index].title} - - - {t('course')}: {courses[index].name} - - - {t('last_submission')}: {submission.submission_status} - - - - - ) - ))} + dayjs(dayjs()).isAfter(project.deadline)) + .sort((a, b) => dayjs(a.deadline).isAfter(dayjs(b.deadline)) ? -1 : 1) + .slice(-2) + } /> + + + + {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} + +
From 1ff6295dd49fee84b0d21ebddef6cd77b30d3d36 Mon Sep 17 00:00:00 2001 From: warre Date: Sun, 7 Apr 2024 22:02:59 +0200 Subject: [PATCH 04/29] homepage changes --- frontend/public/locales/en/translation.json | 3 +- frontend/public/locales/nl/translation.json | 3 +- frontend/src/pages/home/Home_student.tsx | 130 +++++++++++--------- 3 files changed, 75 insertions(+), 61 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 2a4ffcfd..32177a24 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -26,6 +26,7 @@ "SUCCESS": "Success", "FAIL": "Fail", "deadlinesOnDay": "Deadlines on: ", - "noDeadline": "No deadlines" + "noDeadline": "No deadlines", + "no_submission_yet" : "No submission yet" } } diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 61d8d1b9..86522d32 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -26,7 +26,8 @@ "SUCCESS": "Geslaagd", "FAIL": "Gefaald", "deadlinesOnDay": "Deadlines op: ", - "noDeadline": "Geen deadlines" + "noDeadline": "Geen deadlines", + "no_submission_yet" : "Nog geen indiening" } } \ No newline at end of file diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index 42a93bde..13aa9cc1 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -16,7 +16,7 @@ interface ShortSubmission { submission_status:string } interface Project { - project_id:number , + project_id:string , title :string, description:string, assignment_file:string, @@ -27,7 +27,8 @@ interface Project { test_path:string, script_name:string, regex_expressions:string[], - short_submission: ShortSubmission + short_submission: ShortSubmission, + course:Course } interface Course { @@ -74,16 +75,18 @@ const ProjectCard: React.FC = ({ deadlines }) => { {deadlines.map((project, index) => ( - + - + {project.title} - {t('course')}: {"placeholder name"} + {t('course')}: {project.course.name} - {t('last_submission')}: {project.short_submission.submission_status} + {t('last_submission')}: {project.short_submission ? + t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} Deadline: {dayjs(project.deadline).format('MMMM D, YYYY')} @@ -92,7 +95,6 @@ const ProjectCard: React.FC = ({ deadlines }) => { ))} - ); }; @@ -123,22 +125,6 @@ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] ); } -const changeMonth = ( - date:Dayjs, - projects:Project[], - setHighlightedDays:React.Dispatch>, -) =>{ - const month = date.month() - const year = date.year() - const hDays:number[] = [] - projects.map((project, ) => { - if(project.deadline.getMonth() == month && project.deadline.getFullYear() == year){ - hDays.push(project.deadline.getDate()) - } - } - ); - setHighlightedDays(hDays) -} const handleMonthChange =( date: Dayjs, projects:Project[], @@ -147,31 +133,53 @@ const handleMonthChange =( setHighlightedDays([]); // projects are now only fetched on page load - changeMonth(date, projects, setHighlightedDays) + const hDays:number[] = [] + projects.map((project, ) => { + if(project.deadline.getMonth() == date.month() && project.deadline.getFullYear() == date.year()){ + hDays.push(project.deadline.getDate()) + } + } + ); + setHighlightedDays(hDays) }; const fetchProjects = async (setProjects: React.Dispatch>) => { + const header = { + "Authorization": "teacher2" // todo add true authorization + } const response = await fetch(`${apiUrl}/projects`, { - headers: { - "Authorization": "teacher2" // todo add true authorization - }, + headers:header }) const jsonData = await response.json(); const formattedData: Project[] = await Promise.all( jsonData.data.map(async (item:Project) => { - const uid:string = "Bart" // todo check if we can fecth it so and get the uid of the logged in user - const project_id:number = 94 // todo make this item.project_id when fixed + const project_id:string = item.project_id.split("/")[1]// todo this can change later + const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${project_id}`), { - headers: { - "Authorization": "teacher2" // todo add true authorization - }, + headers: header })).json() + //get the latest submission const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ - submission_id: submission.submission_id,//todo convert this into a number after bugfix + submission_id: submission.submission_id,//this is the path submission_time: new Date(submission.submission_time), submission_status: submission.submission_status } )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; + // fetch the course id of the project + const project_item = await (await fetch(encodeURI(`${apiUrl}/${item.project_id}`), { + headers:header + })).json() + + //fetch the course + const response_courses = await (await fetch(encodeURI(`${apiUrl}/courses/${project_item.data.course_id}`), { + headers: header + })).json() + const course = { + course_id: response_courses.data.course_id, + name: response_courses.data.name, + teacher: response_courses.data.teacher, + ufora_id: response_courses.data.ufora_id + } return { project_id: item.project_id, // todo convert this into a number after bug fix "project_id" title: item.title, @@ -184,7 +192,8 @@ const fetchProjects = async (setProjects: React.Dispatch - + {t('myProjects')} @@ -230,25 +239,6 @@ export default function HomeStudent() { } /> - - - {handleMonthChange(date, projects, - setHighlightedDays)}} - onChange={handleDaySelect} - renderLoading={() => } - slots={{ - day: ServerDay, - }} - slotProps={{ - day: { - highlightedDays, - } as any, - }} - /> - - {t('deadlines')} @@ -259,11 +249,33 @@ export default function HomeStudent() { .slice(-2) } /> - - - {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} - - + + + + {handleMonthChange(date, projects, + setHighlightedDays)}} + onChange={handleDaySelect} + renderLoading={() => } + slots={{ + day: ServerDay, + }} + slotProps={{ + day: { + highlightedDays, + } as any, + }} + /> + + + + {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} + + + + + From 252ed380316b3d01c46404915f5141b433a33807 Mon Sep 17 00:00:00 2001 From: warre Date: Sun, 7 Apr 2024 19:40:05 +0200 Subject: [PATCH 05/29] Added: Homepage and Student Homepage --- frontend/package-lock.json | 7 + frontend/public/locales/en/translation.json | 10 +- frontend/public/locales/nl/translation.json | 9 +- frontend/src/pages/home/Home.tsx | 28 +- frontend/src/pages/home/Home_student.tsx | 356 ++++++++++++-------- 5 files changed, 258 insertions(+), 152 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e43063d3..63ccb886 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,6 +39,7 @@ "i18next": "^23.10.1", "i18next-browser-languagedetector": "^7.2.1", "i18next-http-backend": "^2.5.0", + "react-error-overlay": "^6.0.9", "react-i18next": "^14.1.0", "typescript": "^5.2.2", "vite": "^5.1.7" @@ -5379,6 +5380,12 @@ "react": "^18.2.0" } }, + "node_modules/react-error-overlay": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", + "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==", + "dev": true + }, "node_modules/react-i18next": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz", diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 8a0c03b6..32177a24 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -9,7 +9,8 @@ }, "home": { "homepage": "Homepage", - "welcomeDescription": "Welcome to Peristeronas the online submission platform of UGent" + "welcomeDescription": "Welcome to Perister贸nas the online submission platform of UGent", + "login": "Login" }, "courseForm": { "courseName": "Course Name", @@ -19,10 +20,13 @@ "student" : { "myProjects": "My Projects", "myCourses": "My Courses", - "deadlines": "Past deadline", + "deadlines": "Past deadlines", "last_submission" : "Last submission", "course": "Course", "SUCCESS": "Success", - "FAIL": "Fail" + "FAIL": "Fail", + "deadlinesOnDay": "Deadlines on: ", + "noDeadline": "No deadlines", + "no_submission_yet" : "No submission yet" } } diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 9cc94701..86522d32 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -9,7 +9,8 @@ }, "home": { "homepage": "Homepage", - "welcomeDescription": "Welkom bij Peristeronas het online indieningsplatform van UGent" + "welcomeDescription": "Welkom bij Perister贸nas het online indieningsplatform van UGent", + "login": "Aanmelden" }, "courseForm": { "courseName": "Vak Naam", @@ -23,6 +24,10 @@ "course": "Vak", "last_submission": "Laatste indiening", "SUCCESS": "Geslaagd", - "FAIL": "Gefaald" + "FAIL": "Gefaald", + "deadlinesOnDay": "Deadlines op: ", + "noDeadline": "Geen deadlines", + "no_submission_yet" : "Nog geen indiening" + } } \ No newline at end of file diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index f962af82..be629648 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { Button, Container, Typography, Box } from "@mui/material"; - +import {Link } from "react-router-dom"; /** * This component is the home page component that will be rendered when on the index route. @@ -8,9 +8,8 @@ import { Button, Container, Typography, Box } from "@mui/material"; */ export default function Home() { const { t } = useTranslation('translation', { keyPrefix: 'home' }); - const handleLoginClick = () => { - // Handle login button click here - }; + //console.log("log env", process.env.REACT_APP_LOGIN_LINK) + const login_redirect:string =import.meta.env.VITE_LOGIN_LINK return ( - - - Peristeronas + + + Perister贸nas - + {t('welcomeDescription', 'Welcome to Peristeronas.')} - diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index f92b0618..404e7831 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -1,33 +1,110 @@ import { useTranslation } from "react-i18next"; -import {Card, CardContent, Typography, Grid, Container, Badge} from '@mui/material'; +import {Card, CardContent, Typography, Grid, Container, Badge, Box} from '@mui/material'; import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; import {DayCalendarSkeleton, LocalizationProvider} from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { CardActionArea } from '@mui/material'; import {Link } from "react-router-dom"; -import React, { useState } from 'react'; +import React, {useEffect, useState} from 'react'; import dayjs, {Dayjs} from "dayjs"; import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; +interface ShortSubmission { + submission_id:number, + submission_time:Date, + submission_status:string +} +interface Project { + project_id:string , + title :string, + description:string, + assignment_file:string, + deadline:Date, + course_id:number, + visible_for_students:boolean, + archived:boolean, + test_path:string, + script_name:string, + regex_expressions:string[], + short_submission: ShortSubmission, + course:Course -function fakeFetch(date: Dayjs, { signal }: { signal: AbortSignal }) { - return new Promise<{ daysToHighlight: number[] }>((resolve, reject) => { - const timeout = setTimeout(() => { - const daysInMonth = date.daysInMonth(); - const daysToHighlight: number[] = [5, 12, 15]; - - resolve({ daysToHighlight }); - }, 500); - - signal.onabort = () => { - clearTimeout(timeout); - reject(new DOMException('aborted', 'AbortError')); - }; - }); } +interface Course { + course_id: string; + name: string; + teacher: string; + ufora_id: string; +} +const apiUrl = import.meta.env.VITE_APP_API_URL const initialValue = dayjs(Date.now()); +interface DeadlineInfoProps { + selectedDay: Dayjs; + deadlines: Project[]; +} +interface ProjectCardProps{ + deadlines:Project[] +} +type ExtendedPickersDayProps = PickersDayProps & { highlightedDays?: number[] }; + +const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) => { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + const deadlinesOnSelectedDay = deadlines.filter( + deadline => dayjs(deadline.deadline).isSame(selectedDay, 'day') + ); + //list of the corresponding assignment + return ( +
+ {deadlinesOnSelectedDay.length === 0 ? ( + + + + {t('noDeadline')} + + + + ) : } +
+ ); +}; +const ProjectCard: React.FC = ({ deadlines }) => { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + //list of the corresponding assignment + return ( + + {deadlines.map((project, index) => ( + + + + + {project.title} + + + {t('course')}: {project.course.name} + + + {t('last_submission')}: {project.short_submission ? + t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} + + + Deadline: {dayjs(project.deadline).format('MMMM D, YYYY')} + + + + + ))} + + ); +}; + +/** + * + * @param props - The day and the deadlines + * @returns - The ServerDay component that displays a badge for specific days + */ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] }) { const { highlightedDays = [], day, outsideCurrentMonth, ...other } = props; @@ -41,9 +118,9 @@ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] badgeContent={isSelected ? '馃敶' : undefined} sx={{ '.MuiBadge-badge': { - fontSize: '0.5em', // Adjust as needed - top: 8, // Adjust as needed - right: 8, // Adjust as needed + fontSize: '0.5em', + top: 8, + right: 8, }, }} > @@ -51,153 +128,156 @@ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] ); } +const handleMonthChange =( + date: Dayjs, + projects:Project[], + setHighlightedDays: React.Dispatch>, +) => { + + setHighlightedDays([]); + // projects are now only fetched on page load + const hDays:number[] = [] + projects.map((project, ) => { + if(project.deadline.getMonth() == date.month() && project.deadline.getFullYear() == date.year()){ + hDays.push(project.deadline.getDate()) + } + } + ); + setHighlightedDays(hDays) + +}; +const fetchProjects = async (setProjects: React.Dispatch>) => { + const header = { + "Authorization": "teacher2" // todo add true authorization + } + const response = await fetch(`${apiUrl}/projects`, { + headers:header + }) + const jsonData = await response.json(); + const formattedData: Project[] = await Promise.all( jsonData.data.map(async (item:Project) => { + const project_id:string = item.project_id.split("/")[1]// todo check if this does not change later + + const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${project_id}`), { + headers: header + })).json() + + //get the latest submission + const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ + submission_id: submission.submission_id,//this is the path + submission_time: new Date(submission.submission_time), + submission_status: submission.submission_status + } + )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; + // fetch the course id of the project + const project_item = await (await fetch(encodeURI(`${apiUrl}/${item.project_id}`), { + headers:header + })).json() + + //fetch the course + const response_courses = await (await fetch(encodeURI(`${apiUrl}/courses/${project_item.data.course_id}`), { + headers: header + })).json() + const course = { + course_id: response_courses.data.course_id, + name: response_courses.data.name, + teacher: response_courses.data.teacher, + ufora_id: response_courses.data.ufora_id + } + return { + project_id: item.project_id, // is not a number but a path + title: item.title, + description: item.description, + assignment_file: item.assignment_file, + deadline: new Date(item.deadline), + course_id: Number(item.course_id), + visible_for_students: Boolean(item.visible_for_students), + archived: Boolean(item.archived), + test_path: item.test_path, + script_name: item.script_name, + regex_expressions: item.regex_expressions, + short_submission: latest_submission, + course: course + }})); + setProjects(formattedData); + return formattedData +} /** * This component is the home page component that will be rendered when on the index route. * @returns - The home page component */ export default function HomeStudent() { - const list_of_projects = [ - {"deadline": "2024-05-01", "title": "Python lists", "course_id":"123", "project_id": "111"}, - {"deadline": "2024-06-05", "title": "Prolog intro", "course_id":"333", "project_id": "222"}, - {"deadline": "2024-04-01", "title": "Verlopen", "course_id":"333", "project_id": "222"} - ] - // get the corresponding course and latest submission, order is important needs to be the same for matching - const latest_submissions = [{"project_id": "111", "submission_status": "FAIL", - "submission_id":"111"}, - {"project_id": "333", "submission_status": "SUCCESS", "submission_id": "232"}, - {"project_id": "333", "submission_status": "SUCCESS", "submission_id": "444"} - - ] - const courses = [{"course_id": "123", "name": "Programmeren"}, - {"course_id": "222", "name": "LOGPROG"}, - {"course_id": "222", "name": "FUNPROG"} - ] const { t } = useTranslation('translation', { keyPrefix: 'student' }); - //const [value, setValue] = useState(new Date()); - /*useEffect(() => { - fetch("http://172.17.0.2:5000/project?uid=123") - .then(response => response.json()) - }, []);*/ - const requestAbortController = React.useRef(null); - const [isLoading, setIsLoading] = React.useState(false); - const [highlightedDays, setHighlightedDays] = React.useState([]); - const fetchHighlightedDays = (date: Dayjs) => { - const controller = new AbortController(); - fakeFetch(date, { - signal: controller.signal, - }) - .then(({ daysToHighlight }) => { - setHighlightedDays(daysToHighlight); - setIsLoading(false); - }) - .catch((error) => { - // ignore the error if it's caused by `controller.abort` - if (error.name !== 'AbortError') { - throw error; - } - }); - - requestAbortController.current = controller; - }; + const [projects, setProjects] = useState([]); - React.useEffect(() => { - fetchHighlightedDays(initialValue); - // abort request on unmount - return () => requestAbortController.current?.abort(); - }, []); + const [highlightedDays, setHighlightedDays] = React.useState([]); - const handleMonthChange = (date: Dayjs) => { - if (requestAbortController.current) { - // make sure that you are aborting useless requests - // because it is possible to switch between months pretty quickly - requestAbortController.current.abort(); - } + const [selectedDay, setSelectedDay] = useState(dayjs(Date.now())); - setIsLoading(true); - setHighlightedDays([]); - fetchHighlightedDays(date); - }; - const [selectedDay, setSelectedDay] = useState(null); + useEffect(() => { + fetchProjects(setProjects).then(p => { + handleMonthChange(initialValue, p,setHighlightedDays) + }) + }, []); // Update selectedDay state when a day is selected const handleDaySelect = (day: Dayjs) => { - console.log(day.get('day')); setSelectedDay(day); }; return ( - + {t('myProjects')} - {latest_submissions.map((submission, index) => ( - (new Date(list_of_projects[index].deadline).getTime() > Date.now()) && ( - - - - - {list_of_projects[index].title} - - - {t('course')}: {courses[index].name} - - - {t('last_submission')}: {submission.submission_status} - - - - - - ) - - ))} - - - - } - slots={{ - day: ServerDay, - }} - slotProps={{ - day: { - highlightedDays, - } as any, - }} - /> - + + dayjs(dayjs()).isBefore(project.deadline)) + .sort((a, b) => dayjs(a.deadline).isBefore(dayjs(b.deadline)) ? -1 : 1) + .slice(0, 3) + } /> + {t('deadlines')} - {latest_submissions.map((submission, index) => ( - (new Date(list_of_projects[index].deadline).getTime() <= Date.now()) && ( - - - - - {list_of_projects[index].title} - - - {t('course')}: {courses[index].name} - - - {t('last_submission')}: {submission.submission_status} - - - - - ) - ))} + dayjs(dayjs()).isAfter(project.deadline)) + .sort((a, b) => dayjs(a.deadline).isAfter(dayjs(b.deadline)) ? -1 : 1) + .slice(-2) + } /> + + + + + {handleMonthChange(date, projects, + setHighlightedDays)}} + onChange={handleDaySelect} + renderLoading={() => } + slots={{ + day: ServerDay, + }} + slotProps={{ + day: { + highlightedDays, + } as ExtendedPickersDayProps, + }} + /> + + + + {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} + + + + + From 01b878f7d1a4c62c14cd3f9a82bb9dd4683776fa Mon Sep 17 00:00:00 2001 From: warre Date: Fri, 12 Apr 2024 11:42:13 +0200 Subject: [PATCH 06/29] homepage changes --- frontend/package-lock.json | 72 ------------------ frontend/src/pages/home/Home_student.tsx | 96 ++++++++++++++---------- 2 files changed, 56 insertions(+), 112 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 581e8f52..366507a4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -41,7 +41,6 @@ "i18next": "^23.10.1", "i18next-browser-languagedetector": "^7.2.1", "i18next-http-backend": "^2.5.0", - "react-error-overlay": "^6.0.9", "react-i18next": "^14.1.0", "typescript": "^5.2.2", "vite": "^5.1.7" @@ -1643,71 +1642,6 @@ } } }, - "node_modules/@mui/x-date-pickers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.1.0.tgz", - "integrity": "sha512-1ufsYdbaOW0KJriAcu8NSwXRLVnzVgf8fvxPDbJU7Y981doNBPz02nwF8P2Fsza4aVgHNXnEl6ZzSzndxCbL8w==", - "dependencies": { - "@babel/runtime": "^7.24.0", - "@mui/base": "^5.0.0-beta.40", - "@mui/system": "^5.15.14", - "@mui/utils": "^5.15.14", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14", - "date-fns": "^2.25.0 || ^3.2.0", - "date-fns-jalali": "^2.13.0-0", - "dayjs": "^1.10.7", - "luxon": "^3.0.2", - "moment": "^2.29.4", - "moment-hijri": "^2.1.2", - "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "date-fns": { - "optional": true - }, - "date-fns-jalali": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - }, - "moment-hijri": { - "optional": true - }, - "moment-jalaali": { - "optional": true - } - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5504,12 +5438,6 @@ "react": "^18.2.0" } }, - "node_modules/react-error-overlay": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", - "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==", - "dev": true - }, "node_modules/react-i18next": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz", diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index 404e7831..e1fe0ce8 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -15,12 +15,16 @@ interface ShortSubmission { submission_time:Date, submission_status:string } +interface Deadline { + description: string; + deadline: Date; +} interface Project { project_id:string , title :string, description:string, assignment_file:string, - deadline:Date, + deadlines:Deadline[], course_id:number, visible_for_students:boolean, archived:boolean, @@ -45,14 +49,21 @@ interface DeadlineInfoProps { deadlines: Project[]; } interface ProjectCardProps{ - deadlines:Project[] + deadlines:Project[], + pred?: (deadline:Deadline) => boolean } type ExtendedPickersDayProps = PickersDayProps & { highlightedDays?: number[] }; +/** + * Displays the deadlines on a given day + * @param selectedDay - The day of interest + * @param deadlines - All the deadlines to consider + * @returns Element + */ const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) => { const { t } = useTranslation('translation', { keyPrefix: 'student' }); const deadlinesOnSelectedDay = deadlines.filter( - deadline => dayjs(deadline.deadline).isSame(selectedDay, 'day') + deadline => ( deadline.deadlines.map(d => dayjs(d.deadline).isSame(selectedDay, 'day'))) ); //list of the corresponding assignment return ( @@ -69,32 +80,41 @@ const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) = ); }; -const ProjectCard: React.FC = ({ deadlines }) => { +/** + * A clickable display of a project deadline + * @param deadlines - A list of all the deadlines + * @param pred - A predicate to filter the deadlines + * @returns Element + */ +const ProjectCard: React.FC = ({ deadlines, pred = () => true }) => { const { t } = useTranslation('translation', { keyPrefix: 'student' }); //list of the corresponding assignment return ( {deadlines.map((project, index) => ( - - - - - {project.title} - - - {t('course')}: {project.course.name} - - - {t('last_submission')}: {project.short_submission ? - t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} - - - Deadline: {dayjs(project.deadline).format('MMMM D, YYYY')} - - - - + project.deadlines.filter(pred).map((deadline, ) => ( + + + + + {project.title} + + + {t('course')}: {project.course.name} + + + {t('last_submission')}: {project.short_submission ? + t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} + + + Deadline: {dayjs(deadline.deadline).format('MMMM D, YYYY')} + + + + + )) + ))} ); @@ -138,9 +158,12 @@ const handleMonthChange =( // projects are now only fetched on page load const hDays:number[] = [] projects.map((project, ) => { - if(project.deadline.getMonth() == date.month() && project.deadline.getFullYear() == date.year()){ - hDays.push(project.deadline.getDate()) - } + project.deadlines.map((deadline,) => { + if(deadline.deadline.getMonth() == date.month() && deadline.deadline.getFullYear() == date.year()){ + hDays.push(deadline.deadline.getDate()) + } + }) + } ); setHighlightedDays(hDays) @@ -155,6 +178,7 @@ const fetchProjects = async (setProjects: React.Dispatch { + console.log("project", item) const project_id:string = item.project_id.split("/")[1]// todo check if this does not change later const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${project_id}`), { @@ -169,7 +193,7 @@ const fetchProjects = async (setProjects: React.Dispatch b.submission_time.getTime() - a.submission_time.getTime())[0]; // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${apiUrl}/${item.project_id}`), { + const project_item = await (await fetch(encodeURI(`${apiUrl}/${item.project_id}`), { //todo ! headers:header })).json() @@ -184,11 +208,11 @@ const fetchProjects = async (setProjects: React.Dispatch - dayjs(dayjs()).isBefore(project.deadline)) - .sort((a, b) => dayjs(a.deadline).isBefore(dayjs(b.deadline)) ? -1 : 1) - .slice(0, 3) - } /> + (dayjs(dayjs()).isBefore(d.deadline))} deadlines={projects} />
{t('deadlines')} - dayjs(dayjs()).isAfter(project.deadline)) - .sort((a, b) => dayjs(a.deadline).isAfter(dayjs(b.deadline)) ? -1 : 1) - .slice(-2) - } /> + dayjs(dayjs()).isAfter(d.deadline)} deadlines={projects} /> From bacf393abdce4af91127e9b811f03e334ca3d4c9 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Fri, 12 Apr 2024 12:52:36 +0200 Subject: [PATCH 07/29] project projects parser --- .../endpoints/projects/endpoint_parser.py | 36 +-- .../project/endpoints/projects/projects.py | 108 ++++++--- backend/project/models/project.py | 215 +++++++++++++++--- 3 files changed, 278 insertions(+), 81 deletions(-) diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index f4ab93ea..4703d17b 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -1,5 +1,5 @@ """ -Parser for the argument when posting or patching a project +Endpoint parser for the projects arguments """ import json @@ -15,7 +15,13 @@ help='Projects assignment file', location="form" ) -parser.add_argument('deadlines', type=str, help='Projects deadlines', location="form") +parser.add_argument( + 'deadlines', + type=str, + help='Projects deadlines', + location="form", + action='append' +) parser.add_argument("course_id", type=str, help='Projects course_id', location="form") parser.add_argument( "visible_for_students", @@ -28,7 +34,8 @@ "regex_expressions", type=str, help='Projects regex expressions', - location="form" + location="form", + action="append" ) @@ -40,18 +47,19 @@ def parse_project_params(): result_dict = {} for key, value in args.items(): if value is not None: - if "deadlines" == key: - deadlines_parsed = json.loads(value) + if key in ('deadlines', 'regex_expressions'): new_deadlines = [] - for deadline in deadlines_parsed: - new_deadlines.append( - ( - deadline["description"], - deadline["deadline"] - ) - ) + for entry in args[key]: + if key == "deadlines": + parsed_deadline = json.loads(entry) + test = { + "description": parsed_deadline["description"], + "deadline": parsed_deadline["deadline"] + } + new_deadlines.append(test) + else: + new_deadlines.append(entry) result_dict[key] = new_deadlines else: result_dict[key] = value - - return result_dict + return result_dict \ No newline at end of file diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 01873379..2fb0c8a6 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -5,14 +5,12 @@ from urllib.parse import urljoin import zipfile from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.sql import text from flask import request, jsonify from flask_restful import Resource from project.db_in import db - -from project.models.project import Project -from project.utils.query_agent import query_selected_from_model, create_model_instance from project.utils.authentication import authorize_teacher from project.endpoints.projects.endpoint_parser import parse_project_params @@ -28,20 +26,50 @@ class ProjectsEndpoint(Resource): for implementing get method """ - @authorize_teacher + # @authorize_teacher def get(self, teacher_id=None): """ Get method for listing all available projects that are currently in the API """ response_url = urljoin(API_URL, "projects") - return query_selected_from_model( - Project, - response_url, - select_values=["project_id", "title", "description", "deadlines"], - url_mapper={"project_id": response_url}, - filters=request.args - ) + + try: + custom_sql_query = ''' + SELECT + jsonb_build_object( + 'project_id', project_id, + 'title', title, + 'description', description, + 'deadlines', ARRAY_AGG( + jsonb_build_object( + 'deadline_description', d.deadline_description, + 'deadline', to_char(d.deadline, 'YYYY-MM-DD HH24:MI:SS TZ') + ) + ) + ) AS result_tuple + FROM + projects p + JOIN + unnest(p.deadlines) AS d(deadline_description, deadline) ON true + GROUP BY + project_id, title, description; + ''' + projects = db.session.execute(text(custom_sql_query)) + projects_array = [] + for project in projects: # pylint: disable=E1133 + projects_array.append(project[0]) + + respone = { + "data": projects_array, + "messsage": "Recourses fetched succesfully", + "url": response_url + } + return jsonify(respone), 200 + + except SQLAlchemyError: + return {"error": "Something went wrong while querying the database.", + "url": API_URL}, 500 @authorize_teacher def post(self, teacher_id=None): @@ -49,34 +77,52 @@ def post(self, teacher_id=None): Post functionality for project using flask_restfull parse lib """ + file = request.files["assignment_file"] project_json = parse_project_params() filename = None + # project_json["deadlines"] = json.dumps(project_json["deadlines"]) if "assignment_file" in request.files: file = request.files["assignment_file"] filename = os.path.basename(file.filename) - # save the file that is given with the request try: - new_project, status_code = create_model_instance( - Project, - project_json, - urljoin(f"{API_URL}/", "/projects"), - required_fields=[ - "title", - "description", - "course_id", - "visible_for_students", - "archived"] - ) - except SQLAlchemyError: - return jsonify({"error": "Something went wrong while inserting into the database.", - "url": f"{API_URL}/projects"}), 500 + sql_insert = f''' + INSERT + INTO projects + (title, description, deadlines, course_id, visible_for_students, archived, regex_expressions) + VALUES ('{project_json["title"]}', '{project_json["description"]}', ARRAY[''' + + # Add deadlines + for deadline in project_json["deadlines"]: + sql_insert += f'''ROW('{deadline["description"]}', '{deadline["deadline"]}')''' + if deadline != project_json["deadlines"][-1]: + sql_insert += ',' + + sql_insert += f''' + ]::deadline[], + {project_json["course_id"]}, {project_json["visible_for_students"]}, + {project_json["archived"]}, ARRAY[''' - if status_code == 400: - return new_project, status_code + # Add regex expressions + for regex in project_json["regex_expressions"]: + sql_insert += f'''\'{regex}\'''' + if regex != project_json["regex_expressions"][-1]: + sql_insert += ',' + + sql_insert += ''']) RETURNING ROW_TO_JSON(projects.*) AS insterted_data;''' + + sql_statement = text(sql_insert) + query_result = db.session.execute(sql_statement) + db.session.commit() + new_project = query_result.fetchone()[0] + except SQLAlchemyError: + db.session.rollback() + return (jsonify({ + "message": "Something went wrong in the database", + "url": f"{API_URL}/projects",}), 500) - project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}") + project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project['project_id']}") os.makedirs(project_upload_directory, exist_ok=True) if filename is not None: try: @@ -95,5 +141,5 @@ def post(self, teacher_id=None): return { "message": "Project created succesfully", "data": new_project, - "url": f"{API_URL}/projects/{new_project.project_id}" - }, 201 + "url": f"{API_URL}/projects/{new_project['project_id']}" + }, 201 \ No newline at end of file diff --git a/backend/project/models/project.py b/backend/project/models/project.py index 624f9ed0..9b7a931c 100644 --- a/backend/project/models/project.py +++ b/backend/project/models/project.py @@ -1,41 +1,184 @@ -"""Project model""" +""" +Module for project details page +for example /projects/1 if the project id of +the corresponding project is 1 +""" +import os +import zipfile +from urllib.parse import urljoin +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.sql import text + +from flask import request, jsonify +from flask_restful import Resource -from dataclasses import dataclass -from sqlalchemy import ARRAY, Boolean, Column, DateTime, ForeignKey, Integer, String, Text -from sqlalchemy_utils import CompositeType from project.db_in import db +from project.utils.authentication import authorize_teacher_or_project_admin, \ + authorize_teacher_of_project, authorize_project_visible + +from project.endpoints.projects.endpoint_parser import parse_project_params + +API_URL = os.getenv('API_HOST') +RESPONSE_URL = urljoin(API_URL, "projects") +UPLOAD_FOLDER = os.getenv('UPLOAD_URL') + -@dataclass -class Project(db.Model): # pylint: disable=too-many-instance-attributes - """This class describes the projects table, - a projects has an id, a title, a description, - an optional assignment file that can contain more explanation of the projects, - an optional deadline, - the course id of the course to which the project belongs, - visible for students variable so a teacher can decide if the students can see it yet, - archived var so we can implement the archiving functionality, - a test path,script name and regex expressions for automated testing - - Pylint disable too many instance attributes because we can't reduce the amount - of fields of the model +class ProjectDetail(Resource): """ + Class for projects/id endpoints + Inherits from flask_restful.Resource class + for implementing get, delete and put methods + """ + + @authorize_project_visible + def get(self, project_id): + """ + Get method for listing a specific project + filtered by id of that specific project + the id fetched from the url with the reaparse + """ + try: + custom_sql_query = f''' + SELECT + ROW_TO_JSON(t) as json_data + FROM ( + SELECT + project_id, + title, + description, + ARRAY_AGG( + jsonb_build_object( + 'deadline_description', d.deadline_description, + 'deadline', to_char(d.deadline, 'YYYY-MM-DD HH24:MI:SS TZ') + ) + ) AS deadlines, + p.course_id, + p.visible_for_students, + p.archived, + p.regex_expressions + FROM + projects p, + unnest(p.deadlines) AS d(deadline_description, deadline) + WHERE + p.project_id = {project_id} + GROUP BY + project_id, + title, + description + ) t; + ''' + + project = db.session.execute(text(custom_sql_query)).fetchone() + + if project: + return { + "data": project[0], + "message": "Project fetched succesfully", + "url": f'{RESPONSE_URL}/{project_id}' + }, 200 + return { + "message": f"Project with {project_id} not found", + "url": f'{RESPONSE_URL}' + }, 404 + except SQLAlchemyError: + db.session.rollback() + return (jsonify({ + "error": "Something went wrong while querying the database", + "url": f"{RESPONSE_URL}/{project_id}" + }), 500) + + @authorize_teacher_or_project_admin + def patch(self, project_id): # pylint: disable=R0914 + """ + Update method for updating a specific project + filtered by id of that specific project + """ + project_json = parse_project_params() + + try: + patch_values = [] + for key, value in project_json.items(): + update = f"{key} = '{value}'" + patch_values.append(update) + + sql_patch = f''' + UPDATE projects SET {', '.join(patch_values)} + WHERE project_id = {project_id} + RETURNING ROW_TO_JSON(projects.*) AS updated_data;''' + project = db.session.execute(text(sql_patch)).fetchone() + if not project: + return (jsonify({ + "error": "Project was not found", + "url": RESPONSE_URL + }), 404) + db.session.commit() + except SQLAlchemyError: + db.session.rollback() + return (jsonify({ + "error": "Something went wrong while updating the project", + "url": RESPONSE_URL + }, 500)) + + if "assignment_file" in request.files: + file = request.files["assignment_file"] + filename = os.path.basename(file.filename) + project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{project_id}") + os.makedirs(project_upload_directory, exist_ok=True) + try: + # remove the old file + try: + to_rem_files = os.listdir(project_upload_directory) + for to_rem_file in to_rem_files: + to_rem_file_path = os.path.join(project_upload_directory, to_rem_file) + if os.path.isfile(to_rem_file_path): + os.remove(to_rem_file_path) + except FileNotFoundError: + db.session.rollback() + return ({ + "message": "Something went wrong deleting the old project files", + "url": f"{API_URL}/projects/{project_id}" + }) + + # removed all files now upload the new files + file.save(os.path.join(project_upload_directory, filename)) + zip_location = os.path.join(project_upload_directory, filename) + with zipfile.ZipFile(zip_location) as upload_zip: + upload_zip.extractall(project_upload_directory) + + except zipfile.BadZipfile: + db.session.rollback() + return ({ + "message": + "Please provide a valid .zip file for updating the instructions", + "url": f"{API_URL}/projects/{project_id}" + }, + 400) + + return (jsonify({ + "message": "Project patched succesfully", + "data": project[0] + }), 200) - __tablename__ = "projects" - project_id: int = Column(Integer, primary_key=True) - title: str = Column(String(50), nullable=False, unique=False) - description: str = Column(Text, nullable=False) - deadlines: list = Column(ARRAY( - CompositeType( - "deadline", - [ - Column("description", Text), - Column("deadline", DateTime(timezone=True)) - ] - ), - dimensions=1 - ) - ) - course_id: int = Column(Integer, ForeignKey("courses.course_id"), nullable=False) - visible_for_students: bool = Column(Boolean, nullable=False) - archived: bool = Column(Boolean, nullable=False) - regex_expressions: list[str] = Column(ARRAY(String(50))) + @authorize_teacher_of_project + def delete(self, project_id): + """ + Delete a project and all of its submissions in cascade + done by project id + """ + try: + delete_query = f''' + DELETE FROM projects WHERE project_id = {project_id} RETURNING project_id; + ''' + deleted_project = db.session.execute(text(delete_query)).fetchone() + if deleted_project is None: + return (jsonify( + {"message": f"Project with {project_id} doesn't exist", + "url": RESPONSE_URL + }, 404)) + db.session.commit() + return (jsonify({"message": "Resource deleted successfully", + "url": RESPONSE_URL}, 200)) + except SQLAlchemyError: + db.session.rollback() + return {"error": "Something went wrong deleting", + "url": RESPONSE_URL}, 500 \ No newline at end of file From cc585116a095f12b49ed51bfcd1bf80cee243906 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Fri, 12 Apr 2024 12:59:52 +0200 Subject: [PATCH 08/29] test passed --- .../endpoints/projects/project_detail.py | 125 +++++++--- backend/project/models/project.py | 213 +++--------------- backend/tests/endpoints/conftest.py | 30 ++- backend/tests/endpoints/project_test.py | 46 ++-- 4 files changed, 167 insertions(+), 247 deletions(-) diff --git a/backend/project/endpoints/projects/project_detail.py b/backend/project/endpoints/projects/project_detail.py index d2affa57..9b7a931c 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -6,15 +6,13 @@ import os import zipfile from urllib.parse import urljoin +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.sql import text -from flask import request +from flask import request, jsonify from flask_restful import Resource from project.db_in import db - -from project.models.project import Project -from project.utils.query_agent import query_by_id_from_model, delete_by_id_from_model, \ - patch_by_id_from_model from project.utils.authentication import authorize_teacher_or_project_admin, \ authorize_teacher_of_project, authorize_project_visible @@ -39,30 +37,87 @@ def get(self, project_id): filtered by id of that specific project the id fetched from the url with the reaparse """ - - return query_by_id_from_model( - Project, - "project_id", - project_id, - RESPONSE_URL) + try: + custom_sql_query = f''' + SELECT + ROW_TO_JSON(t) as json_data + FROM ( + SELECT + project_id, + title, + description, + ARRAY_AGG( + jsonb_build_object( + 'deadline_description', d.deadline_description, + 'deadline', to_char(d.deadline, 'YYYY-MM-DD HH24:MI:SS TZ') + ) + ) AS deadlines, + p.course_id, + p.visible_for_students, + p.archived, + p.regex_expressions + FROM + projects p, + unnest(p.deadlines) AS d(deadline_description, deadline) + WHERE + p.project_id = {project_id} + GROUP BY + project_id, + title, + description + ) t; + ''' + + project = db.session.execute(text(custom_sql_query)).fetchone() + + if project: + return { + "data": project[0], + "message": "Project fetched succesfully", + "url": f'{RESPONSE_URL}/{project_id}' + }, 200 + return { + "message": f"Project with {project_id} not found", + "url": f'{RESPONSE_URL}' + }, 404 + except SQLAlchemyError: + db.session.rollback() + return (jsonify({ + "error": "Something went wrong while querying the database", + "url": f"{RESPONSE_URL}/{project_id}" + }), 500) @authorize_teacher_or_project_admin - def patch(self, project_id): + def patch(self, project_id): # pylint: disable=R0914 """ Update method for updating a specific project filtered by id of that specific project """ project_json = parse_project_params() - output, status_code = patch_by_id_from_model( - Project, - "project_id", - project_id, - RESPONSE_URL, - project_json - ) - if status_code != 200: - return output, status_code + try: + patch_values = [] + for key, value in project_json.items(): + update = f"{key} = '{value}'" + patch_values.append(update) + + sql_patch = f''' + UPDATE projects SET {', '.join(patch_values)} + WHERE project_id = {project_id} + RETURNING ROW_TO_JSON(projects.*) AS updated_data;''' + project = db.session.execute(text(sql_patch)).fetchone() + if not project: + return (jsonify({ + "error": "Project was not found", + "url": RESPONSE_URL + }), 404) + db.session.commit() + except SQLAlchemyError: + db.session.rollback() + return (jsonify({ + "error": "Something went wrong while updating the project", + "url": RESPONSE_URL + }, 500)) if "assignment_file" in request.files: file = request.files["assignment_file"] @@ -99,7 +154,10 @@ def patch(self, project_id): }, 400) - return output, status_code + return (jsonify({ + "message": "Project patched succesfully", + "data": project[0] + }), 200) @authorize_teacher_of_project def delete(self, project_id): @@ -107,9 +165,20 @@ def delete(self, project_id): Delete a project and all of its submissions in cascade done by project id """ - - return delete_by_id_from_model( - Project, - "project_id", - project_id, - RESPONSE_URL) + try: + delete_query = f''' + DELETE FROM projects WHERE project_id = {project_id} RETURNING project_id; + ''' + deleted_project = db.session.execute(text(delete_query)).fetchone() + if deleted_project is None: + return (jsonify( + {"message": f"Project with {project_id} doesn't exist", + "url": RESPONSE_URL + }, 404)) + db.session.commit() + return (jsonify({"message": "Resource deleted successfully", + "url": RESPONSE_URL}, 200)) + except SQLAlchemyError: + db.session.rollback() + return {"error": "Something went wrong deleting", + "url": RESPONSE_URL}, 500 \ No newline at end of file diff --git a/backend/project/models/project.py b/backend/project/models/project.py index 9b7a931c..dd5ada03 100644 --- a/backend/project/models/project.py +++ b/backend/project/models/project.py @@ -1,184 +1,43 @@ -""" -Module for project details page -for example /projects/1 if the project id of -the corresponding project is 1 -""" -import os -import zipfile -from urllib.parse import urljoin -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.sql import text - -from flask import request, jsonify -from flask_restful import Resource +"""Project model""" +from dataclasses import dataclass +from sqlalchemy import ARRAY, Boolean, Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy_utils import CompositeType from project.db_in import db -from project.utils.authentication import authorize_teacher_or_project_admin, \ - authorize_teacher_of_project, authorize_project_visible - -from project.endpoints.projects.endpoint_parser import parse_project_params -API_URL = os.getenv('API_HOST') -RESPONSE_URL = urljoin(API_URL, "projects") -UPLOAD_FOLDER = os.getenv('UPLOAD_URL') +@dataclass +class Project(db.Model): # pylint: disable=too-many-instance-attributes + """This class describes the projects table, + a projects has an id, a title, a description, + an optional assignment file that can contain more explanation of the projects, + an optional deadline, + the course id of the course to which the project belongs, + visible for students variable so a teacher can decide if the students can see it yet, + archived var so we can implement the archiving functionality, + a test path,script name and regex expressions for automated testing -class ProjectDetail(Resource): + Pylint disable too many instance attributes because we can't reduce the amount + of fields of the model """ - Class for projects/id endpoints - Inherits from flask_restful.Resource class - for implementing get, delete and put methods - """ - - @authorize_project_visible - def get(self, project_id): - """ - Get method for listing a specific project - filtered by id of that specific project - the id fetched from the url with the reaparse - """ - try: - custom_sql_query = f''' - SELECT - ROW_TO_JSON(t) as json_data - FROM ( - SELECT - project_id, - title, - description, - ARRAY_AGG( - jsonb_build_object( - 'deadline_description', d.deadline_description, - 'deadline', to_char(d.deadline, 'YYYY-MM-DD HH24:MI:SS TZ') - ) - ) AS deadlines, - p.course_id, - p.visible_for_students, - p.archived, - p.regex_expressions - FROM - projects p, - unnest(p.deadlines) AS d(deadline_description, deadline) - WHERE - p.project_id = {project_id} - GROUP BY - project_id, - title, - description - ) t; - ''' - - project = db.session.execute(text(custom_sql_query)).fetchone() - - if project: - return { - "data": project[0], - "message": "Project fetched succesfully", - "url": f'{RESPONSE_URL}/{project_id}' - }, 200 - return { - "message": f"Project with {project_id} not found", - "url": f'{RESPONSE_URL}' - }, 404 - except SQLAlchemyError: - db.session.rollback() - return (jsonify({ - "error": "Something went wrong while querying the database", - "url": f"{RESPONSE_URL}/{project_id}" - }), 500) - - @authorize_teacher_or_project_admin - def patch(self, project_id): # pylint: disable=R0914 - """ - Update method for updating a specific project - filtered by id of that specific project - """ - project_json = parse_project_params() - - try: - patch_values = [] - for key, value in project_json.items(): - update = f"{key} = '{value}'" - patch_values.append(update) - - sql_patch = f''' - UPDATE projects SET {', '.join(patch_values)} - WHERE project_id = {project_id} - RETURNING ROW_TO_JSON(projects.*) AS updated_data;''' - project = db.session.execute(text(sql_patch)).fetchone() - if not project: - return (jsonify({ - "error": "Project was not found", - "url": RESPONSE_URL - }), 404) - db.session.commit() - except SQLAlchemyError: - db.session.rollback() - return (jsonify({ - "error": "Something went wrong while updating the project", - "url": RESPONSE_URL - }, 500)) - - if "assignment_file" in request.files: - file = request.files["assignment_file"] - filename = os.path.basename(file.filename) - project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{project_id}") - os.makedirs(project_upload_directory, exist_ok=True) - try: - # remove the old file - try: - to_rem_files = os.listdir(project_upload_directory) - for to_rem_file in to_rem_files: - to_rem_file_path = os.path.join(project_upload_directory, to_rem_file) - if os.path.isfile(to_rem_file_path): - os.remove(to_rem_file_path) - except FileNotFoundError: - db.session.rollback() - return ({ - "message": "Something went wrong deleting the old project files", - "url": f"{API_URL}/projects/{project_id}" - }) - - # removed all files now upload the new files - file.save(os.path.join(project_upload_directory, filename)) - zip_location = os.path.join(project_upload_directory, filename) - with zipfile.ZipFile(zip_location) as upload_zip: - upload_zip.extractall(project_upload_directory) - - except zipfile.BadZipfile: - db.session.rollback() - return ({ - "message": - "Please provide a valid .zip file for updating the instructions", - "url": f"{API_URL}/projects/{project_id}" - }, - 400) - - return (jsonify({ - "message": "Project patched succesfully", - "data": project[0] - }), 200) - @authorize_teacher_of_project - def delete(self, project_id): - """ - Delete a project and all of its submissions in cascade - done by project id - """ - try: - delete_query = f''' - DELETE FROM projects WHERE project_id = {project_id} RETURNING project_id; - ''' - deleted_project = db.session.execute(text(delete_query)).fetchone() - if deleted_project is None: - return (jsonify( - {"message": f"Project with {project_id} doesn't exist", - "url": RESPONSE_URL - }, 404)) - db.session.commit() - return (jsonify({"message": "Resource deleted successfully", - "url": RESPONSE_URL}, 200)) - except SQLAlchemyError: - db.session.rollback() - return {"error": "Something went wrong deleting", - "url": RESPONSE_URL}, 500 \ No newline at end of file + __tablename__ = "projects" + project_id: int = Column(Integer, primary_key=True) + title: str = Column(String(50), nullable=False, unique=False) + description: str = Column(Text, nullable=False) + deadlines: list = Column( + ARRAY( + CompositeType( + "deadline", + [ + Column("deadline_description", Text), + Column("deadline", DateTime(timezone=True)) + ] + ), + dimensions=1 + ) + ) + course_id: int = Column(Integer, ForeignKey("courses.course_id"), nullable=False) + visible_for_students: bool = Column(Boolean, nullable=False) + archived: bool = Column(Boolean, nullable=False) + regex_expressions: list[str] = Column(ARRAY(String(50))) diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index 401de3d0..0f061316 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -2,6 +2,7 @@ import tempfile import os +import json from datetime import datetime from zoneinfo import ZoneInfo import pytest @@ -15,7 +16,6 @@ from project import create_app_with_db from project.db_in import url, db from project.models.submission import Submission, SubmissionStatus -from project.models.project import Project ### AUTHENTICATION & AUTHORIZATION ### @fixture @@ -39,9 +39,11 @@ def valid_submission(valid_user_entry, valid_project_entry): """ Returns a valid submission form """ + data, _ = valid_project_entry + return { "uid": valid_user_entry.uid, - "project_id": valid_project_entry.project_id, + "project_id": data['project_id'], "grading": 16, "submission_time": datetime(2024,3,14,12,0,0,tzinfo=ZoneInfo("GMT")), "submission_path": "/submission/1", @@ -176,25 +178,33 @@ def course_ad(course_teacher_ad: User): return ad2 @pytest.fixture -def valid_project_entry(session, valid_project): +def valid_project_entry(session, client, valid_project): """A project for testing, with the course as the course it belongs to""" - project = Project(**valid_project) - - session.add(project) - session.commit() - return project + valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) + + with open("tests/resources/testzip.zip", "rb") as zip_file: + valid_project["assignment_file"] = zip_file + # post the project + response = client.post( + "/projects", + data=valid_project, + content_type='multipart/form-data', headers={"Authorization": "teacher2"} + ) + return response.json['data'], response.status_code @pytest.fixture def valid_project(valid_course_entry): """A function that return the json form data of a project""" + data = { "title": "Project", "description": "Test project", - "deadlines": [{"deadline": "2024-02-25T12:00:00", "description": "Deadline 1"}], + "deadlines": {"deadline": "2024-02-25T12:00:00", "description": "Deadline 1"}, + "deadlines": {"deadline": "2024-02-25T12:40:00", "description": "Deadline 2"}, "course_id": valid_course_entry.course_id, "visible_for_students": True, "archived": False, - "regex_expressions": ["*.pdf", "*.txt"] + "regex_expressions": "*.pdf" } return data diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 510e24ce..616a2ef5 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,24 +1,13 @@ """Tests for project endpoints.""" -import json - -def test_assignment_download(client, valid_project): +def test_assignment_download(client, valid_project, valid_project_entry): """ Method for assignment download """ - valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) - with open("tests/resources/testzip.zip", "rb") as zip_file: - valid_project["assignment_file"] = zip_file - # post the project - response = client.post( - "/projects", - data=valid_project, - content_type='multipart/form-data', - headers={"Authorization":"teacher2"} - ) - assert response.status_code == 201 - project_id = response.json["data"]["project_id"] + data, status_code = valid_project_entry + assert status_code == 201 + project_id = data["project_id"] response = client.get(f"/projects/{project_id}/assignment", headers={"Authorization":"teacher2"}) # 404 because the file is not found, no assignment.md in zip file @@ -48,31 +37,23 @@ def test_getting_all_projects(client): assert isinstance(response.json['data'], list) -def test_post_project(client, valid_project): +def test_post_project(client, valid_project_entry): """Test posting a project to the database and testing if it's present""" - valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) - with open("tests/resources/testzip.zip", "rb") as zip_file: - valid_project["assignment_file"] = zip_file - # post the project - response = client.post( - "/projects", - data=valid_project, - content_type='multipart/form-data', headers={"Authorization":"teacher2"} - ) + data, status_code = valid_project_entry - assert response.status_code == 201 + assert status_code == 201 # check if the project with the id is present - project_id = response.json["data"]["project_id"] + project_id = data["project_id"] response = client.get(f"/projects/{project_id}", headers={"Authorization":"teacher2"}) assert response.status_code == 200 def test_remove_project(client, valid_project_entry): """Test removing a project to the datab and fetching it, testing if it's not present anymore""" - - project_id = valid_project_entry.project_id + data, _ = valid_project_entry + project_id = data["project_id"] response = client.delete(f"/projects/{project_id}", headers={"Authorization":"teacher2"}) assert response.status_code == 200 @@ -82,11 +63,12 @@ def test_remove_project(client, valid_project_entry): def test_patch_project(client, valid_project_entry): """Test functionality of the PATCH method for projects""" + data, _ = valid_project_entry - project_id = valid_project_entry.project_id + project_id = data['project_id'] - new_title = valid_project_entry.title + "hallo" - new_archived = not valid_project_entry.archived + new_title = data['title'] + "hallo" + new_archived = not data['archived'] response = client.patch(f"/projects/{project_id}", json={ "title": new_title, "archived": new_archived From 32991c7a6e7d46789d95f5417b3bbbbb019875f6 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Fri, 12 Apr 2024 13:16:45 +0200 Subject: [PATCH 09/29] linter --- backend/project/endpoints/projects/endpoint_parser.py | 2 +- backend/project/endpoints/projects/project_detail.py | 2 +- backend/project/endpoints/projects/projects.py | 4 ++-- backend/tests/endpoints/conftest.py | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index 4703d17b..866b1636 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -62,4 +62,4 @@ def parse_project_params(): result_dict[key] = new_deadlines else: result_dict[key] = value - return result_dict \ No newline at end of file + return result_dict diff --git a/backend/project/endpoints/projects/project_detail.py b/backend/project/endpoints/projects/project_detail.py index 9b7a931c..ad1868a2 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -181,4 +181,4 @@ def delete(self, project_id): except SQLAlchemyError: db.session.rollback() return {"error": "Something went wrong deleting", - "url": RESPONSE_URL}, 500 \ No newline at end of file + "url": RESPONSE_URL}, 500 diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 2fb0c8a6..574a589c 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -26,7 +26,7 @@ class ProjectsEndpoint(Resource): for implementing get method """ - # @authorize_teacher + @authorize_teacher def get(self, teacher_id=None): """ Get method for listing all available projects @@ -142,4 +142,4 @@ def post(self, teacher_id=None): "message": "Project created succesfully", "data": new_project, "url": f"{API_URL}/projects/{new_project['project_id']}" - }, 201 \ No newline at end of file + }, 201 diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index 0f061316..c2eca9ba 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -196,6 +196,8 @@ def valid_project_entry(session, client, valid_project): def valid_project(valid_course_entry): """A function that return the json form data of a project""" + # disables because multiform datatype requires multiple keys + # pylint: disable=duplicate-key data = { "title": "Project", "description": "Test project", From 0dcb46e0c828a3c69ec9328e2c597f459aee0dd4 Mon Sep 17 00:00:00 2001 From: gerwoud Date: Fri, 12 Apr 2024 13:27:18 +0200 Subject: [PATCH 10/29] pr review --- backend/project/endpoints/projects/projects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 574a589c..145ddba9 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -57,12 +57,13 @@ def get(self, teacher_id=None): ''' projects = db.session.execute(text(custom_sql_query)) projects_array = [] + # disables because pylinter says it's not iterable while the alchemySQL type is iterable for project in projects: # pylint: disable=E1133 projects_array.append(project[0]) respone = { "data": projects_array, - "messsage": "Recourses fetched succesfully", + "messsage": "Recources fetched succesfully", "url": response_url } return jsonify(respone), 200 @@ -81,7 +82,7 @@ def post(self, teacher_id=None): project_json = parse_project_params() filename = None - # project_json["deadlines"] = json.dumps(project_json["deadlines"]) + if "assignment_file" in request.files: file = request.files["assignment_file"] filename = os.path.basename(file.filename) From e5451b6cdd166f1c4632edddff9b4d9385f137cd Mon Sep 17 00:00:00 2001 From: warre Date: Sat, 13 Apr 2024 11:50:52 +0200 Subject: [PATCH 11/29] Project card refactor --- frontend/src/pages/home/Home_student.tsx | 98 +++---------------- .../projectDeadline/ProjectDeadline.tsx | 34 +++++++ .../projectDeadline/ProjectDeadlineCard.tsx | 51 ++++++++++ 3 files changed, 97 insertions(+), 86 deletions(-) create mode 100644 frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx create mode 100644 frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index e1fe0ce8..e3eede07 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -1,57 +1,22 @@ import { useTranslation } from "react-i18next"; -import {Card, CardContent, Typography, Grid, Container, Badge, Box} from '@mui/material'; +import {Card, CardContent, Typography, Grid, Container, Badge} from '@mui/material'; import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; import {DayCalendarSkeleton, LocalizationProvider} from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { CardActionArea } from '@mui/material'; -import {Link } from "react-router-dom"; - import React, {useEffect, useState} from 'react'; import dayjs, {Dayjs} from "dayjs"; import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; +import {ProjectDeadlineCard} from "../project/projectDeadline/ProjectDeadlineCard.tsx"; +import {ProjectDeadline, ShortSubmission} from "../project/projectDeadline/ProjectDeadline.tsx"; -interface ShortSubmission { - submission_id:number, - submission_time:Date, - submission_status:string -} -interface Deadline { - description: string; - deadline: Date; -} -interface Project { - project_id:string , - title :string, - description:string, - assignment_file:string, - deadlines:Deadline[], - course_id:number, - visible_for_students:boolean, - archived:boolean, - test_path:string, - script_name:string, - regex_expressions:string[], - short_submission: ShortSubmission, - course:Course - -} -interface Course { - course_id: string; - name: string; - teacher: string; - ufora_id: string; -} const apiUrl = import.meta.env.VITE_APP_API_URL const initialValue = dayjs(Date.now()); interface DeadlineInfoProps { selectedDay: Dayjs; - deadlines: Project[]; -} -interface ProjectCardProps{ - deadlines:Project[], - pred?: (deadline:Deadline) => boolean + deadlines: ProjectDeadline[]; } + type ExtendedPickersDayProps = PickersDayProps & { highlightedDays?: number[] }; /** @@ -76,49 +41,10 @@ const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) = - ) : } + ) : } ); }; -/** - * A clickable display of a project deadline - * @param deadlines - A list of all the deadlines - * @param pred - A predicate to filter the deadlines - * @returns Element - */ -const ProjectCard: React.FC = ({ deadlines, pred = () => true }) => { - const { t } = useTranslation('translation', { keyPrefix: 'student' }); - //list of the corresponding assignment - return ( - - {deadlines.map((project, index) => ( - project.deadlines.filter(pred).map((deadline, ) => ( - - - - - {project.title} - - - {t('course')}: {project.course.name} - - - {t('last_submission')}: {project.short_submission ? - t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} - - - Deadline: {dayjs(deadline.deadline).format('MMMM D, YYYY')} - - - - - )) - - ))} - - ); -}; /** * @@ -150,7 +76,7 @@ function ServerDay(props: PickersDayProps & { highlightedDays?: number[] } const handleMonthChange =( date: Dayjs, - projects:Project[], + projects:ProjectDeadline[], setHighlightedDays: React.Dispatch>, ) => { @@ -169,7 +95,7 @@ const handleMonthChange =( setHighlightedDays(hDays) }; -const fetchProjects = async (setProjects: React.Dispatch>) => { +const fetchProjects = async (setProjects: React.Dispatch>) => { const header = { "Authorization": "teacher2" // todo add true authorization } @@ -177,7 +103,7 @@ const fetchProjects = async (setProjects: React.Dispatch { + const formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:ProjectDeadline) => { console.log("project", item) const project_id:string = item.project_id.split("/")[1]// todo check if this does not change later @@ -233,7 +159,7 @@ const fetchProjects = async (setProjects: React.Dispatch([]); + const [projects, setProjects] = useState([]); const [highlightedDays, setHighlightedDays] = React.useState([]); @@ -258,14 +184,14 @@ export default function HomeStudent() { {t('myProjects')} - (dayjs(dayjs()).isBefore(d.deadline))} deadlines={projects} /> + (dayjs(dayjs()).isBefore(d.deadline))} deadlines={projects} /> {t('deadlines')} - dayjs(dayjs()).isAfter(d.deadline)} deadlines={projects} /> + dayjs(dayjs()).isAfter(d.deadline)} deadlines={projects} /> diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx new file mode 100644 index 00000000..e20eeac9 --- /dev/null +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx @@ -0,0 +1,34 @@ +export interface ProjectDeadline { + project_id:string , + title :string, + description:string, + assignment_file:string, + deadline:Deadline, + course_id:number, + visible_for_students:boolean, + archived:boolean, + test_path:string, + script_name:string, + regex_expressions:string[], + short_submission: ShortSubmission, + course:Course + +} +export interface Deadline { + description: string; + deadline: Date; +} + +export interface Course { + course_id: string; + name: string; + teacher: string; + ufora_id: string; +} +export interface ShortSubmission { + submission_id:number, + submission_time:Date, + submission_status:string +} + + diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx new file mode 100644 index 00000000..b4288ea1 --- /dev/null +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx @@ -0,0 +1,51 @@ +import { CardActionArea,Card, CardContent, Typography,Box } from '@mui/material'; +import {Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import dayjs from "dayjs"; +import {ProjectDeadline, Deadline} from "./ProjectDeadline.tsx"; +import React from "react"; + +interface ProjectCardProps{ + deadlines:ProjectDeadline[], + pred?: (deadline:Deadline) => boolean +} + +/** + * A clickable display of a project deadline + * @param deadlines - A list of all the deadlines + * @param pred - A predicate to filter the deadlines + * @returns Element + */ +export const ProjectDeadlineCard: React.FC = ({ deadlines }) => { + const { t } = useTranslation('translation', { keyPrefix: 'student' }); + //list of the corresponding assignment + return ( + + {deadlines.map((project, index) => ( + + + + + + {project.title} + + + {t('course')}: {project.course.name} + + + {t('last_submission')}: {project.short_submission ? + t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} + + + Deadline: {dayjs(project.deadline.deadline).format('MMMM D, YYYY')} + + + + + )) + + } + + ); +}; \ No newline at end of file From b6343746dffd11ae728000b619dcaf55510e2b11 Mon Sep 17 00:00:00 2001 From: warre Date: Sat, 13 Apr 2024 12:25:03 +0200 Subject: [PATCH 12/29] homepage change fix --- frontend/src/pages/home/Home_student.tsx | 67 +++++++++++-------- .../projectDeadline/ProjectDeadline.tsx | 16 +++++ 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index e3eede07..8960f03a 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -7,7 +7,7 @@ import React, {useEffect, useState} from 'react'; import dayjs, {Dayjs} from "dayjs"; import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; import {ProjectDeadlineCard} from "../project/projectDeadline/ProjectDeadlineCard.tsx"; -import {ProjectDeadline, ShortSubmission} from "../project/projectDeadline/ProjectDeadline.tsx"; +import {ProjectDeadline, ShortSubmission, Project} from "../project/projectDeadline/ProjectDeadline.tsx"; const apiUrl = import.meta.env.VITE_APP_API_URL const initialValue = dayjs(Date.now()); @@ -28,7 +28,7 @@ type ExtendedPickersDayProps = PickersDayProps & { highlightedDays?: numb const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) => { const { t } = useTranslation('translation', { keyPrefix: 'student' }); const deadlinesOnSelectedDay = deadlines.filter( - deadline => ( deadline.deadlines.map(d => dayjs(d.deadline).isSame(selectedDay, 'day'))) + project => (dayjs(project.deadline.deadline).isSame(selectedDay, 'day')) ); //list of the corresponding assignment return ( @@ -84,11 +84,9 @@ const handleMonthChange =( // projects are now only fetched on page load const hDays:number[] = [] projects.map((project, ) => { - project.deadlines.map((deadline,) => { - if(deadline.deadline.getMonth() == date.month() && deadline.deadline.getFullYear() == date.year()){ - hDays.push(deadline.deadline.getDate()) - } - }) + if(project.deadline.deadline.getMonth() == date.month() && project.deadline.deadline.getFullYear() == date.year()){ + hDays.push(project.deadline.deadline.getDate()) + } } ); @@ -103,11 +101,9 @@ const fetchProjects = async (setProjects: React.Dispatch { + const formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:Project) => { console.log("project", item) - const project_id:string = item.project_id.split("/")[1]// todo check if this does not change later - - const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${project_id}`), { + const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${item.project_id}`), { headers: header })).json() @@ -119,7 +115,7 @@ const fetchProjects = async (setProjects: React.Dispatch b.submission_time.getTime() - a.submission_time.getTime())[0]; // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${apiUrl}/${item.project_id}`), { //todo ! + const project_item = await (await fetch(encodeURI(`${apiUrl}/projects/${item.project_id}`), { headers:header })).json() @@ -133,21 +129,24 @@ const fetchProjects = async (setProjects: React.Dispatch { + return { + project_id: Number(item.project_id), + title: item.title, + description: item.description, + assignment_file: item.assignment_file, + deadline: d, + course_id: Number(item.course_id), + visible_for_students: Boolean(item.visible_for_students), + archived: Boolean(item.archived), + test_path: item.test_path, + script_name: item.script_name, + regex_expressions: item.regex_expressions, + short_submission: latest_submission, + course: course + } + }) + })); setProjects(formattedData); return formattedData } @@ -184,14 +183,24 @@ export default function HomeStudent() { {t('myProjects')} - (dayjs(dayjs()).isBefore(d.deadline))} deadlines={projects} /> + (dayjs(dayjs()).isBefore(p.deadline.deadline))) + .sort((a, b) => dayjs(a.deadline.deadline).diff(dayjs(b.deadline.deadline))) + .slice(0, 3) // only show the first 3 + } /> {t('deadlines')} - dayjs(dayjs()).isAfter(d.deadline)} deadlines={projects} /> + dayjs(dayjs()).isAfter(p.deadline.deadline)) + .sort((a, b) => dayjs(b.deadline.deadline).diff(dayjs(a.deadline.deadline))) + .slice(0, 3) // only show the first 3 + } /> diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx index e20eeac9..42b63ef5 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx @@ -13,6 +13,22 @@ export interface ProjectDeadline { short_submission: ShortSubmission, course:Course +} +export interface Project { + project_id:string , + title :string, + description:string, + assignment_file:string, + deadlines:Deadline[], + course_id:number, + visible_for_students:boolean, + archived:boolean, + test_path:string, + script_name:string, + regex_expressions:string[], + short_submission: ShortSubmission, + course:Course + } export interface Deadline { description: string; From bc5976645174f41418d29e2d394ee81f9fd5e237 Mon Sep 17 00:00:00 2001 From: warre Date: Sat, 13 Apr 2024 13:12:20 +0200 Subject: [PATCH 13/29] Revert "Merge remote-tracking branch 'origin/backend/projectendpoint-fix' into frontend/feature/homepage" This reverts commit 645443a0aca84b31465aee99e59eda91cf8dadc4, reversing changes made to e5451b6cdd166f1c4632edddff9b4d9385f137cd. --- .../endpoints/projects/endpoint_parser.py | 34 ++--- .../endpoints/projects/project_detail.py | 125 ++++-------------- .../project/endpoints/projects/projects.py | 105 ++++----------- backend/project/models/project.py | 20 ++- backend/tests/endpoints/conftest.py | 32 ++--- backend/tests/endpoints/project_test.py | 46 +++++-- 6 files changed, 121 insertions(+), 241 deletions(-) diff --git a/backend/project/endpoints/projects/endpoint_parser.py b/backend/project/endpoints/projects/endpoint_parser.py index 866b1636..f4ab93ea 100644 --- a/backend/project/endpoints/projects/endpoint_parser.py +++ b/backend/project/endpoints/projects/endpoint_parser.py @@ -1,5 +1,5 @@ """ -Endpoint parser for the projects arguments +Parser for the argument when posting or patching a project """ import json @@ -15,13 +15,7 @@ help='Projects assignment file', location="form" ) -parser.add_argument( - 'deadlines', - type=str, - help='Projects deadlines', - location="form", - action='append' -) +parser.add_argument('deadlines', type=str, help='Projects deadlines', location="form") parser.add_argument("course_id", type=str, help='Projects course_id', location="form") parser.add_argument( "visible_for_students", @@ -34,8 +28,7 @@ "regex_expressions", type=str, help='Projects regex expressions', - location="form", - action="append" + location="form" ) @@ -47,19 +40,18 @@ def parse_project_params(): result_dict = {} for key, value in args.items(): if value is not None: - if key in ('deadlines', 'regex_expressions'): + if "deadlines" == key: + deadlines_parsed = json.loads(value) new_deadlines = [] - for entry in args[key]: - if key == "deadlines": - parsed_deadline = json.loads(entry) - test = { - "description": parsed_deadline["description"], - "deadline": parsed_deadline["deadline"] - } - new_deadlines.append(test) - else: - new_deadlines.append(entry) + for deadline in deadlines_parsed: + new_deadlines.append( + ( + deadline["description"], + deadline["deadline"] + ) + ) result_dict[key] = new_deadlines else: result_dict[key] = value + return result_dict diff --git a/backend/project/endpoints/projects/project_detail.py b/backend/project/endpoints/projects/project_detail.py index ad1868a2..d2affa57 100644 --- a/backend/project/endpoints/projects/project_detail.py +++ b/backend/project/endpoints/projects/project_detail.py @@ -6,13 +6,15 @@ import os import zipfile from urllib.parse import urljoin -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.sql import text -from flask import request, jsonify +from flask import request from flask_restful import Resource from project.db_in import db + +from project.models.project import Project +from project.utils.query_agent import query_by_id_from_model, delete_by_id_from_model, \ + patch_by_id_from_model from project.utils.authentication import authorize_teacher_or_project_admin, \ authorize_teacher_of_project, authorize_project_visible @@ -37,87 +39,30 @@ def get(self, project_id): filtered by id of that specific project the id fetched from the url with the reaparse """ - try: - custom_sql_query = f''' - SELECT - ROW_TO_JSON(t) as json_data - FROM ( - SELECT - project_id, - title, - description, - ARRAY_AGG( - jsonb_build_object( - 'deadline_description', d.deadline_description, - 'deadline', to_char(d.deadline, 'YYYY-MM-DD HH24:MI:SS TZ') - ) - ) AS deadlines, - p.course_id, - p.visible_for_students, - p.archived, - p.regex_expressions - FROM - projects p, - unnest(p.deadlines) AS d(deadline_description, deadline) - WHERE - p.project_id = {project_id} - GROUP BY - project_id, - title, - description - ) t; - ''' - - project = db.session.execute(text(custom_sql_query)).fetchone() - - if project: - return { - "data": project[0], - "message": "Project fetched succesfully", - "url": f'{RESPONSE_URL}/{project_id}' - }, 200 - return { - "message": f"Project with {project_id} not found", - "url": f'{RESPONSE_URL}' - }, 404 - except SQLAlchemyError: - db.session.rollback() - return (jsonify({ - "error": "Something went wrong while querying the database", - "url": f"{RESPONSE_URL}/{project_id}" - }), 500) + + return query_by_id_from_model( + Project, + "project_id", + project_id, + RESPONSE_URL) @authorize_teacher_or_project_admin - def patch(self, project_id): # pylint: disable=R0914 + def patch(self, project_id): """ Update method for updating a specific project filtered by id of that specific project """ project_json = parse_project_params() - try: - patch_values = [] - for key, value in project_json.items(): - update = f"{key} = '{value}'" - patch_values.append(update) - - sql_patch = f''' - UPDATE projects SET {', '.join(patch_values)} - WHERE project_id = {project_id} - RETURNING ROW_TO_JSON(projects.*) AS updated_data;''' - project = db.session.execute(text(sql_patch)).fetchone() - if not project: - return (jsonify({ - "error": "Project was not found", - "url": RESPONSE_URL - }), 404) - db.session.commit() - except SQLAlchemyError: - db.session.rollback() - return (jsonify({ - "error": "Something went wrong while updating the project", - "url": RESPONSE_URL - }, 500)) + output, status_code = patch_by_id_from_model( + Project, + "project_id", + project_id, + RESPONSE_URL, + project_json + ) + if status_code != 200: + return output, status_code if "assignment_file" in request.files: file = request.files["assignment_file"] @@ -154,10 +99,7 @@ def patch(self, project_id): # pylint: disable=R0914 }, 400) - return (jsonify({ - "message": "Project patched succesfully", - "data": project[0] - }), 200) + return output, status_code @authorize_teacher_of_project def delete(self, project_id): @@ -165,20 +107,9 @@ def delete(self, project_id): Delete a project and all of its submissions in cascade done by project id """ - try: - delete_query = f''' - DELETE FROM projects WHERE project_id = {project_id} RETURNING project_id; - ''' - deleted_project = db.session.execute(text(delete_query)).fetchone() - if deleted_project is None: - return (jsonify( - {"message": f"Project with {project_id} doesn't exist", - "url": RESPONSE_URL - }, 404)) - db.session.commit() - return (jsonify({"message": "Resource deleted successfully", - "url": RESPONSE_URL}, 200)) - except SQLAlchemyError: - db.session.rollback() - return {"error": "Something went wrong deleting", - "url": RESPONSE_URL}, 500 + + return delete_by_id_from_model( + Project, + "project_id", + project_id, + RESPONSE_URL) diff --git a/backend/project/endpoints/projects/projects.py b/backend/project/endpoints/projects/projects.py index 145ddba9..01873379 100644 --- a/backend/project/endpoints/projects/projects.py +++ b/backend/project/endpoints/projects/projects.py @@ -5,12 +5,14 @@ from urllib.parse import urljoin import zipfile from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.sql import text from flask import request, jsonify from flask_restful import Resource from project.db_in import db + +from project.models.project import Project +from project.utils.query_agent import query_selected_from_model, create_model_instance from project.utils.authentication import authorize_teacher from project.endpoints.projects.endpoint_parser import parse_project_params @@ -33,44 +35,13 @@ def get(self, teacher_id=None): that are currently in the API """ response_url = urljoin(API_URL, "projects") - - try: - custom_sql_query = ''' - SELECT - jsonb_build_object( - 'project_id', project_id, - 'title', title, - 'description', description, - 'deadlines', ARRAY_AGG( - jsonb_build_object( - 'deadline_description', d.deadline_description, - 'deadline', to_char(d.deadline, 'YYYY-MM-DD HH24:MI:SS TZ') - ) - ) - ) AS result_tuple - FROM - projects p - JOIN - unnest(p.deadlines) AS d(deadline_description, deadline) ON true - GROUP BY - project_id, title, description; - ''' - projects = db.session.execute(text(custom_sql_query)) - projects_array = [] - # disables because pylinter says it's not iterable while the alchemySQL type is iterable - for project in projects: # pylint: disable=E1133 - projects_array.append(project[0]) - - respone = { - "data": projects_array, - "messsage": "Recources fetched succesfully", - "url": response_url - } - return jsonify(respone), 200 - - except SQLAlchemyError: - return {"error": "Something went wrong while querying the database.", - "url": API_URL}, 500 + return query_selected_from_model( + Project, + response_url, + select_values=["project_id", "title", "description", "deadlines"], + url_mapper={"project_id": response_url}, + filters=request.args + ) @authorize_teacher def post(self, teacher_id=None): @@ -78,52 +49,34 @@ def post(self, teacher_id=None): Post functionality for project using flask_restfull parse lib """ - file = request.files["assignment_file"] project_json = parse_project_params() filename = None - if "assignment_file" in request.files: file = request.files["assignment_file"] filename = os.path.basename(file.filename) + # save the file that is given with the request try: - sql_insert = f''' - INSERT - INTO projects - (title, description, deadlines, course_id, visible_for_students, archived, regex_expressions) - VALUES ('{project_json["title"]}', '{project_json["description"]}', ARRAY[''' - - # Add deadlines - for deadline in project_json["deadlines"]: - sql_insert += f'''ROW('{deadline["description"]}', '{deadline["deadline"]}')''' - if deadline != project_json["deadlines"][-1]: - sql_insert += ',' - - sql_insert += f''' - ]::deadline[], - {project_json["course_id"]}, {project_json["visible_for_students"]}, - {project_json["archived"]}, ARRAY[''' - - # Add regex expressions - for regex in project_json["regex_expressions"]: - sql_insert += f'''\'{regex}\'''' - if regex != project_json["regex_expressions"][-1]: - sql_insert += ',' - - sql_insert += ''']) RETURNING ROW_TO_JSON(projects.*) AS insterted_data;''' - - sql_statement = text(sql_insert) - query_result = db.session.execute(sql_statement) - db.session.commit() - new_project = query_result.fetchone()[0] + new_project, status_code = create_model_instance( + Project, + project_json, + urljoin(f"{API_URL}/", "/projects"), + required_fields=[ + "title", + "description", + "course_id", + "visible_for_students", + "archived"] + ) except SQLAlchemyError: - db.session.rollback() - return (jsonify({ - "message": "Something went wrong in the database", - "url": f"{API_URL}/projects",}), 500) + return jsonify({"error": "Something went wrong while inserting into the database.", + "url": f"{API_URL}/projects"}), 500 + + if status_code == 400: + return new_project, status_code - project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project['project_id']}") + project_upload_directory = os.path.join(f"{UPLOAD_FOLDER}", f"{new_project.project_id}") os.makedirs(project_upload_directory, exist_ok=True) if filename is not None: try: @@ -142,5 +95,5 @@ def post(self, teacher_id=None): return { "message": "Project created succesfully", "data": new_project, - "url": f"{API_URL}/projects/{new_project['project_id']}" + "url": f"{API_URL}/projects/{new_project.project_id}" }, 201 diff --git a/backend/project/models/project.py b/backend/project/models/project.py index dd5ada03..624f9ed0 100644 --- a/backend/project/models/project.py +++ b/backend/project/models/project.py @@ -5,7 +5,6 @@ from sqlalchemy_utils import CompositeType from project.db_in import db - @dataclass class Project(db.Model): # pylint: disable=too-many-instance-attributes """This class describes the projects table, @@ -25,16 +24,15 @@ class Project(db.Model): # pylint: disable=too-many-instance-attributes project_id: int = Column(Integer, primary_key=True) title: str = Column(String(50), nullable=False, unique=False) description: str = Column(Text, nullable=False) - deadlines: list = Column( - ARRAY( - CompositeType( - "deadline", - [ - Column("deadline_description", Text), - Column("deadline", DateTime(timezone=True)) - ] - ), - dimensions=1 + deadlines: list = Column(ARRAY( + CompositeType( + "deadline", + [ + Column("description", Text), + Column("deadline", DateTime(timezone=True)) + ] + ), + dimensions=1 ) ) course_id: int = Column(Integer, ForeignKey("courses.course_id"), nullable=False) diff --git a/backend/tests/endpoints/conftest.py b/backend/tests/endpoints/conftest.py index c2eca9ba..401de3d0 100644 --- a/backend/tests/endpoints/conftest.py +++ b/backend/tests/endpoints/conftest.py @@ -2,7 +2,6 @@ import tempfile import os -import json from datetime import datetime from zoneinfo import ZoneInfo import pytest @@ -16,6 +15,7 @@ from project import create_app_with_db from project.db_in import url, db from project.models.submission import Submission, SubmissionStatus +from project.models.project import Project ### AUTHENTICATION & AUTHORIZATION ### @fixture @@ -39,11 +39,9 @@ def valid_submission(valid_user_entry, valid_project_entry): """ Returns a valid submission form """ - data, _ = valid_project_entry - return { "uid": valid_user_entry.uid, - "project_id": data['project_id'], + "project_id": valid_project_entry.project_id, "grading": 16, "submission_time": datetime(2024,3,14,12,0,0,tzinfo=ZoneInfo("GMT")), "submission_path": "/submission/1", @@ -178,35 +176,25 @@ def course_ad(course_teacher_ad: User): return ad2 @pytest.fixture -def valid_project_entry(session, client, valid_project): +def valid_project_entry(session, valid_project): """A project for testing, with the course as the course it belongs to""" - valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) - - with open("tests/resources/testzip.zip", "rb") as zip_file: - valid_project["assignment_file"] = zip_file - # post the project - response = client.post( - "/projects", - data=valid_project, - content_type='multipart/form-data', headers={"Authorization": "teacher2"} - ) - return response.json['data'], response.status_code + project = Project(**valid_project) + + session.add(project) + session.commit() + return project @pytest.fixture def valid_project(valid_course_entry): """A function that return the json form data of a project""" - - # disables because multiform datatype requires multiple keys - # pylint: disable=duplicate-key data = { "title": "Project", "description": "Test project", - "deadlines": {"deadline": "2024-02-25T12:00:00", "description": "Deadline 1"}, - "deadlines": {"deadline": "2024-02-25T12:40:00", "description": "Deadline 2"}, + "deadlines": [{"deadline": "2024-02-25T12:00:00", "description": "Deadline 1"}], "course_id": valid_course_entry.course_id, "visible_for_students": True, "archived": False, - "regex_expressions": "*.pdf" + "regex_expressions": ["*.pdf", "*.txt"] } return data diff --git a/backend/tests/endpoints/project_test.py b/backend/tests/endpoints/project_test.py index 616a2ef5..510e24ce 100644 --- a/backend/tests/endpoints/project_test.py +++ b/backend/tests/endpoints/project_test.py @@ -1,13 +1,24 @@ """Tests for project endpoints.""" -def test_assignment_download(client, valid_project, valid_project_entry): +import json + +def test_assignment_download(client, valid_project): """ Method for assignment download """ - data, status_code = valid_project_entry - assert status_code == 201 - project_id = data["project_id"] + valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) + with open("tests/resources/testzip.zip", "rb") as zip_file: + valid_project["assignment_file"] = zip_file + # post the project + response = client.post( + "/projects", + data=valid_project, + content_type='multipart/form-data', + headers={"Authorization":"teacher2"} + ) + assert response.status_code == 201 + project_id = response.json["data"]["project_id"] response = client.get(f"/projects/{project_id}/assignment", headers={"Authorization":"teacher2"}) # 404 because the file is not found, no assignment.md in zip file @@ -37,23 +48,31 @@ def test_getting_all_projects(client): assert isinstance(response.json['data'], list) -def test_post_project(client, valid_project_entry): +def test_post_project(client, valid_project): """Test posting a project to the database and testing if it's present""" - data, status_code = valid_project_entry + valid_project["deadlines"] = json.dumps(valid_project["deadlines"]) + with open("tests/resources/testzip.zip", "rb") as zip_file: + valid_project["assignment_file"] = zip_file + # post the project + response = client.post( + "/projects", + data=valid_project, + content_type='multipart/form-data', headers={"Authorization":"teacher2"} + ) - assert status_code == 201 + assert response.status_code == 201 # check if the project with the id is present - project_id = data["project_id"] + project_id = response.json["data"]["project_id"] response = client.get(f"/projects/{project_id}", headers={"Authorization":"teacher2"}) assert response.status_code == 200 def test_remove_project(client, valid_project_entry): """Test removing a project to the datab and fetching it, testing if it's not present anymore""" - data, _ = valid_project_entry - project_id = data["project_id"] + + project_id = valid_project_entry.project_id response = client.delete(f"/projects/{project_id}", headers={"Authorization":"teacher2"}) assert response.status_code == 200 @@ -63,12 +82,11 @@ def test_remove_project(client, valid_project_entry): def test_patch_project(client, valid_project_entry): """Test functionality of the PATCH method for projects""" - data, _ = valid_project_entry - project_id = data['project_id'] + project_id = valid_project_entry.project_id - new_title = data['title'] + "hallo" - new_archived = not data['archived'] + new_title = valid_project_entry.title + "hallo" + new_archived = not valid_project_entry.archived response = client.patch(f"/projects/{project_id}", json={ "title": new_title, "archived": new_archived From b76dec9ee7c5b030ee122544355509cc418b0431 Mon Sep 17 00:00:00 2001 From: warre Date: Sat, 13 Apr 2024 15:43:21 +0200 Subject: [PATCH 14/29] homepage changes --- frontend/src/pages/home/Home_student.tsx | 30 ++++++++++--------- .../projectDeadline/ProjectDeadline.tsx | 7 ++--- .../projectDeadline/ProjectDeadlineCard.tsx | 23 ++++++++++++-- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index 8960f03a..b54c9a77 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -28,7 +28,7 @@ type ExtendedPickersDayProps = PickersDayProps & { highlightedDays?: numb const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) => { const { t } = useTranslation('translation', { keyPrefix: 'student' }); const deadlinesOnSelectedDay = deadlines.filter( - project => (dayjs(project.deadline.deadline).isSame(selectedDay, 'day')) + project => (dayjs(project.deadline).isSame(selectedDay, 'day')) ); //list of the corresponding assignment return ( @@ -84,8 +84,8 @@ const handleMonthChange =( // projects are now only fetched on page load const hDays:number[] = [] projects.map((project, ) => { - if(project.deadline.deadline.getMonth() == date.month() && project.deadline.deadline.getFullYear() == date.year()){ - hDays.push(project.deadline.deadline.getDate()) + if(project.deadline.getMonth() == date.month() && project.deadline.getFullYear() == date.year()){ + hDays.push(project.deadline.getDate()) } } @@ -101,9 +101,9 @@ const fetchProjects = async (setProjects: React.Dispatch { - console.log("project", item) - const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${item.project_id}`), { + let formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:Project) => { + const project_id = item.project_id.split('/')[1] + const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${project_id}`), { headers: header })).json() @@ -115,7 +115,7 @@ const fetchProjects = async (setProjects: React.Dispatch b.submission_time.getTime() - a.submission_time.getTime())[0]; // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${apiUrl}/projects/${item.project_id}`), { + const project_item = await (await fetch(encodeURI(`${apiUrl}/${item.project_id}`), { headers:header })).json() @@ -129,13 +129,14 @@ const fetchProjects = async (setProjects: React.Dispatch { + return item.deadlines.map((d:string[]) => { return { - project_id: Number(item.project_id), + project_id: item.project_id, title: item.title, description: item.description, assignment_file: item.assignment_file, - deadline: d, + deadline: new Date(d[1]), + deadline_description: d[0], course_id: Number(item.course_id), visible_for_students: Boolean(item.visible_for_students), archived: Boolean(item.archived), @@ -147,6 +148,7 @@ const fetchProjects = async (setProjects: React.Dispatch (dayjs(dayjs()).isBefore(p.deadline.deadline))) - .sort((a, b) => dayjs(a.deadline.deadline).diff(dayjs(b.deadline.deadline))) + .filter((p) => (dayjs(dayjs()).isBefore(p.deadline))) + .sort((a, b) => dayjs(a.deadline).diff(dayjs(b.deadline))) .slice(0, 3) // only show the first 3 } /> @@ -197,8 +199,8 @@ export default function HomeStudent() { dayjs(dayjs()).isAfter(p.deadline.deadline)) - .sort((a, b) => dayjs(b.deadline.deadline).diff(dayjs(a.deadline.deadline))) + .filter((p) => dayjs(dayjs()).isAfter(p.deadline)) + .sort((a, b) => dayjs(b.deadline).diff(dayjs(a.deadline))) .slice(0, 3) // only show the first 3 } /> diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx index 42b63ef5..bf94ed83 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx @@ -3,7 +3,8 @@ export interface ProjectDeadline { title :string, description:string, assignment_file:string, - deadline:Deadline, + deadline:Date, + deadline_description:string, course_id:number, visible_for_students:boolean, archived:boolean, @@ -19,7 +20,7 @@ export interface Project { title :string, description:string, assignment_file:string, - deadlines:Deadline[], + deadlines:string[][], course_id:number, visible_for_students:boolean, archived:boolean, @@ -46,5 +47,3 @@ export interface ShortSubmission { submission_time:Date, submission_status:string } - - diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx index b4288ea1..ddad355f 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx @@ -1,9 +1,10 @@ -import { CardActionArea,Card, CardContent, Typography,Box } from '@mui/material'; +import {CardActionArea, Card, CardContent, Typography, Box, Button} from '@mui/material'; import {Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import dayjs from "dayjs"; import {ProjectDeadline, Deadline} from "./ProjectDeadline.tsx"; import React from "react"; +import { useNavigate } from 'react-router-dom'; interface ProjectCardProps{ deadlines:ProjectDeadline[], @@ -18,6 +19,8 @@ interface ProjectCardProps{ */ export const ProjectDeadlineCard: React.FC = ({ deadlines }) => { const { t } = useTranslation('translation', { keyPrefix: 'student' }); + const navigate = useNavigate(); + //list of the corresponding assignment return ( @@ -31,14 +34,28 @@ export const ProjectDeadlineCard: React.FC = ({ deadlines }) {project.title} - {t('course')}: {project.course.name} + {t('course')}: + {t('last_submission')}: {project.short_submission ? t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} - Deadline: {dayjs(project.deadline.deadline).format('MMMM D, YYYY')} + Deadline: {dayjs(project.deadline).format('MMMM D, YYYY')} From 7b0e99c75bf07a9e8f99d8f6b78b4c57ac33f9e4 Mon Sep 17 00:00:00 2001 From: warre Date: Sat, 13 Apr 2024 15:46:57 +0200 Subject: [PATCH 15/29] rm comment --- frontend/src/pages/home/Home.tsx | 1 - .../src/pages/project/projectDeadline/ProjectDeadlineCard.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index be629648..1d7006e0 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -8,7 +8,6 @@ import {Link } from "react-router-dom"; */ export default function Home() { const { t } = useTranslation('translation', { keyPrefix: 'home' }); - //console.log("log env", process.env.REACT_APP_LOGIN_LINK) const login_redirect:string =import.meta.env.VITE_LOGIN_LINK return ( diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx index ddad355f..da450643 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx @@ -65,4 +65,4 @@ export const ProjectDeadlineCard: React.FC = ({ deadlines }) } ); -}; \ No newline at end of file +}; From 96b4f65e43f8bc615c703788ede4e843090b4e79 Mon Sep 17 00:00:00 2001 From: warre Date: Sat, 13 Apr 2024 16:19:22 +0200 Subject: [PATCH 16/29] pr changes --- frontend/public/locales/en/translation.json | 5 +---- frontend/public/locales/nl/translation.json | 7 ++----- frontend/src/pages/home/Home_student.tsx | 2 +- .../pages/project/projectDeadline/ProjectDeadlineCard.tsx | 5 +++-- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 628c2f88..95eed5fd 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -1,7 +1,4 @@ { - "home": { - "title": "Homepage" - }, "header": { "myProjects": "My Projects", "myCourses": "My Courses", @@ -12,7 +9,7 @@ }, "home": { "homepage": "Homepage", - "welcomeDescription": "Welcome to Perister贸nas the online submission platform of UGent", + "welcomeDescription": "Welcome to Perister贸nas, the online submission platform of UGent", "login": "Login" }, "courseForm": { diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index c4e0c119..5e487a6c 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -1,7 +1,4 @@ { - "home": { - "title": "Homepagina" - }, "header": { "myProjects": "Mijn Projecten", "myCourses": "Mijn Vakken", @@ -11,8 +8,8 @@ "homepage": "Homepage" }, "home": { - "homepage": "Homepage", - "welcomeDescription": "Welkom bij Perister贸nas het online indieningsplatform van UGent", + "homepage": "Homepagina", + "welcomeDescription": "Welkom bij Perister贸nas, het online indieningsplatform van UGent", "login": "Aanmelden" }, "courseForm": { diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/Home_student.tsx index b54c9a77..2a44cb1e 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/Home_student.tsx @@ -95,7 +95,7 @@ const handleMonthChange =( }; const fetchProjects = async (setProjects: React.Dispatch>) => { const header = { - "Authorization": "teacher2" // todo add true authorization + "Authorization": "teacher2" } const response = await fetch(`${apiUrl}/projects`, { headers:header diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx index da450643..4e5da9a5 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx @@ -19,6 +19,7 @@ interface ProjectCardProps{ */ export const ProjectDeadlineCard: React.FC = ({ deadlines }) => { const { t } = useTranslation('translation', { keyPrefix: 'student' }); + const { i18n } = useTranslation(); const navigate = useNavigate(); //list of the corresponding assignment @@ -27,7 +28,7 @@ export const ProjectDeadlineCard: React.FC = ({ deadlines }) {deadlines.map((project, index) => ( - + @@ -44,7 +45,7 @@ export const ProjectDeadlineCard: React.FC = ({ deadlines }) onClick={(event) => { event.stopPropagation(); // stops the event from reaching CardActionArea event.preventDefault(); - navigate(`/courses/${project.course.course_id}`) + navigate(`/${i18n.language}/courses/${project.course.course_id}`) }} > {project.course.name} From 48956fa22a22c15dfa4fab1c4fd93f9d0aeb5152 Mon Sep 17 00:00:00 2001 From: warre Date: Sun, 14 Apr 2024 12:11:38 +0200 Subject: [PATCH 17/29] pr changes --- frontend/public/locales/en/translation.json | 4 +- frontend/public/locales/nl/translation.json | 4 +- frontend/public/logo_app.png | Bin 0 -> 704 bytes frontend/src/App.tsx | 2 +- .../{Home_student.tsx => HomeStudent.tsx} | 117 ++++++++++-------- 5 files changed, 72 insertions(+), 55 deletions(-) create mode 100644 frontend/public/logo_app.png rename frontend/src/pages/home/{Home_student.tsx => HomeStudent.tsx} (69%) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 95eed5fd..2402e40e 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -50,6 +50,8 @@ "FAIL": "Fail", "deadlinesOnDay": "Deadlines on: ", "noDeadline": "No deadlines", - "no_submission_yet" : "No submission yet" + "no_submission_yet" : "No submission yet", + "loading": "Loading...", + "no_projects": "There are no projects." } } diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 5e487a6c..42cc3850 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -27,7 +27,9 @@ "FAIL": "Gefaald", "deadlinesOnDay": "Deadlines op: ", "noDeadline": "Geen deadlines", - "no_submission_yet" : "Nog geen indiening" + "no_submission_yet" : "Nog geen indiening", + "loading": "Laden...", + "no_projects": "Er zijn geen projecten." }, "projectView": { diff --git a/frontend/public/logo_app.png b/frontend/public/logo_app.png new file mode 100644 index 0000000000000000000000000000000000000000..7a36d43ced7a9641bec3ff2ec4c0607d9bcb853d GIT binary patch literal 704 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&i3a$DxcX!k>UaGFp-wRB`f1Sh zvr;Q*CQt!eNswPKLx@J(-EF4xl_vp3I14-?iy0W0Uw|;<*6N^a1_mZoPZ!6KiaBqi zPER^)z+s&52Z*Qs`ExQ~t(EE5Jci^ejBB<#eR-9s^8T}X zmwTfP=MBBaxN43skMG#dD_MCVAzpUH#RKP=e%(7eVNK(`6B+q>n$!6oS#clfn^?yo za_Zi``z-r4ewvwVV6YPkD3rZ8v7zg=2jBUGMh07xz|o?mm&`=EZU!T<63wTyF*&yQl> z=`lZqd0$5TG=_ay^+Eip4R(v$t&YgYG0SbVcVy_j#Qr_t*iYr`1BbsIzt&*$rO{MC zzWz$-k;QMh*D>yV)W5p%$4lj`19iWhvknM`8xMMU8L8I5TQRq31-C+gk{KG66*m(xRL?`a0!C*=|c8oyQZEbypuXH}Ut zf8n>=9|Tkmf8w4t-}!d>HO`Xn7XQV%k0x#T-&I@Nx3#`vw!hL%+5M|c`(kYEPtUZe zxOw-coM?WLR9a!axy2bF?T6po|9@Dam(co+?V#EPImrb=8Q*yVIJZ=@8T>o;faTUS zM!m+e(+f%(tMr-ANbGYibWUJS6wc^9$hl?OPK`>=jDL_we|Cg_-ImzD?Fp7yz*NMS f8sVAd>&u`8WOD#92wV!D45B<;{an^LB{Ts5F^D&V literal 0 HcmV?d00001 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d15ae316..e1a91785 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; -import HomeStudent from "./pages/home/Home_student.tsx"; +import HomeStudent from "./pages/home/HomeStudent.tsx"; import Home from "./pages/home/Home.tsx" import { Header } from "./components/Header/Header"; import LanguagePath from "./components/LanguagePath"; diff --git a/frontend/src/pages/home/Home_student.tsx b/frontend/src/pages/home/HomeStudent.tsx similarity index 69% rename from frontend/src/pages/home/Home_student.tsx rename to frontend/src/pages/home/HomeStudent.tsx index 2a44cb1e..f49a2096 100644 --- a/frontend/src/pages/home/Home_student.tsx +++ b/frontend/src/pages/home/HomeStudent.tsx @@ -9,7 +9,7 @@ import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; import {ProjectDeadlineCard} from "../project/projectDeadline/ProjectDeadlineCard.tsx"; import {ProjectDeadline, ShortSubmission, Project} from "../project/projectDeadline/ProjectDeadline.tsx"; -const apiUrl = import.meta.env.VITE_APP_API_URL +const API_URL = import.meta.env.VITE_APP_API_URL const initialValue = dayjs(Date.now()); interface DeadlineInfoProps { @@ -97,13 +97,13 @@ const fetchProjects = async (setProjects: React.Dispatch { const project_id = item.project_id.split('/')[1] - const response_submissions = await (await fetch(encodeURI(`${apiUrl}/submissions?&project_id=${project_id}`), { + const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?&project_id=${project_id}`), { headers: header })).json() @@ -115,12 +115,12 @@ const fetchProjects = async (setProjects: React.Dispatch b.submission_time.getTime() - a.submission_time.getTime())[0]; // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${apiUrl}/${item.project_id}`), { + const project_item = await (await fetch(encodeURI(`${API_URL}/${item.project_id}`), { headers:header })).json() //fetch the course - const response_courses = await (await fetch(encodeURI(`${apiUrl}/courses/${project_item.data.course_id}`), { + const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { headers: header })).json() const course = { @@ -165,10 +165,12 @@ export default function HomeStudent() { const [highlightedDays, setHighlightedDays] = React.useState([]); const [selectedDay, setSelectedDay] = useState(dayjs(Date.now())); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetchProjects(setProjects).then(p => { handleMonthChange(initialValue, p,setHighlightedDays) + setIsLoading(false) }) }, []); @@ -180,58 +182,69 @@ export default function HomeStudent() { return ( - - - {t('myProjects')} + {isLoading ? ( + + {t('loading')} - - (dayjs(dayjs()).isBefore(p.deadline))) - .sort((a, b) => dayjs(a.deadline).diff(dayjs(b.deadline))) - .slice(0, 3) // only show the first 3 - } /> - - - - - {t('deadlines')} + ) : projects.length === 0 ? ( + + {t('no_projects')} - dayjs(dayjs()).isAfter(p.deadline)) - .sort((a, b) => dayjs(b.deadline).diff(dayjs(a.deadline))) - .slice(0, 3) // only show the first 3 - } /> - - - - - {handleMonthChange(date, projects, - setHighlightedDays)}} - onChange={handleDaySelect} - renderLoading={() => } - slots={{ - day: ServerDay, - }} - slotProps={{ - day: { - highlightedDays, - } as ExtendedPickersDayProps, - }} - /> - - + ) : ( + <> + - {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} + {t('myProjects')} - - - - + (dayjs(dayjs()).isBefore(p.deadline))) + .sort((a, b) => dayjs(a.deadline).diff(dayjs(b.deadline))) + .slice(0, 3) // only show the first 3 + } /> + + + + + {t('deadlines')} + + dayjs(dayjs()).isAfter(p.deadline)) + .sort((a, b) => dayjs(b.deadline).diff(dayjs(a.deadline))) + .slice(0, 3) // only show the first 3 + } /> + + + + + { handleMonthChange(date, projects, setHighlightedDays) }} + onChange={handleDaySelect} + renderLoading={() => } + slots={{ + day: ServerDay, + }} + slotProps={{ + day: { + highlightedDays, + } as ExtendedPickersDayProps, + }} + /> + + + + {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} + + + + + + + + )}
); From 4e2d4f6bd7944dd53697ad9d49b0b37188715d74 Mon Sep 17 00:00:00 2001 From: warre Date: Sun, 14 Apr 2024 16:07:32 +0200 Subject: [PATCH 18/29] added support for no deadline projects --- frontend/public/locales/en/translation.json | 2 +- frontend/public/locales/nl/translation.json | 2 +- frontend/src/pages/home/HomeStudent.tsx | 181 +++++++++++------- .../projectDeadline/ProjectDeadline.tsx | 2 +- .../projectDeadline/ProjectDeadlineCard.tsx | 7 +- 5 files changed, 121 insertions(+), 73 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 2402e40e..b025df0f 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -52,6 +52,6 @@ "noDeadline": "No deadlines", "no_submission_yet" : "No submission yet", "loading": "Loading...", - "no_projects": "There are no projects." + "no_projects": "There are no projects here." } } diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json index 42cc3850..cea9e75d 100644 --- a/frontend/public/locales/nl/translation.json +++ b/frontend/public/locales/nl/translation.json @@ -29,7 +29,7 @@ "noDeadline": "Geen deadlines", "no_submission_yet" : "Nog geen indiening", "loading": "Laden...", - "no_projects": "Er zijn geen projecten." + "no_projects": "Er zijn hier geen projecten." }, "projectView": { diff --git a/frontend/src/pages/home/HomeStudent.tsx b/frontend/src/pages/home/HomeStudent.tsx index f49a2096..ee2393a6 100644 --- a/frontend/src/pages/home/HomeStudent.tsx +++ b/frontend/src/pages/home/HomeStudent.tsx @@ -28,7 +28,7 @@ type ExtendedPickersDayProps = PickersDayProps & { highlightedDays?: numb const DeadlineInfo: React.FC = ({ selectedDay, deadlines }) => { const { t } = useTranslation('translation', { keyPrefix: 'student' }); const deadlinesOnSelectedDay = deadlines.filter( - project => (dayjs(project.deadline).isSame(selectedDay, 'day')) + project => (project.deadline && dayjs(project.deadline).isSame(selectedDay, 'day')) ); //list of the corresponding assignment return ( @@ -84,7 +84,7 @@ const handleMonthChange =( // projects are now only fetched on page load const hDays:number[] = [] projects.map((project, ) => { - if(project.deadline.getMonth() == date.month() && project.deadline.getFullYear() == date.year()){ + if(project.deadline && project.deadline.getMonth() == date.month() && project.deadline.getFullYear() == date.year()){ hDays.push(project.deadline.getDate()) } @@ -97,46 +97,68 @@ const fetchProjects = async (setProjects: React.Dispatch { - const project_id = item.project_id.split('/')[1] - const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?&project_id=${project_id}`), { - headers: header - })).json() - - //get the latest submission - const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ - submission_id: submission.submission_id,//this is the path - submission_time: new Date(submission.submission_time), - submission_status: submission.submission_status - } - )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; - // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${API_URL}/${item.project_id}`), { + try{ + const response = await fetch(`${API_URL}/projects`, { headers:header - })).json() - - //fetch the course - const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { - headers: header - })).json() - const course = { - course_id: response_courses.data.course_id, - name: response_courses.data.name, - teacher: response_courses.data.teacher, - ufora_id: response_courses.data.ufora_id - } - return item.deadlines.map((d:string[]) => { - return { + }) + const jsonData = await response.json(); + let formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:Project) => { + const url_split = item.project_id.split('/') + const project_id = url_split[url_split.length -1] + const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?project_id=${project_id}`), { + headers: header + })).json() + + //get the latest submission + const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ + submission_id: submission.submission_id,//this is the path + submission_time: new Date(submission.submission_time), + submission_status: submission.submission_status + } + )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; + // fetch the course id of the project + const project_item = await (await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { + headers:header + })).json() + + //fetch the course + const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { + headers: header + })).json() + const course = { + course_id: response_courses.data.course_id, + name: response_courses.data.name, + teacher: response_courses.data.teacher, + ufora_id: response_courses.data.ufora_id + } + if(item.deadlines){ + return item.deadlines.map((d:string[]) => { + return { + project_id: item.project_id, + title: item.title, + description: item.description, + assignment_file: item.assignment_file, + deadline: new Date(d[1]), + deadline_description: d[0], + course_id: Number(item.course_id), + visible_for_students: Boolean(item.visible_for_students), + archived: Boolean(item.archived), + test_path: item.test_path, + script_name: item.script_name, + regex_expressions: item.regex_expressions, + short_submission: latest_submission, + course: course + } + }) + } + // contains no dealine: + return [{ project_id: item.project_id, title: item.title, description: item.description, assignment_file: item.assignment_file, - deadline: new Date(d[1]), - deadline_description: d[0], + deadline: undefined, + deadline_description: undefined, course_id: Number(item.course_id), visible_for_students: Boolean(item.visible_for_students), archived: Boolean(item.archived), @@ -145,12 +167,16 @@ const fetchProjects = async (setProjects: React.Dispatch { setSelectedDay(day); }; + const futureProjects = projects + .filter((p) => (p.deadline && dayjs(dayjs()).isBefore(p.deadline))) + .sort((a, b) => dayjs(a.deadline).diff(dayjs(b.deadline))) + .slice(0, 3) // only show the first 3 + const pastDeadlines = projects + .filter((p) => p.deadline && (dayjs()).isAfter(p.deadline)) + .sort((a, b) => dayjs(b.deadline).diff(dayjs(a.deadline))) + .slice(0, 3) // only show the first 3 + const noDeadlineProject = projects.filter((p) => p.deadline === undefined) return ( @@ -186,36 +221,46 @@ export default function HomeStudent() { {t('loading')} - ) : projects.length === 0 ? ( - - {t('no_projects')} - - ) : ( + ): ( <> - - {t('myProjects')} - - - (dayjs(dayjs()).isBefore(p.deadline))) - .sort((a, b) => dayjs(a.deadline).diff(dayjs(b.deadline))) - .slice(0, 3) // only show the first 3 - } /> - + + + + {t('myProjects')} + + {futureProjects.length + noDeadlineProject.length > 0? ( + <> + + + + ) : ( + + {t('no_projects')} + + )} + + + - - {t('deadlines')} - - dayjs(dayjs()).isAfter(p.deadline)) - .sort((a, b) => dayjs(b.deadline).diff(dayjs(a.deadline))) - .slice(0, 3) // only show the first 3 - } /> + + + + + {t('deadlines')} + + {pastDeadlines.length > 0 ? ( + + ) : ( + + {t('no_projects')} + + )} + + + diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx index bf94ed83..50f16a60 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadline.tsx @@ -3,7 +3,7 @@ export interface ProjectDeadline { title :string, description:string, assignment_file:string, - deadline:Date, + deadline:Date|undefined, deadline_description:string, course_id:number, visible_for_students:boolean, diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx index 4e5da9a5..7fe1d9e9 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx @@ -55,9 +55,12 @@ export const ProjectDeadlineCard: React.FC = ({ deadlines }) {t('last_submission')}: {project.short_submission ? t(project.short_submission.submission_status.toString()) : t('no_submission_yet')} - + {project.deadline && ( + Deadline: {dayjs(project.deadline).format('MMMM D, YYYY')} - + + )} + From 2c2654f2842fc467cc9017c4e56085a419de0a41 Mon Sep 17 00:00:00 2001 From: warre Date: Sun, 14 Apr 2024 16:40:50 +0200 Subject: [PATCH 19/29] link changed --- .../src/pages/project/projectDeadline/ProjectDeadlineCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx index 7fe1d9e9..56d2351d 100644 --- a/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx +++ b/frontend/src/pages/project/projectDeadline/ProjectDeadlineCard.tsx @@ -28,7 +28,7 @@ export const ProjectDeadlineCard: React.FC = ({ deadlines }) {deadlines.map((project, index) => ( - + From 18bce9dc4b5c3b7f018c7d1bf23c138305e12836 Mon Sep 17 00:00:00 2001 From: warre Date: Sun, 14 Apr 2024 16:41:59 +0200 Subject: [PATCH 20/29] link changed --- frontend/src/pages/home/HomeStudent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/home/HomeStudent.tsx b/frontend/src/pages/home/HomeStudent.tsx index ee2393a6..d672a6d0 100644 --- a/frontend/src/pages/home/HomeStudent.tsx +++ b/frontend/src/pages/home/HomeStudent.tsx @@ -134,7 +134,7 @@ const fetchProjects = async (setProjects: React.Dispatch { return { - project_id: item.project_id, + project_id: project_id, title: item.title, description: item.description, assignment_file: item.assignment_file, @@ -153,7 +153,7 @@ const fetchProjects = async (setProjects: React.Dispatch Date: Tue, 16 Apr 2024 19:14:26 +0200 Subject: [PATCH 21/29] pr changes --- frontend/src/App.tsx | 3 +- frontend/src/pages/home/HomeStudent.tsx | 242 ++++++------------- frontend/src/pages/project/FetchProjects.tsx | 92 +++++++ 3 files changed, 163 insertions(+), 174 deletions(-) create mode 100644 frontend/src/pages/project/FetchProjects.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 430a99c4..81b49145 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,17 +4,18 @@ import Home from "./pages/home/Home"; import HomeStudent from "./pages/home/HomeStudent.tsx"; import LanguagePath from "./components/LanguagePath"; import ProjectView from "./pages/project/projectView/ProjectView"; +import {fetchProjects} from "./pages/project/FetchProjects.tsx"; const router = createBrowserRouter( createRoutesFromElements( }> } /> }> + } loader={fetchProjects}/> } /> }/> - } /> ) diff --git a/frontend/src/pages/home/HomeStudent.tsx b/frontend/src/pages/home/HomeStudent.tsx index d672a6d0..ddbb57d8 100644 --- a/frontend/src/pages/home/HomeStudent.tsx +++ b/frontend/src/pages/home/HomeStudent.tsx @@ -3,14 +3,12 @@ import {Card, CardContent, Typography, Grid, Container, Badge} from '@mui/materi import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; import {DayCalendarSkeleton, LocalizationProvider} from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import dayjs, {Dayjs} from "dayjs"; import { PickersDay, PickersDayProps } from '@mui/x-date-pickers/PickersDay'; import {ProjectDeadlineCard} from "../project/projectDeadline/ProjectDeadlineCard.tsx"; -import {ProjectDeadline, ShortSubmission, Project} from "../project/projectDeadline/ProjectDeadline.tsx"; - -const API_URL = import.meta.env.VITE_APP_API_URL -const initialValue = dayjs(Date.now()); +import {ProjectDeadline} from "../project/projectDeadline/ProjectDeadline.tsx"; +import {useLoaderData} from "react-router-dom"; interface DeadlineInfoProps { selectedDay: Dayjs; @@ -93,91 +91,6 @@ const handleMonthChange =( setHighlightedDays(hDays) }; -const fetchProjects = async (setProjects: React.Dispatch>) => { - const header = { - "Authorization": "teacher2" - } - try{ - const response = await fetch(`${API_URL}/projects`, { - headers:header - }) - const jsonData = await response.json(); - let formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:Project) => { - const url_split = item.project_id.split('/') - const project_id = url_split[url_split.length -1] - const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?project_id=${project_id}`), { - headers: header - })).json() - - //get the latest submission - const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ - submission_id: submission.submission_id,//this is the path - submission_time: new Date(submission.submission_time), - submission_status: submission.submission_status - } - )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; - // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { - headers:header - })).json() - - //fetch the course - const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { - headers: header - })).json() - const course = { - course_id: response_courses.data.course_id, - name: response_courses.data.name, - teacher: response_courses.data.teacher, - ufora_id: response_courses.data.ufora_id - } - if(item.deadlines){ - return item.deadlines.map((d:string[]) => { - return { - project_id: project_id, - title: item.title, - description: item.description, - assignment_file: item.assignment_file, - deadline: new Date(d[1]), - deadline_description: d[0], - course_id: Number(item.course_id), - visible_for_students: Boolean(item.visible_for_students), - archived: Boolean(item.archived), - test_path: item.test_path, - script_name: item.script_name, - regex_expressions: item.regex_expressions, - short_submission: latest_submission, - course: course - } - }) - } - // contains no dealine: - return [{ - project_id: project_id, - title: item.title, - description: item.description, - assignment_file: item.assignment_file, - deadline: undefined, - deadline_description: undefined, - course_id: Number(item.course_id), - visible_for_students: Boolean(item.visible_for_students), - archived: Boolean(item.archived), - test_path: item.test_path, - script_name: item.script_name, - regex_expressions: item.regex_expressions, - short_submission: latest_submission, - course: course - }] - - })); - formattedData = formattedData.flat() - setProjects(formattedData); - return formattedData - } catch (e) { - console.error("A server error occurred"); - return [] - } -} /** * This component is the home page component that will be rendered when on the index route. @@ -186,20 +99,10 @@ const fetchProjects = async (setProjects: React.Dispatch([]); - const [highlightedDays, setHighlightedDays] = React.useState([]); const [selectedDay, setSelectedDay] = useState(dayjs(Date.now())); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - fetchProjects(setProjects).then(p => { - handleMonthChange(initialValue, p,setHighlightedDays) - setIsLoading(false) - }) - }, []); - + const projects = useLoaderData() as ProjectDeadline[] // Update selectedDay state when a day is selected const handleDaySelect = (day: Dayjs) => { setSelectedDay(day); @@ -217,79 +120,72 @@ export default function HomeStudent() { return ( - {isLoading ? ( - - {t('loading')} - - ): ( - <> - - - - - {t('myProjects')} - - {futureProjects.length + noDeadlineProject.length > 0? ( - <> - - - - ) : ( - - {t('no_projects')} - - )} - - - - - - - - - - {t('deadlines')} - - {pastDeadlines.length > 0 ? ( - - ) : ( - - {t('no_projects')} - - )} - - - - - - - - { handleMonthChange(date, projects, setHighlightedDays) }} - onChange={handleDaySelect} - renderLoading={() => } - slots={{ - day: ServerDay, - }} - slotProps={{ - day: { - highlightedDays, - } as ExtendedPickersDayProps, - }} - /> - - - - {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} - - - + + + + + {t('myProjects')} + + {futureProjects.length + noDeadlineProject.length > 0? ( + <> + + + + ) : ( + + {t('no_projects')} + + )} + + + + + + + + + + {t('deadlines')} + + {pastDeadlines.length > 0 ? ( + + ) : ( + + {t('no_projects')} + + )} + + + + + + + + { handleMonthChange(date, projects, setHighlightedDays) }} + onChange={handleDaySelect} + renderLoading={() => } + slots={{ + day: ServerDay, + }} + slotProps={{ + day: { + highlightedDays, + } as ExtendedPickersDayProps, + }} + /> + + + + {t('deadlinesOnDay')} {selectedDay.format('MMMM D, YYYY')} + + + + + + - - - - )} ); diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx new file mode 100644 index 00000000..31775157 --- /dev/null +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -0,0 +1,92 @@ +import {Project, ProjectDeadline, ShortSubmission} from "./projectDeadline/ProjectDeadline.tsx"; +const API_URL = import.meta.env.VITE_APP_API_URL + +export const fetchProjects = async () => { + const header = { + "Authorization": "teacher2" + } + try{ + const response = await fetch(`${API_URL}/projects`, { + headers:header + }) + const jsonData = await response.json(); + let formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:Project) => { + try{ + const url_split = item.project_id.split('/') + const project_id = url_split[url_split.length -1] + const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?project_id=${project_id}`), { + headers: header + })).json() + + //get the latest submission + const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ + submission_id: submission.submission_id,//this is the path + submission_time: new Date(submission.submission_time), + submission_status: submission.submission_status + } + )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; + // fetch the course id of the project + const project_item = await (await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { + headers:header + })).json() + + //fetch the course + const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { + headers: header + })).json() + const course = { + course_id: response_courses.data.course_id, + name: response_courses.data.name, + teacher: response_courses.data.teacher, + ufora_id: response_courses.data.ufora_id + } + if(item.deadlines){ + return item.deadlines.map((d:string[]) => { + return { + project_id: project_id, + title: item.title, + description: item.description, + assignment_file: item.assignment_file, + deadline: new Date(d[1]), + deadline_description: d[0], + course_id: Number(item.course_id), + visible_for_students: Boolean(item.visible_for_students), + archived: Boolean(item.archived), + test_path: item.test_path, + script_name: item.script_name, + regex_expressions: item.regex_expressions, + short_submission: latest_submission, + course: course + } + }) + } + // contains no dealine: + return [{ + project_id: project_id, + title: item.title, + description: item.description, + assignment_file: item.assignment_file, + deadline: undefined, + deadline_description: undefined, + course_id: Number(item.course_id), + visible_for_students: Boolean(item.visible_for_students), + archived: Boolean(item.archived), + test_path: item.test_path, + script_name: item.script_name, + regex_expressions: item.regex_expressions, + short_submission: latest_submission, + course: course + }] + + }catch (e){ + return [] + } + } + + )); + formattedData = formattedData.flat() + return formattedData + } catch (e) { + return [] + } +} From 4f07f24c11375a4034436a11f7dcfacd47e0c903 Mon Sep 17 00:00:00 2001 From: warre Date: Wed, 17 Apr 2024 18:56:54 +0200 Subject: [PATCH 22/29] homepage change --- frontend/package-lock.json | 15 +++++++++++++++ frontend/package.json | 4 +++- frontend/src/App.tsx | 9 ++++----- frontend/src/pages/home/HomePages.tsx | 16 ++++++++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 frontend/src/pages/home/HomePages.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 78e8bf0e..a8a24e98 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,10 +16,12 @@ "@mui/styled-engine-sc": "^6.0.0-alpha.16", "@mui/x-data-grid": "^7.1.1", "@mui/x-date-pickers": "^7.1.1", + "@types/js-cookie": "^3.0.6", "axios": "^1.6.8", "dayjs": "^1.11.10", "i18next-browser-languagedetector": "^7.2.0", "i18next-http-backend": "^2.5.0", + "js-cookie": "^3.0.5", "jszip": "^3.10.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1806,6 +1808,11 @@ "@types/unist": "*" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5026,6 +5033,14 @@ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5247a580..f7ed2eb3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,11 +19,13 @@ "@mui/material": "^5.15.10", "@mui/styled-engine-sc": "^6.0.0-alpha.16", "@mui/x-data-grid": "^7.1.1", - "axios": "^1.6.8", "@mui/x-date-pickers": "^7.1.1", + "@types/js-cookie": "^3.0.6", + "axios": "^1.6.8", "dayjs": "^1.11.10", "i18next-browser-languagedetector": "^7.2.0", "i18next-http-backend": "^2.5.0", + "js-cookie": "^3.0.5", "jszip": "^3.10.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 81b49145..6aa013e3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,18 +1,17 @@ import { Route,RouterProvider, createBrowserRouter, createRoutesFromElements } from "react-router-dom"; import Layout from "./components/Header/Layout"; -import Home from "./pages/home/Home"; -import HomeStudent from "./pages/home/HomeStudent.tsx"; + import LanguagePath from "./components/LanguagePath"; import ProjectView from "./pages/project/projectView/ProjectView"; import {fetchProjects} from "./pages/project/FetchProjects.tsx"; +import HomePages from "./pages/home/HomePages.tsx"; const router = createBrowserRouter( createRoutesFromElements( }> - } /> + } loader={fetchProjects}/> }> - } loader={fetchProjects}/> - } /> + } loader={fetchProjects} /> }/> diff --git a/frontend/src/pages/home/HomePages.tsx b/frontend/src/pages/home/HomePages.tsx new file mode 100644 index 00000000..54fb118c --- /dev/null +++ b/frontend/src/pages/home/HomePages.tsx @@ -0,0 +1,16 @@ +import HomeStudent from './HomeStudent'; +import Home from "./Home.tsx"; +import Cookies from 'js-cookie'; + + +export default function HomePages() { + const loginStatus = Cookies.get('login_status'); + + if (loginStatus === 'STUDENT') { + return ; + } /*else if (loginStatus === 'TEACHER') { + return ; + }*/ else { + return ; + } +} \ No newline at end of file From b4f46f39f90b110163ff817e507e2aec339cc693 Mon Sep 17 00:00:00 2001 From: warre Date: Wed, 17 Apr 2024 19:05:41 +0200 Subject: [PATCH 23/29] homepage change --- frontend/src/pages/home/HomePages.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/home/HomePages.tsx b/frontend/src/pages/home/HomePages.tsx index 54fb118c..dad196f0 100644 --- a/frontend/src/pages/home/HomePages.tsx +++ b/frontend/src/pages/home/HomePages.tsx @@ -2,7 +2,10 @@ import HomeStudent from './HomeStudent'; import Home from "./Home.tsx"; import Cookies from 'js-cookie'; - +/** + * Gives the requested home page based on the login status + * @returns - The home page component + */ export default function HomePages() { const loginStatus = Cookies.get('login_status'); @@ -13,4 +16,4 @@ export default function HomePages() { }*/ else { return ; } -} \ No newline at end of file +} From 77f56f4f942bdfe58f1171ffc04ef957c83b9d4f Mon Sep 17 00:00:00 2001 From: warre Date: Thu, 18 Apr 2024 16:53:08 +0200 Subject: [PATCH 24/29] API URL to API_HOST --- frontend/src/pages/project/FetchProjects.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 31775157..49f1b8a1 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -1,5 +1,5 @@ import {Project, ProjectDeadline, ShortSubmission} from "./projectDeadline/ProjectDeadline.tsx"; -const API_URL = import.meta.env.VITE_APP_API_URL +const API_URL = import.meta.env.VITE_APP_API_HOST export const fetchProjects = async () => { const header = { From cf23151b24692f8118d0f87b5d00c1b2b2076573 Mon Sep 17 00:00:00 2001 From: warre Date: Thu, 18 Apr 2024 20:35:07 +0200 Subject: [PATCH 25/29] end point with /me --- frontend/public/{ => img}/logo_app.png | Bin frontend/public/locales/en/translation.json | 2 ++ frontend/src/App.tsx | 9 ++---- frontend/src/pages/home/Home.tsx | 4 +-- .../home/{HomeStudent.tsx => HomePage.tsx} | 9 ++++-- frontend/src/pages/home/HomePages.tsx | 20 +++++++------ frontend/src/pages/project/FetchProjects.tsx | 27 ++++++++++++++++-- 7 files changed, 49 insertions(+), 22 deletions(-) rename frontend/public/{ => img}/logo_app.png (100%) rename frontend/src/pages/home/{HomeStudent.tsx => HomePage.tsx} (97%) diff --git a/frontend/public/logo_app.png b/frontend/public/img/logo_app.png similarity index 100% rename from frontend/public/logo_app.png rename to frontend/public/img/logo_app.png diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 35be07d3..27bcab24 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -10,6 +10,8 @@ "projectUploadForm": "Project upload form" }, "home": { + "home": "Home", + "tag": "en", "homepage": "Homepage", "welcomeDescription": "Welcome to Perister贸nas, the online submission platform of UGent", "login": "Login" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b50777b2..b1038469 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,21 +1,18 @@ import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from "react-router-dom"; import Layout from "./components/Header/Layout"; -import Home from "./pages/home/Home"; import LanguagePath from "./components/LanguagePath"; import ProjectView from "./pages/project/projectView/ProjectView"; import { ErrorBoundary } from "./pages/error/ErrorBoundary.tsx"; import ProjectCreateHome from "./pages/create_project/ProjectCreateHome.tsx"; -import {fetchProjects} from "./pages/project/FetchProjects.tsx"; +import {fetchProjectPage} from "./pages/project/FetchProjects.tsx"; import HomePages from "./pages/home/HomePages.tsx"; const router = createBrowserRouter( createRoutesFromElements( } errorElement={}> - } /> - }> - } loader={fetchProjects}/> + } loader={fetchProjectPage}/> }> - } loader={fetchProjects} /> + } loader={fetchProjectPage} /> }/> diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 1d7006e0..54de654d 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -22,13 +22,13 @@ export default function Home() { gap: 3, }} > - ([]); const [selectedDay, setSelectedDay] = useState(dayjs(Date.now())); - const projects = useLoaderData() as ProjectDeadline[] + const loader = useLoaderData() as { + projects: ProjectDeadline[], + me: string + } + const projects = loader.projects + // Update selectedDay state when a day is selected const handleDaySelect = (day: Dayjs) => { setSelectedDay(day); diff --git a/frontend/src/pages/home/HomePages.tsx b/frontend/src/pages/home/HomePages.tsx index dad196f0..8edd9bc9 100644 --- a/frontend/src/pages/home/HomePages.tsx +++ b/frontend/src/pages/home/HomePages.tsx @@ -1,19 +1,21 @@ -import HomeStudent from './HomeStudent'; +import HomePage from './HomePage.tsx'; import Home from "./Home.tsx"; -import Cookies from 'js-cookie'; +import {useLoaderData} from "react-router-dom"; +import {ProjectDeadline} from "../project/projectDeadline/ProjectDeadline.tsx"; /** * Gives the requested home page based on the login status * @returns - The home page component */ export default function HomePages() { - const loginStatus = Cookies.get('login_status'); - - if (loginStatus === 'STUDENT') { - return ; - } /*else if (loginStatus === 'TEACHER') { - return ; - }*/ else { + const loader = useLoaderData() as { + projects: ProjectDeadline[], + me: string + } + const me = loader.me + if (me === 'LOGGED_IN') { + return ; + } else { return ; } } diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 49f1b8a1..4a5c3cb1 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -1,10 +1,31 @@ import {Project, ProjectDeadline, ShortSubmission} from "./projectDeadline/ProjectDeadline.tsx"; const API_URL = import.meta.env.VITE_APP_API_HOST +const header = { + "Authorization": "teacher2" +} +export const fetchProjectPage = async () => { + const projects = await fetchProjects() + const me = await fetchMe() + return {projects, me} +} -export const fetchProjects = async () => { - const header = { - "Authorization": "teacher2" +export const fetchMe = async () => { + try { + const response = await fetch(`${API_URL}/me`, { + headers:header + }) + if(response.status == 200){ + return "LOGGED_IN" + }else { + return "UNKNOWN" + } + } catch (e){ + return "UNKNOWN" } + +} +export const fetchProjects = async () => { + try{ const response = await fetch(`${API_URL}/projects`, { headers:header From 03d3fbe8d75d47a6ed8c0f65807740c730285b0d Mon Sep 17 00:00:00 2001 From: warre Date: Thu, 18 Apr 2024 20:55:12 +0200 Subject: [PATCH 26/29] home --- frontend/src/pages/home/HomePages.tsx | 6 +++--- frontend/src/pages/project/FetchProjects.tsx | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/home/HomePages.tsx b/frontend/src/pages/home/HomePages.tsx index 8edd9bc9..140be48c 100644 --- a/frontend/src/pages/home/HomePages.tsx +++ b/frontend/src/pages/home/HomePages.tsx @@ -13,9 +13,9 @@ export default function HomePages() { me: string } const me = loader.me - if (me === 'LOGGED_IN') { - return ; - } else { + if (me === 'UNKNOWN') { return ; + } else { + return ; } } diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 4a5c3cb1..e65bff43 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -15,7 +15,8 @@ export const fetchMe = async () => { headers:header }) if(response.status == 200){ - return "LOGGED_IN" + const data = await response.json() + return data.role }else { return "UNKNOWN" } From 94ef7cb567714b7fd3f45cb8a68ee6711326262b Mon Sep 17 00:00:00 2001 From: warre Date: Thu, 18 Apr 2024 21:28:15 +0200 Subject: [PATCH 27/29] deadline fix --- frontend/src/pages/project/FetchProjects.tsx | 23 ++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index e65bff43..987171a5 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -10,6 +10,7 @@ export const fetchProjectPage = async () => { } export const fetchMe = async () => { + return "STUDENT" try { const response = await fetch(`${API_URL}/me`, { headers:header @@ -62,21 +63,21 @@ export const fetchProjects = async () => { teacher: response_courses.data.teacher, ufora_id: response_courses.data.ufora_id } - if(item.deadlines){ - return item.deadlines.map((d:string[]) => { + if(project_item.data.deadlines){ + return project_item.data.deadlines.map((d:string[]) => { return { project_id: project_id, - title: item.title, - description: item.description, - assignment_file: item.assignment_file, + title: project_item.data.title, + description: project_item.data.description, + assignment_file: project_item.data.assignment_file, deadline: new Date(d[1]), deadline_description: d[0], - course_id: Number(item.course_id), - visible_for_students: Boolean(item.visible_for_students), - archived: Boolean(item.archived), - test_path: item.test_path, - script_name: item.script_name, - regex_expressions: item.regex_expressions, + course_id: Number(project_item.data.course_id), + visible_for_students: Boolean(project_item.data.visible_for_students), + archived: Boolean(project_item.data.archived), + test_path: project_item.data.test_path, + script_name: project_item.data.script_name, + regex_expressions: project_item.data.regex_expressions, short_submission: latest_submission, course: course } From 3a686a5cc030a2fb21d1c279457494e24d8e0a36 Mon Sep 17 00:00:00 2001 From: warre Date: Thu, 18 Apr 2024 21:29:10 +0200 Subject: [PATCH 28/29] deadline fix --- frontend/src/pages/project/FetchProjects.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 987171a5..a10f4817 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -10,7 +10,6 @@ export const fetchProjectPage = async () => { } export const fetchMe = async () => { - return "STUDENT" try { const response = await fetch(`${API_URL}/me`, { headers:header From 36fa3d3458fe262e4906a9032ea2474c8760059c Mon Sep 17 00:00:00 2001 From: warre Date: Thu, 18 Apr 2024 21:30:14 +0200 Subject: [PATCH 29/29] deadline fix --- frontend/src/pages/project/FetchProjects.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index a10f4817..1b988825 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -85,17 +85,17 @@ export const fetchProjects = async () => { // contains no dealine: return [{ project_id: project_id, - title: item.title, - description: item.description, - assignment_file: item.assignment_file, + title: project_item.data.title, + description: project_item.data.description, + assignment_file: project_item.data.assignment_file, deadline: undefined, deadline_description: undefined, - course_id: Number(item.course_id), - visible_for_students: Boolean(item.visible_for_students), - archived: Boolean(item.archived), - test_path: item.test_path, - script_name: item.script_name, - regex_expressions: item.regex_expressions, + course_id: Number(project_item.data.course_id), + visible_for_students: Boolean(project_item.data.visible_for_students), + archived: Boolean(project_item.data.archived), + test_path: project_item.data.test_path, + script_name: project_item.data.script_name, + regex_expressions: project_item.data.regex_expressions, short_submission: latest_submission, course: course }]