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

Homescreen deadlines #214

Merged
merged 7 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
9 changes: 7 additions & 2 deletions backend/src/project/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from datetime import datetime
from typing import Optional, Sequence, List
from html import escape
from typing import List, Optional, Sequence

from pydantic import BaseModel, ConfigDict, Field, field_validator

Expand Down Expand Up @@ -45,6 +44,12 @@ class ProjectList(BaseModel):
projects: Sequence[Project]


class UserProjectList(BaseModel):
model_config = ConfigDict(from_attributes=True)
as_instructor: Sequence[Project]
as_student: Sequence[Project]


class ProjectUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1)
deadline: Optional[datetime] = None
Expand Down
18 changes: 12 additions & 6 deletions backend/src/project/service.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from sqlalchemy import null
from typing import Sequence

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from src.subject.models import InstructorSubject, StudentSubject, Subject

from src.subject.models import StudentSubject, Subject
from .exceptions import ProjectNotFound
from .models import Project, Requirement
from .schemas import ProjectCreate, ProjectList, ProjectUpdate
Expand All @@ -29,15 +30,20 @@ async def get_project(db: AsyncSession, project_id: int) -> Project:
return result.scalars().first()


async def get_projects_by_user(db: AsyncSession, user_id: str) -> ProjectList:
result = await db.execute(
async def get_projects_by_user(db: AsyncSession, user_id: str) -> tuple[Sequence[Project], Sequence[Project]]:
student_result = await db.execute(
select(Project)
.join(Subject, Project.subject_id == Subject.id)
.join(StudentSubject, StudentSubject.c.subject_id == Subject.id)
.where(StudentSubject.c.uid == user_id)
)
projects = result.scalars().unique().all()
return ProjectList(projects=projects)
instructor_result = await db.execute(
select(Project)
.join(Subject, Project.subject_id == Subject.id)
.join(InstructorSubject, InstructorSubject.c.subject_id == Subject.id)
.where(InstructorSubject.c.uid == user_id)
)
return student_result.scalars().unique().all(), instructor_result.scalars().unique().all()


async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> ProjectList:
Expand Down
4 changes: 2 additions & 2 deletions backend/src/subject/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ async def get_subjects_by_user(
db: AsyncSession, user_id: str
) -> tuple[Sequence[Subject], Sequence[Subject]]:
instructors_subjects = await db.execute(
select(Subject).join(InstructorSubject).filter(
select(Subject).join(InstructorSubject).where(
InstructorSubject.c.uid == user_id)
)
students_subjects = await db.execute(
select(Subject).join(StudentSubject).filter(
select(Subject).join(StudentSubject).where(
StudentSubject.c.uid == user_id)

)
Expand Down
9 changes: 6 additions & 3 deletions backend/src/user/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from src.auth.exceptions import NotAuthorized, UnAuthenticated
from src.dependencies import get_async_db
from src.group.schemas import GroupList
from src.project.schemas import ProjectList
from src.project.schemas import UserProjectList

from .exceptions import UserNotFound
from .schemas import User, UserSubjectList
Expand Down Expand Up @@ -77,5 +77,8 @@ async def retrieve_groups(
async def retrieve_projects(
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_async_db),
) -> ProjectList:
return await project_service.get_projects_by_user(db, user.uid)
) -> UserProjectList:
student_projects, instructor_projects = await project_service.get_projects_by_user(
db, user.uid
)
return UserProjectList(as_student=student_projects, as_instructor=instructor_projects)
6 changes: 3 additions & 3 deletions backend/src/user/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from src.auth.dependencies import authentication_validation
from src.dependencies import get_async_db
from src.group.schemas import GroupList
from src.project.schemas import ProjectList
from src.project.schemas import UserProjectList

from .dependencies import (
get_authenticated_user,
Expand Down Expand Up @@ -73,8 +73,8 @@ async def list_subjects(

@router.get("/me/projects")
async def list_projects(
projects: ProjectList = Depends(retrieve_projects),
) -> ProjectList:
projects: UserProjectList = Depends(retrieve_projects),
) -> UserProjectList:
"""
Get the projects of the current user
"""
Expand Down
22 changes: 9 additions & 13 deletions frontend/src/components/home/cards/DeadlinesCard.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
<template>
<HomeScreenCard :title="'homescreen.deadlines'">
<DeadlineItem
v-for="deadline in deadlines"
:deadline="deadline"
:key="deadline.project.id"
/>
<DeadlineItem v-for="project in filteredProjects" :project="project" :key="project.id" />
</HomeScreenCard>
</template>

<script setup lang="ts">
import HomeScreenCard from "@/components/home/cards/HomeScreenCard.vue";
import DeadlineItem from "@/components/home/listcontent/DeadlineItem.vue";
import { type Deadline } from "@/models/Project";
import { useProjectsQuery } from "@/queries/Project";
import { computed } from "vue";

const { data: projects } = useProjectsQuery();
const deadlines = computed<Deadline[]>(
() =>
projects.value?.map((project) => ({
project,
status: "none",
})) || []
);

const filteredProjects = computed(() => {
if (!projects.value) return [];
return [...projects.value.as_student, ...projects.value.as_instructor]
.filter((project) => project.deadline > new Date())
.sort((a, b) => a.deadline.getTime() - b.deadline.getTime())
.slice(0, 5);
});
</script>
57 changes: 33 additions & 24 deletions frontend/src/components/home/listcontent/DeadlineItem.vue
Original file line number Diff line number Diff line change
@@ -1,48 +1,52 @@
<template>
<div class="projectbtn" @click="navigateToProject">
<div :class="getBackgroundClass()"></div>
<div :class="submissionToClass(latestSubmissionStatus)"></div>
<div class="leftcontent">
<h3>{{ deadline.project.name }}</h3>
<p class="p">{{ deadline.project.subject_id }}</p>
<h3>{{ project.name }}</h3>
<p v-if="!isSubjectLoading" class="p">{{ subject!.name }}</p>
</div>
<div class="rightcontent">
{{ formattedDate }}
{{ $d(project.deadline, "short") }}
</div>
</div>
</template>

<script setup lang="ts">
import { type Deadline } from "@/models/Project";
import { computed, toRefs } from "vue";
import router from "@/router";
import { toRefs, computed } from "vue";
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";

const props = defineProps<{
deadline: Deadline;
project: Project;
}>();

const { deadline } = toRefs(props);
const { project } = toRefs(props);

const getBackgroundClass = () => {
const { data: submissions } = useProjectSubmissionsQuery(project.value.id);

const latestSubmissionStatus = computed(() => {
if (!submissions.value || submissions.value.length === 0) return null;
return [...submissions.value].sort((a, b) => b.date.getTime() - a.date.getTime())[0];
});

const { data: subject, isLoading: isSubjectLoading } = useSubjectQuery(project.value.subject_id);

function submissionToClass(submission: Submission | null) {
return {
block: true,
accepted: deadline.value.status === "accepted",
rejected: deadline.value.status === "rejected",
none: deadline.value.status === "none",
in_progress: submission?.status === Status.InProgress,
accepted: submission?.status === Status.Accepted,
rejected: submission?.status === Status.Rejected || submission?.status === Status.Crashed,
none: !submission,
};
};

const formattedDate = computed(() =>
deadline.value.project.deadline.toLocaleTimeString([], {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
);
}

const navigateToProject = () => {
router.push(`/project/${deadline.value.project.id}`);
router.push(`/project/${project.value.id}`);
};
</script>

Expand Down Expand Up @@ -79,6 +83,11 @@ const navigateToProject = () => {
.rejected {
background-color: darkred;
}

.in_progress {
background-color: orange;
}

.leftcontent {
margin-left: 20px;
}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/models/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export default interface Project {
capacity: number;
}

export interface UserProjectList {
as_student: Project[];
as_instructor: Project[];
}

export interface ProjectForm {
name: string;
deadline: Date;
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/queries/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
deleteGroup,
getGroup,
getProjectGroups,
getGroupWithProjectId,
getUserGroups,
addToGroup,
joinGroup,
Expand All @@ -24,7 +23,7 @@ function GROUP_QUERY_KEY(groupId: number): (string | number)[] {
}

function PROJECT_GROUPS_QUERY_KEY(projectId: number): (string | number)[] {
return ["project", "groups", projectId];
return ["groups", "project", projectId];
}

function USER_GROUPS_QUERY_KEY(): string[] {
Expand Down Expand Up @@ -70,15 +69,23 @@ export function useUserGroupsQuery(): UseQueryReturnType<Group[], Error> {

/**
* Query composable for fetching the group a user is in for a project
* @param projectId The id of the project
* @returns The group the user is in for the project, undefined if the user is not in a group
*/
export function useProjectGroupQuery(
projectId: MaybeRefOrGetter<number | undefined>
): UseQueryReturnType<Group | null, Error> {
const { data: groups } = useUserGroupsQuery();
return useQuery<Group | null, Error>({
const { data: projectGroups } = useProjectGroupsQuery(projectId);
const { data: user } = useCurrentUserQuery();
const userGroup = computed(
() =>
projectGroups.value?.find((group) =>
group.members.some((member) => member.uid === user.value?.uid)
) || null
);
return useQuery({
queryKey: computed(() => PROJECT_USER_GROUP_QUERY_KEY(toValue(projectId)!)),
queryFn: () => getGroupWithProjectId(groups.value!, toValue(projectId)!),
enabled: !!toValue(projectId) && groups.value !== undefined,
queryFn: () => userGroup,
});
}

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/queries/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { MaybeRefOrGetter } from "vue";
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
import type { UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query";
import type Project from "@/models/Project";
import type { ProjectForm } from "@/models/Project";
import type { ProjectForm, UserProjectList } from "@/models/Project";
import { getProject, createProject, getProjects } from "@/services/project";

function PROJECT_QUERY_KEY(projectId: number): (string | number)[] {
Expand All @@ -30,8 +30,8 @@ export function useProjectQuery(
/**
* Query composable for fetching all projects of the current user
*/
export function useProjectsQuery(): UseQueryReturnType<Project[], Error> {
return useQuery<Project[], Error>({
export function useProjectsQuery(): UseQueryReturnType<UserProjectList, Error> {
return useQuery<UserProjectList>({
queryKey: PROJECTS_QUERY_KEY(),
queryFn: getProjects,
});
Expand Down
28 changes: 26 additions & 2 deletions frontend/src/queries/Submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import type { UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-qu
import { createSubmission, getFiles, getSubmission, getSubmissions } from "@/services/submission";
import type Submission from "@/models/Submission";
import type FileInfo from "@/models/File";
import { useProjectGroupQuery } from "./Group";

function SUBMISSION_QUERY_KEY(submissionId: number): (string | number)[] {
return ["submission", submissionId];
}

function SUBMISSIONS_QUERY_KEY(groupId: number): (string | number)[] {
return ["submissions", groupId];
return ["submissions", "group", groupId];
}

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

function FILES_QUERY_KEY(submissionId: number): (string | number)[] {
Expand All @@ -28,7 +33,6 @@ export function useSubmissionQuery(
queryKey: computed(() => SUBMISSION_QUERY_KEY(toValue(submissionId)!)),
queryFn: () => getSubmission(toValue(submissionId)!),
enabled: () => !!toValue(submissionId),
retry: false,
});
}

Expand All @@ -45,6 +49,26 @@ 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(
projectId: MaybeRefOrGetter<number | undefined>
): UseQueryReturnType<Submission[], Error> {
const { data: group } = useProjectGroupQuery(projectId);
return useQuery({
queryKey: computed(() => 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...
if (group.value === null) return [];
return await getSubmissions(group.value!.id);
},
enabled: () => !!toValue(projectId),
});
}

/**
* Query composable for fetching files for a submission
*/
Expand Down
9 changes: 0 additions & 9 deletions frontend/src/services/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ export async function getUserGroups(): Promise<Group[]> {
return result.groups;
}

export function getGroupWithProjectId(groups: Group[], projectId: number): Group | null {
for (const group of groups) {
if (group.project_id === projectId) {
return group;
}
}
return null;
}

/**
* Fetches all groups of a project.
*/
Expand Down
Loading
Loading