Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download alle indieningen (files) + csv (met indieningen info) in 1 keer als zip #228

Merged
merged 3 commits into from
May 20, 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
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 @@ -95,9 +95,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 @@ -61,6 +61,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>
Loading