Skip to content

Commit

Permalink
Project overview (#219)
Browse files Browse the repository at this point in the history
* goeie styling yallah

* pls werk

* werkt hopelijk

* linter

---------

Co-authored-by: Siebe Vlietinck <[email protected]>
  • Loading branch information
Gerwoud and Vucis authored Apr 18, 2024
1 parent 99ac445 commit c5e9fbc
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 33 deletions.
6 changes: 6 additions & 0 deletions backend/project/endpoints/projects/project_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from project.endpoints.projects.project_detail import ProjectDetail
from project.endpoints.projects.project_assignment_file import ProjectAssignmentFiles
from project.endpoints.projects.project_submissions_download import SubmissionDownload
from project.endpoints.projects.project_last_submission import SubmissionPerUser


project_bp = Blueprint('project_endpoint', __name__)
Expand All @@ -32,3 +33,8 @@
'/projects/<int:project_id>/submissions-download',
view_func=SubmissionDownload.as_view('project_submissions')
)

project_bp.add_url_rule(
'/projects/<int:project_id>/latest-per-user',
view_func=SubmissionPerUser.as_view('latest_per_user')
)
24 changes: 24 additions & 0 deletions backend/project/endpoints/projects/project_last_submission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
This module gives the last submission for a project for every user
"""

from os import getenv
from urllib.parse import urljoin
from flask_restful import Resource
from project.endpoints.projects.project_submissions_download import get_last_submissions_per_user

API_HOST = getenv("API_HOST")
UPLOAD_FOLDER = getenv("UPLOAD_FOLDER")
BASE_URL = urljoin(f"{API_HOST}/", "/projects")

class SubmissionPerUser(Resource):
"""
Recourse to get all the submissions for users
"""

def get(self, project_id: int):
"""
Download all submissions for a project as a zip file.
"""

return get_last_submissions_per_user(project_id)
71 changes: 41 additions & 30 deletions backend/project/endpoints/projects/project_submissions_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,43 @@
UPLOAD_FOLDER = getenv("UPLOAD_FOLDER")
BASE_URL = urljoin(f"{API_HOST}/", "/projects")

def get_last_submissions_per_user(project_id):
"""
Get the last submissions per user for a given project
"""
try:
project = Project.query.get(project_id)
except SQLAlchemyError:
return {"message": "Internal server error"}, 500

if project is None:
return {
"message": f"Project (project_id={project_id}) not found",
"url": BASE_URL}, 404

# Define a subquery to find the latest submission times for each user
latest_submissions = db.session.query(
Submission.uid,
func.max(Submission.submission_time).label('max_time')
).filter(
Submission.project_id == project_id,
Submission.submission_status != 'LATE'
).group_by(
Submission.uid
).subquery()

# Use the subquery to fetch the actual submissions
submissions = db.session.query(Submission).join(
latest_submissions,
(Submission.uid == latest_submissions.c.uid) &
(Submission.submission_time == latest_submissions.c.max_time)
).all()

if not submissions:
return {"message": "No submissions found", "url": BASE_URL}, 404

return {"message": "Resource fetched succesfully", "data": submissions}, 200

class SubmissionDownload(Resource):
"""
Resource to download all submissions for a project.
Expand All @@ -27,37 +64,11 @@ def get(self, project_id: int):
"""
Download all submissions for a project as a zip file.
"""
data, status_code = get_last_submissions_per_user(project_id)

try:
project = Project.query.get(project_id)
except SQLAlchemyError:
return {"message": "Internal server error"}, 500

if project is None:
return {
"message": f"Project (project_id={project_id}) not found",
"url": BASE_URL}, 404

# Define a subquery to find the latest submission times for each user
latest_submissions = db.session.query(
Submission.uid,
func.max(Submission.submission_time).label('max_time')
).filter(
Submission.project_id == project_id,
Submission.submission_status != 'LATE'
).group_by(
Submission.uid
).subquery()

# Use the subquery to fetch the actual submissions
submissions = db.session.query(Submission).join(
latest_submissions,
(Submission.uid == latest_submissions.c.uid) &
(Submission.submission_time == latest_submissions.c.max_time)
).all()

if not submissions:
return {"message": "No submissions found", "url": BASE_URL}, 404
if status_code != 200:
return data, status_code
submissions = data["data"]

