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

Submission list voor een vak leerkracht of assistent #190

Merged
merged 8 commits into from
May 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
5 changes: 3 additions & 2 deletions backend/src/project/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 8 additions & 2 deletions backend/src/submission/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 7 additions & 2 deletions backend/src/submission/service.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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))
Comment on lines +16 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

epic query 💯


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,
Expand Down
13 changes: 13 additions & 0 deletions backend/src/submission/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import shutil
import zipfile
import pathlib
import io
import fnmatch
from uuid import uuid4

Expand Down Expand Up @@ -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))
4 changes: 2 additions & 2 deletions frontend/src/components/home/listcontent/DeadlineItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ 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;
}>();

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;
Expand Down
24 changes: 21 additions & 3 deletions frontend/src/components/project/ProjectInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,26 @@
</v-card-title>
<div v-html="renderQuillContent(project.description)"></div>
</v-card-item>
<v-card-actions>
<v-btn color="blue" v-if="isTeacher" :to="`/project/${project.id}/submissions`">
{{ $t("project.submissions_list_teacher") }}
</v-btn>
</v-card-actions>
</v-card>
<SubmitInfo class="submitInfo" v-if="group" :project="project" :group="group" />
<SubmitInfo
class="submitInfo"
v-if="group && !isTeacher"
:project="project"
:group="group"
/>
</v-container>
</template>

<script setup lang="ts">
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";
Expand All @@ -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"));
Expand Down
25 changes: 18 additions & 7 deletions frontend/src/components/submission/SubmissionCard.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<v-card color="white">
<v-card>
<v-card-title>
{{ $t("submission.status") }}
<p v-if="submission.date <= project.deadline" :class="Status[submission.status]">
<p v-if="new Date(submission.date) <= deadline" :class="Status[submission.status]">
{{ Status[submission.status] }}
</p>
<p v-else class="Deadline">{{ $t("submission.after_deadline") }}</p>
Expand Down Expand Up @@ -50,36 +50,40 @@
color="error"
:text="error!.message"
></v-alert>
<v-skeleton-loader v-else :loading="isLoading" type="card" color="white">
<v-skeleton-loader v-else :loading="isLoading" type="card">
<v-col>
<v-chip
class="ma-2"
v-for="(item, index) in files"
label
color="blue"
:key="item.filename"
:key="item.filesortedname"
@click="() => downloadFile(index)"
>
{{ item.filename }}
</v-chip>
</v-col>
</v-skeleton-loader>
</v-container>
<v-card-actions>
<v-btn @click="downloadAll">
{{ $t("submission.download_all_files") }}
</v-btn>
</v-card-actions>
</v-card-item>
</v-card>
</template>

<script setup lang="ts">
import type { Status } from "@/models/Submission";
import { Status } from "@/models/Submission";
import { useFilesQuery } from "@/queries/Submission";
import { toRefs, computed } from "vue";
import { download_file } from "@/utils";
import type Project from "@/models/Project";
import type Submission from "@/models/Submission";

const props = defineProps<{
submission: Submission;
project: Project;
deadline: Date;
}>();

const { submission } = toRefs(props);
Expand All @@ -95,6 +99,13 @@ const downloadFile = (index: number) => {
const file = files.value![index];
download_file(`/api/submissions/${submission.value!.id}/files/${file.filename}`, file.filename);
};

const downloadAll = () => {
download_file(
`/api/submissions/${submission.value!.id}/zip`,
`submission_group_${submission.value?.group_id}`
);
};
</script>

<style scoped>
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/submission/SubmissionTeacherCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<v-card>
<v-card-title>
{{ $t("project.group", { number: submission.group_id }) }}
</v-card-title>

<SubmissionCard class="ma-3" :submission="submission" :deadline="deadline" />

<v-card-actions>
<v-btn :to="`/groups/${submission.group_id}/submissions`">
{{ $t("project.submissions_list") }}
</v-btn>
</v-card-actions>
</v-card>
</template>

<script setup lang="ts">
import SubmissionCard from "@/components/submission/SubmissionCard.vue";
import { toRefs } from "vue";

const props = defineProps<{
submission: Submission;
deadline: string;
}>();

