diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 624b985e..cba03df1 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -77,10 +77,11 @@ async def list_groups(groups: GroupList = Depends(retrieve_groups_by_project)): @router.get("/{project_id}/submissions", dependencies=[Depends(patch_permission_validation)]) -async def list_submissions(group_id: int, +async def list_submissions(project_id: int, db: AsyncSession = Depends(get_async_db) ) -> Sequence[Submission]: - return await get_submissions_by_project(db, group_id) + """Return a list of the latest submission of each group of this project""" + return await get_submissions_by_project(db, project_id) @router.get("/{project_id}/test_files") diff --git a/backend/src/submission/router.py b/backend/src/submission/router.py index 99a48d6b..683001cb 100644 --- a/backend/src/submission/router.py +++ b/backend/src/submission/router.py @@ -2,7 +2,7 @@ from typing import Sequence from fastapi import APIRouter, Depends, BackgroundTasks -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from src.dependencies import get_async_db @@ -15,7 +15,7 @@ ) from src.submission.exceptions import FileNotFound from src.submission.exceptions import FilesNotFound -from src.submission.utils import upload_files, remove_files +from src.submission.utils import upload_files, remove_files, zip_stream from src.user.dependencies import admin_user_validation, get_authenticated_user from src.user.schemas import User from . import service @@ -94,6 +94,12 @@ async def get_file(path: str, submission: Submission = Depends(retrieve_submissi return FileResponse(path=path) +@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") + + @router.get("/{submission_id}/artifacts", response_model=list[File]) async def get_artifacts(submission: Submission = Depends(retrieve_submission)): if submission.status == Status.InProgress: diff --git a/backend/src/submission/service.py b/backend/src/submission/service.py index 11db8abd..108ee011 100644 --- a/backend/src/submission/service.py +++ b/backend/src/submission/service.py @@ -1,6 +1,7 @@ from typing import Sequence, List from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select +from sqlalchemy import func from . import models, schemas from .models import Status @@ -12,9 +13,13 @@ async def get_submissions(db: AsyncSession) -> Sequence[models.Submission]: async def get_submissions_by_project(db: AsyncSession, project_id: int) -> Sequence[models.Submission]: + # SQL for the win + subquery = select(models.Submission.group_id, func.max(models.Submission.date).label( + 'max_date')).group_by(models.Submission.group_id).subquery() + query = select(models.Submission).join(subquery, (models.Submission.group_id == subquery.c.group_id) & ( + models.Submission.date == subquery.c.max_date) & (models.Submission.project_id == project_id)) - return (await db.execute(select(models.Submission). - filter_by(project_id=project_id))).unique().scalars().all() + return (await db.execute(query)).unique().scalars().all() async def get_submissions_by_group(db: AsyncSession, diff --git a/backend/src/submission/utils.py b/backend/src/submission/utils.py index bc741ea6..7e0afe1d 100644 --- a/backend/src/submission/utils.py +++ b/backend/src/submission/utils.py @@ -1,6 +1,8 @@ import os import shutil import zipfile +import pathlib +import io import fnmatch from uuid import uuid4 @@ -45,5 +47,16 @@ def upload_files(files: list[UploadFile], project: Project) -> str: return uuid +def zip_stream(path, group_id: int): + base_path = pathlib.Path(path) + 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, "")}" + z.write(f_name, arcname=name) + data.seek(0) + yield from data + + def remove_files(uuid: str): shutil.rmtree(submissions_path(uuid)) diff --git a/frontend/src/components/home/listcontent/DeadlineItem.vue b/frontend/src/components/home/listcontent/DeadlineItem.vue index 0f5cc5ef..e593015f 100644 --- a/frontend/src/components/home/listcontent/DeadlineItem.vue +++ b/frontend/src/components/home/listcontent/DeadlineItem.vue @@ -18,7 +18,7 @@ import type Project from "@/models/Project"; import type Submission from "@/models/Submission"; import { Status } from "@/models/Submission"; import { useSubjectQuery } from "@/queries/Subject"; -import { useProjectSubmissionsQuery } from "@/queries/Submission"; +import { useUserProjectSubmissionsQuery } from "@/queries/Submission"; const props = defineProps<{ project: Project; @@ -26,7 +26,7 @@ const props = defineProps<{ const { project } = toRefs(props); -const { data: submissions } = useProjectSubmissionsQuery(project.value.id); +const { data: submissions } = useUserProjectSubmissionsQuery(project.value.id); const latestSubmissionStatus = computed(() => { if (!submissions.value || submissions.value.length === 0) return null; diff --git a/frontend/src/components/project/ProjectInfo.vue b/frontend/src/components/project/ProjectInfo.vue index 00b80d37..fd3e2777 100644 --- a/frontend/src/components/project/ProjectInfo.vue +++ b/frontend/src/components/project/ProjectInfo.vue @@ -28,8 +28,18 @@
+ + + {{ $t("project.submissions_list_teacher") }} + + - + @@ -37,7 +47,7 @@ import type Project from "@/models/Project"; import type Group from "@/models/Group"; import SubmitInfo from "@/components/project/submit/SubmitInfo.vue"; -import { toRefs } from "vue"; +import { toRefs, computed } from "vue"; import { Quill } from "@vueup/vue-quill"; import type User from "@/models/User"; import type Subject from "@/models/Subject"; @@ -46,10 +56,18 @@ const props = defineProps<{ project: Project; group: Group | null; instructors: User[]; + user: User; subject: Subject; }>(); -const { project, group, instructors, subject } = toRefs(props); +const { project, group, instructors, subject, user } = toRefs(props); + +const isTeacher = computed( + () => + user.value.is_teacher || + user.value.is_admin || + instructors.value?.some((element) => element.uid == user.value.uid) +); const renderQuillContent = (content: string) => { const quill = new Quill(document.createElement("div")); diff --git a/frontend/src/components/submission/SubmissionCard.vue b/frontend/src/components/submission/SubmissionCard.vue index de0cd4b9..0e1beb08 100644 --- a/frontend/src/components/submission/SubmissionCard.vue +++ b/frontend/src/components/submission/SubmissionCard.vue @@ -1,8 +1,8 @@