def zip_directory_stream():
with io.BytesIO() as memory_file:
Expand Down
1 change: 1 addition & 0 deletions backend/project/endpoints/submissions/submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from project.utils.submissions.evaluator import run_evaluator
from project.utils.models.project_utils import get_course_of_project


API_HOST = getenv("API_HOST")
UPLOAD_FOLDER = getenv("UPLOAD_FOLDER")
BASE_URL = urljoin(f"{API_HOST}/", "/submissions")
Expand Down
13 changes: 13 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@mui/x-date-pickers": "^7.1.1",
"axios": "^1.6.8",
"dayjs": "^1.11.10",
"downloadjs": "^1.4.7",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-http-backend": "^2.5.0",
"jszip": "^3.10.1",
Expand All @@ -34,6 +35,7 @@
"styled-components": "^6.1.8"
},
"devDependencies": {
"@types/downloadjs": "^1.4.6",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-router-dom": "^5.3.3",
Expand Down
6 changes: 6 additions & 0 deletions frontend/public/locales/en/submissionOverview.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"submissionOverview": {
"submissionOverviewHeader": "Project status overview",
"downloadButton": "DOWNLOAD ALL PROJECTS"
}
}
6 changes: 6 additions & 0 deletions frontend/public/locales/nl/submissionsOverview.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"submissionOverview": {
"submissionOverviewHeader": "Project status overzicht",
"downloadButton": "DOWNLOAD ALLE PROJECTEN"
}
}
7 changes: 5 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 SubmissionsOverview from "./pages/submission_overview/SubmissionsOverview.tsx";
import {fetchProjectPage} from "./pages/project/FetchProjects.tsx";
import HomePages from "./pages/home/HomePages.tsx";

Expand All @@ -13,8 +14,10 @@ const router = createBrowserRouter(
<Route index element={<HomePages />} loader={fetchProjectPage}/>
<Route path=":lang" element={<LanguagePath/>}>
<Route path="home" element={<HomePages />} loader={fetchProjectPage} />
<Route path="project" >
<Route path=":projectId" element={<ProjectView />}/>
<Route path="project/:projectId/overview" element={<SubmissionsOverview/>}/>
<Route path="project">
<Route path=":projectId" element={<ProjectView />}>
</Route>
</Route>
<Route path="projects">
<Route path="create" element={<ProjectCreateHome />} />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ProjectForm/ProjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ interface RegexData {
regex: string;
}

const apiUrl = import.meta.env.VITE_APP_API_URL
const apiUrl = import.meta.env.VITE_API_HOST
const user = "Gunnar"

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Box, Button, Typography} from "@mui/material";
import {useEffect, useState} from "react";
import {useParams} from "react-router-dom";
import ProjectSubmissionsOverviewDatagrid from "./ProjectSubmissionOverviewDatagrid.tsx";
import download from 'downloadjs';
import {useTranslation} from "react-i18next";
const apiUrl = import.meta.env.VITE_API_HOST
const user = "teacher"

/**
* @returns Overview page for submissions
*/
export default function ProjectSubmissionOverview() {

const { t } = useTranslation('submissionOverview', { keyPrefix: 'submissionOverview' });

useEffect(() => {
fetchProject();
});

const fetchProject = async () => {
const response = await fetch(`${apiUrl}/projects/${projectId}`, {
headers: {
"Authorization": user
},
})
const jsonData = await response.json();
setProjectTitle(jsonData["data"].title);

}

const downloadProjectSubmissions = async () => {
await fetch(`${apiUrl}/projects/${projectId}/submissions-download`, {
headers: {
"Authorization": user
},
})
.then(res => {
return res.blob();
})
.then(blob => {
download(blob, 'submissions.zip');
});
}

const [projectTitle, setProjectTitle] = useState<string>("")
const { projectId } = useParams<{ projectId: string }>();

return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
paddingTop="50px"
>
<Box width="40%">
<Typography minWidth="440px" variant="h6" align="left">{projectTitle}</Typography>
<ProjectSubmissionsOverviewDatagrid />
</Box>
<Button onClick={downloadProjectSubmissions} variant="contained">{t("downloadButton")}</Button>
</Box>
)
}
Loading

0 comments on commit c5e9fbc

Please sign in to comment.