const { submission } = toRefs(props);
</script>
4 changes: 4 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export default {
after_deadline: "After deadline",
submissions_title: "Submissions for project {project}",
no_submissions: "No submissions yet",
teacher_submissions_info:
"This page contains a list of the latest submission of each group for this project.",
docker_test: "Tests Output",
download_all_files: "Download all files",
},
project: {
deadline: "Deadline",
Expand All @@ -56,6 +59,7 @@ export default {
capacity_group: "Capacity: ",
edit: "Edit project",
submissions_list: "All submissions",
submissions_list_teacher: "All submissions for this project",
not_found: "No projects found.",
finished: "Finished",
not_found2: "Project not found",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/i18n/locales/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export default {
after_deadline: "Na deadline",
submissions_title: "Indieningen voor project {project}",
no_submissions: "Nog geen indieningen",
teacher_submissions_info:
"Deze pagina bevat een lijst van de laatste indiening van elke groep voor dit project.",
docker_test: "Testen Output",
download_all_files: "Download alle bestanden",
},
project: {
deadline: "Deadline",
Expand All @@ -56,6 +59,7 @@ export default {
capacity_group: "Capaciteit: ",
edit: "Bewerk project",
submissions_list: "Alle indieningen",
submissions_list_teacher: "Alle indieningen voor dit project",
not_found: "Geen projecten teruggevonden.",
finished: "Afgerond",
not_found2: "Project niet teruggevonden",
Expand Down
30 changes: 26 additions & 4 deletions frontend/src/queries/Submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { computed, toValue } from "vue";
import type { MaybeRefOrGetter } from "vue";
import { useQuery, useQueryClient, useMutation } from "@tanstack/vue-query";
import type { UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query";
import { createSubmission, getFiles, getSubmission, getSubmissions } from "@/services/submission";
import {
createSubmission,
getFiles,
getProjectSubmissions,
getSubmission,
getSubmissions,
} from "@/services/submission";
import type Submission from "@/models/Submission";
import type FileInfo from "@/models/File";
import { useProjectGroupQuery } from "./Group";
Expand All @@ -15,6 +21,10 @@ function SUBMISSIONS_QUERY_KEY(groupId: number): (string | number)[] {
return ["submissions", "group", groupId];
}

function USER_PROJECT_SUBMISSIONS_QUERY_KEY(projectId: number): (string | number)[] {
return ["submissions", "project", "user", projectId];
}

function PROJECT_SUBMISSIONS_QUERY_KEY(projectId: number): (string | number)[] {
return ["submissions", "project", projectId];
}
Expand Down Expand Up @@ -53,12 +63,12 @@ export function useSubmissionsQuery(
* Query composable for fetching all submissions of the group of the current user
* in the project with the given id
*/
export function useProjectSubmissionsQuery(
export function useUserProjectSubmissionsQuery(
projectId: MaybeRefOrGetter<number | undefined>
): UseQueryReturnType<Submission[], Error> {
const { data: group } = useProjectGroupQuery(projectId);
return useQuery({
queryKey: computed(() => PROJECT_SUBMISSIONS_QUERY_KEY(toValue(projectId)!)),
queryKey: computed(() => USER_PROJECT_SUBMISSIONS_QUERY_KEY(toValue(projectId)!)),
queryFn: async () => {
// HACK: Without this null-check, queries where there is no group will take a long time to resolve
// also, this should be `!group.value`, but javascript...
Expand All @@ -69,6 +79,19 @@ export function useProjectSubmissionsQuery(
});
}

/**
* Query composable for fetching all latest submissions of each group from a project.
*/
export function useProjectSubmissionsQuery(
projectId: MaybeRefOrGetter<number | undefined>
): UseQueryReturnType<Submission[], Error> {
return useQuery<Submission[], Error>({
queryKey: computed(() => PROJECT_SUBMISSIONS_QUERY_KEY(toValue(projectId)!)),
queryFn: () => getProjectSubmissions(toValue(projectId)!),
enabled: () => !!toValue(projectId),
});
}

/**
* Query composable for fetching files for a submission
*/
Expand All @@ -79,7 +102,6 @@ export function useFilesQuery(
queryKey: computed(() => FILES_QUERY_KEY(toValue(submissionId)!)),
queryFn: () => getFiles(toValue(submissionId)!),
enabled: () => !!toValue(submissionId),
retry: false,
});
}

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ const router = createRouter({
component: () => import("../views/GroupView.vue"),
props: (route) => ({ groupId: Number(route.params.groupId) }),
},
{
path: "/project/:projectId(\\d+)/submissions",
name: "projectSubmissions",
component: () => import("../views/SubmissionsTeacherView.vue"),
props: (route) => ({ projectId: Number(route.params.projectId) }),
},
{
path: "/subjects",
name: "subjects",
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/services/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export async function getSubmissions(groupId: number): Promise<Submission[]> {
return result.map(initSubmissionDate);
}

/**
* Fetches all latest submissions of each group from a project.
*/
export async function getProjectSubmissions(projectId: number): Promise<Submission[]> {
const result = await authorized_fetch<Submission[]>(`/api/projects/${projectId}/submissions`, {
method: "GET",
});
return result;
}

/**
* Creates a new submission for a group.
*/
Expand Down
Loading
Loading