Skip to content

Commit

Permalink
Download alle indieningen (files) + csv (met indieningen info) in 1 k…
Browse files Browse the repository at this point in the history
…eer als zip (#228)

* backend: implement project zip

* backend: implement csv

* frontend: download all submissions
  • Loading branch information
xerbalind authored May 20, 2024
1 parent fd91c2c commit 4f66dbf
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 8 deletions.
11 changes: 11 additions & 0 deletions backend/src/project/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

from docker import DockerClient
from fastapi import APIRouter, Depends, UploadFile, BackgroundTasks
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession

from src import dependencies
from src.auth.dependencies import authentication_validation
from src.dependencies import get_async_db
from src.group.dependencies import retrieve_groups_by_project
from src.group.schemas import GroupList
from src.project.utils import project_zip_stream
from src.submission.schemas import Submission
from src.submission.service import get_submissions_by_project
from . import service
Expand Down Expand Up @@ -84,6 +87,14 @@ async def list_submissions(project_id: int,
return await get_submissions_by_project(db, project_id)


@router.get("/{project_id}/zip", response_class=StreamingResponse, dependencies=[Depends(patch_permission_validation)])
async def get_submissions_dump(project_id: int, db: AsyncSession = Depends(get_async_db)):
"""Return zip file containing all submission files and csv"""
submissions = await get_submissions_by_project(db, project_id)
data = await project_zip_stream(db, submissions, project_id)
return StreamingResponse(data, media_type="application/zip")


@router.get("/{project_id}/test_files")
async def get_test_files(test_files_uuid: str = Depends(retrieve_test_files_uuid)):
return get_files_from_dir(tests_path(test_files_uuid))
Expand Down
45 changes: 45 additions & 0 deletions backend/src/project/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

from sqlalchemy.ext.asyncio import AsyncSession
from src.docker_tests.utils import submission_path
import pathlib
import zipfile
import csv
import io

from src.submission.models import Status
from src.submission.schemas import Submission
from typing import Sequence

from src.group.service import get_group_by_id
from src.group.exceptions import GroupNotFound


async def project_zip_stream(db: AsyncSession, submissions: Sequence[Submission], project_id):
data = io.BytesIO()
csvdata = io.StringIO()
writer = csv.DictWriter(csvdata, fieldnames=[
"project", "group", "date", "status", "remarks", "stdout", "stderr"])
writer.writeheader()

with zipfile.ZipFile(data, mode='w') as z:

for submission in submissions:
path = pathlib.Path(submission_path(submission.files_uuid, ""))

group = await get_group_by_id(db, submission.group_id)

if not group:
raise GroupNotFound()

writer.writerow({'project': submission.project_id, 'group': group.num, 'date': submission.date, 'status': Status(
submission.status).name, 'remarks': submission.remarks, 'stdout': submission.stdout, 'stderr': submission.stderr})

for f_name in path.iterdir():
name = f"project_{
project_id}/group_{group.num}/{str(f_name).replace(str(path), "")}"
z.write(f_name, arcname=name)

z.writestr(f"project_{project_id}/submissions.csv", csvdata.getvalue())

data.seek(0)
return data
6 changes: 3 additions & 3 deletions backend/src/submission/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ async def get_file(path: str, submission: Submission = Depends(retrieve_submissi


@router.get("/{submission_id}/zip", response_class=StreamingResponse)
async def get_all_files(submission: Submission = Depends(retrieve_submission)):
path = submission_path(submission.files_uuid, "")
return StreamingResponse(zip_stream(path, submission.group_id), media_type="application/zip")
async def get_all_files(submission: Submission = Depends(retrieve_submission), db: AsyncSession = Depends(get_async_db)):
data = await zip_stream(db, submission)
return StreamingResponse(data, media_type="application/zip")


@router.get("/{submission_id}/artifacts", response_model=list[File])
Expand Down
21 changes: 16 additions & 5 deletions backend/src/submission/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
from uuid import uuid4

from fastapi import UploadFile
from sqlalchemy.ext.asyncio import AsyncSession

from src.docker_tests.utils import submission_path, submissions_path
from src.docker_tests.utils import submissions_path
from src.project.schemas import Project
from src.submission.exceptions import UnMetRequirements
from src.submission.schemas import Submission

from src.group.service import get_group_by_id
from src.group.exceptions import GroupNotFound
from ..docker_tests.utils import submission_path, get_files_from_dir, artifacts_path


def upload_files(files: list[UploadFile], project: Project) -> str:
Expand Down Expand Up @@ -47,15 +53,20 @@ def upload_files(files: list[UploadFile], project: Project) -> str:
return uuid


def zip_stream(path, group_id: int):
base_path = pathlib.Path(path)
async def zip_stream(db: AsyncSession, submission: Submission):
base_path = pathlib.Path(submission_path(submission.files_uuid))
group = await get_group_by_id(db, submission.group_id)

if not group:
raise GroupNotFound()

data = io.BytesIO()
with zipfile.ZipFile(data, mode='w') as z:
for f_name in base_path.iterdir():
name = f"group_{group_id}/{str(f_name).replace(path, "")}"
name = f"group_{group.num}/{str(f_name).replace(str(base_path), "")}"
z.write(f_name, arcname=name)
data.seek(0)
yield from data
return data


def remove_files(uuid: str):
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default {
edit: "Edit project",
submissions_list: "All submissions",
submissions_list_teacher: "All submissions for this project",
submissions_zip: "Download all submissions",
not_found: "No projects found.",
finished: "Finished",
not_found2: "Project not found",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default {
edit: "Bewerk project",
submissions_list: "Alle indieningen",
submissions_list_teacher: "Alle indieningen voor dit project",
submissions_zip: "Download alle indieningen",
not_found: "Geen projecten teruggevonden.",
finished: "Afgerond",
not_found2: "Project niet teruggevonden",
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/views/SubmissionsTeacherView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
<v-skeleton-loader v-else :loading="projectLoading || submissionsLoading" type="card">
<v-col class="mx-auto">
<h1>{{ $t("submission.submissions_title", { project: project.name }) }}</h1>

<v-btn class="primary-button" @click="downloadAll" prepend-icon="mdi-download">
{{ $t("project.submissions_zip") }}
</v-btn>

<v-alert v-if="submissions.length == 0" icon="$warning" color="warning">
{{ $t("submission.no_submissions") }}</v-alert
>
Expand All @@ -28,6 +33,7 @@ import { useProjectQuery } from "@/queries/Project";
import { useProjectSubmissionsQuery } from "@/queries/Submission";
import { toRefs } from "vue";
import SubmissionTeacherCard from "@/components/submission/SubmissionTeacherCard.vue";
import { download_file } from "@/utils";
const props = defineProps<{
projectId: number;
Expand All @@ -42,4 +48,17 @@ const {
error,
} = useProjectSubmissionsQuery(projectId);
const { data: project, isLoading: projectLoading } = useProjectQuery(projectId);
const downloadAll = () => {
download_file(`/api/projects/${projectId.value}/zip`, `project_${projectId.value}`);
};
</script>

<style scoped>
.primary-button {
margin-bottom: 5px;
min-width: 150px;
background-color: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-navtext));
}
</style>

0 comments on commit 4f66dbf

Please sign in to comment.