Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Project overview #219

Merged
merged 6 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recourse -> Resource

"""

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
Loading