diff --git a/tasky/src/models/assignment.rs b/tasky/src/models/assignment.rs index e52f76f..e08cec0 100644 --- a/tasky/src/models/assignment.rs +++ b/tasky/src/models/assignment.rs @@ -3,6 +3,7 @@ use super::{Paginate, PaginatedModel, DB}; use crate::schema::assignments::dsl; use chrono::NaiveDateTime; use diesel::associations::HasTable; +use diesel::dsl::not; use diesel::prelude::*; use diesel::{ prelude::{Insertable, Queryable}, @@ -155,4 +156,26 @@ impl AssignmentRepository { .get_results::(conn) .expect("Cannot load assignment IDs") } + + /// Gets all pending assignments for students + pub fn get_student_pending_assignments( + student_id: i32, + page: i64, + conn: &mut DB, + ) -> PaginatedModel { + dsl::assignments + .left_join(crate::schema::groups::table) + .left_join(crate::schema::solutions::table) + .filter(crate::schema::groups::dsl::members.contains(vec![Some(student_id)])) + .filter(not(crate::schema::solutions::dsl::submitter_id + .eq(student_id) + .and( + crate::schema::solutions::dsl::approval_status.eq("APPROVED"), + ))) + .select(Assignment::as_select()) + .group_by(dsl::id) + .paginate(page) + .load_and_count_pages::(conn) + .expect("Cannot fetch pending assignments for student") + } } diff --git a/tasky/src/models/assignment_wish.rs b/tasky/src/models/assignment_wish.rs index 761dd0b..33430da 100644 --- a/tasky/src/models/assignment_wish.rs +++ b/tasky/src/models/assignment_wish.rs @@ -70,4 +70,19 @@ impl AssignmentWishRepository { .execute(conn) .expect("Cannot delete assignment wish"); } + + /// Gets all assignment wishes for a tutor + pub fn get_tutor_wishes( + tutor_id: i32, + page: i64, + conn: &mut DB, + ) -> PaginatedModel { + dsl::assignment_wishes + .left_join(crate::schema::groups::table) + .filter(crate::schema::groups::dsl::tutor.eq(tutor_id)) + .select(AssignmentWish::as_select()) + .paginate(page) + .load_and_count_pages::(conn) + .expect("Cannot fetch tutor assignment wishes") + } } diff --git a/tasky/src/models/solution.rs b/tasky/src/models/solution.rs index fe76dde..c93bd0e 100644 --- a/tasky/src/models/solution.rs +++ b/tasky/src/models/solution.rs @@ -170,4 +170,25 @@ impl SolutionRepository { .get_result::(conn) .expect("Cannot create new solution") } + + /// Gets all pending solutions for tutor + pub fn get_pending_solutions_for_tutor( + tutor_id: i32, + page: i64, + conn: &mut DB, + ) -> PaginatedModel { + dsl::solutions + .left_join(crate::schema::groups::table) + .filter(crate::schema::groups::dsl::tutor.eq(tutor_id)) + .filter( + dsl::approval_status + .ne("APPROVED") + .and(dsl::approval_status.ne("REJECTED")), + ) + .select(Solution::as_select()) + .group_by(dsl::id) + .paginate(page) + .load_and_count_pages::(conn) + .expect("Cannot load pending solutions for tutor") + } } diff --git a/tasky/src/response/assignment.rs b/tasky/src/response/assignment.rs index 491add9..18bd57c 100644 --- a/tasky/src/response/assignment.rs +++ b/tasky/src/response/assignment.rs @@ -61,6 +61,7 @@ pub struct MinifiedAssignmentResponse { pub description: String, pub language: AssignmentLanguage, pub completed: Option, + pub group_id: i32, } /// A vec of assignments @@ -83,6 +84,7 @@ impl Enrich for MinifiedAssignmentResponse { description: from.description.clone(), language: from.language.clone(), completed: None, + group_id: from.group_id, }) } } diff --git a/tasky/src/routes/assignment.rs b/tasky/src/routes/assignment.rs index 8e904fb..ba914db 100644 --- a/tasky/src/routes/assignment.rs +++ b/tasky/src/routes/assignment.rs @@ -1,24 +1,15 @@ use super::PaginationParams; -use crate::handler::assignment::handle_update_multipart; -use crate::models::assignment::QuestionCatalogueElement; -use crate::AppState; -use actix_multipart::form::MultipartForm; -use actix_web::get; -use actix_web::post; -use actix_web::web; -use actix_web::HttpResponse; -use chrono::NaiveDateTime; -use serde::Serialize; - use crate::auth_middleware::UserData; use crate::error::ApiError; use crate::handler::assignment::handle_create_multipart; +use crate::handler::assignment::handle_update_multipart; use crate::handler::assignment::CreateCodeTestMultipart; use crate::handler::questions::handle_catalogue_creation; use crate::models::assignment::Assignment; use crate::models::assignment::AssignmentLanguage; use crate::models::assignment::AssignmentRepository; use crate::models::assignment::CreateAssignment; +use crate::models::assignment::QuestionCatalogueElement; use crate::models::group::Group; use crate::models::group::GroupRepository; use crate::models::DB; @@ -29,8 +20,17 @@ use crate::response::Enrich; use crate::security::IsGranted; use crate::security::SecurityAction; use crate::security::StaticSecurity; +use crate::security::StaticSecurityAction; use crate::util::mongo::parse_object_ids; +use crate::AppState; +use actix_multipart::form::MultipartForm; +use actix_web::get; +use actix_web::post; +use actix_web::web; +use actix_web::HttpResponse; use chrono::DateTime; +use chrono::NaiveDateTime; +use serde::Serialize; use serde::{Deserialize, Deserializer}; fn deserialize_naive_datetime<'de, D>(deserializer: D) -> Result, D::Error> @@ -322,6 +322,30 @@ pub async fn create_question_catalogue( Ok(HttpResponse::Ok().json(response)) } +#[get("/student_pending_assignments")] +pub async fn get_student_pending_assignments( + data: web::Data, + user: web::ReqData, + pagination: web::Query, +) -> Result { + let user_data = user.into_inner(); + let conn = &mut data.db.db.get().unwrap(); + + if !StaticSecurity::is_granted(StaticSecurityAction::IsStudent, &user_data) { + return Err(ApiError::BadRequest { + message: "Cannot get as non student".to_string(), + }); + } + let assignments = AssignmentRepository::get_student_pending_assignments( + user_data.user_id, + pagination.page, + conn, + ); + let enriched = + AssignmentsResponse::enrich(&assignments, &mut data.user_api.clone(), conn).await?; + Ok(HttpResponse::Ok().json(enriched)) +} + /// Gets group and assignment from request params and connection. /// Furthermore, it handles all the user security checks fn get_group_and_assignment( diff --git a/tasky/src/routes/assignment_wish.rs b/tasky/src/routes/assignment_wish.rs index 970cab6..f969229 100644 --- a/tasky/src/routes/assignment_wish.rs +++ b/tasky/src/routes/assignment_wish.rs @@ -2,6 +2,7 @@ use super::PaginationParams; use crate::models::assignment_wish::AssignmentWishRepository; use crate::models::group::GroupRepository; use crate::security::{IsGranted, SecurityAction}; +use crate::security::{StaticSecurity, StaticSecurityAction}; use crate::{ auth_middleware::UserData, error::ApiError, models::assignment_wish::CreateAssignmentWish, AppState, @@ -138,3 +139,22 @@ pub async fn delete_wish( AssignmentWishRepository::delete_wish(&wish.unwrap(), conn); Ok(HttpResponse::Ok().finish()) } + +#[get("/tutor_assignment_wishes")] +pub async fn tutor_pending_wishes( + data: web::Data, + user: web::ReqData, + pagination: web::Query, +) -> Result { + let user_data = user.into_inner(); + let conn = &mut data.db.db.get().unwrap(); + + if !StaticSecurity::is_granted(StaticSecurityAction::IsTutor, &user_data) { + return Err(ApiError::BadRequest { + message: "Cannot get as non tutor".to_string(), + }); + } + let wishes = + AssignmentWishRepository::get_tutor_wishes(user_data.user_id, pagination.page, conn); + Ok(HttpResponse::Ok().json(wishes)) +} diff --git a/tasky/src/routes/mod.rs b/tasky/src/routes/mod.rs index 580f121..4969386 100644 --- a/tasky/src/routes/mod.rs +++ b/tasky/src/routes/mod.rs @@ -44,6 +44,7 @@ pub fn init_services(cfg: &mut web::ServiceConfig) { .service(assignment::view_assignment_test) .service(assignment::create_question_catalogue) .service(assignment::update_assignment_test) + .service(assignment::get_student_pending_assignments) .service(solution::create_solution) .service(solution::get_solution) .service(solution::get_solutions_for_assignment) @@ -51,10 +52,12 @@ pub fn init_services(cfg: &mut web::ServiceConfig) { .service(solution::approve_solution) .service(solution::reject_solution) .service(solution::get_solution_files) + .service(solution::get_tutor_solutions) .service(assignment_wish::create_wish) .service(assignment_wish::get_wishes) .service(assignment_wish::get_wish) .service(assignment_wish::delete_wish) + .service(assignment_wish::tutor_pending_wishes) .service(code_comment::get_code_comments) .service(code_comment::create_code_comment) .service(notifications::get_notifiations) diff --git a/tasky/src/routes/solution.rs b/tasky/src/routes/solution.rs index 2fd40c7..5d4f289 100644 --- a/tasky/src/routes/solution.rs +++ b/tasky/src/routes/solution.rs @@ -98,6 +98,30 @@ pub async fn get_solutions_for_user( Ok(HttpResponse::Ok().json(response)) } +#[get("/tutor_solutions")] +pub async fn get_tutor_solutions( + data: web::Data, + user: web::ReqData, + pagination: web::Query, +) -> Result { + let user_data = user.into_inner(); + let conn = &mut data.db.db.get().unwrap(); + + if !StaticSecurity::is_granted(StaticSecurityAction::IsTutor, &user_data) { + return Err(ApiError::BadRequest { + message: "Cannot get as non tutor".to_string(), + }); + } + + let solutions = SolutionRepository::get_pending_solutions_for_tutor( + user_data.user_id, + pagination.page, + conn, + ); + let response = SolutionsResponse::enrich(&solutions, &mut data.user_api.clone(), conn).await?; + Ok(HttpResponse::Ok().json(response)) +} + /// Endpoint to get all solutions for an assignment #[get("/assignments/{assignment_id}/solutions")] pub async fn get_solutions_for_assignment( diff --git a/web/app/pending-assignments/page.tsx b/web/app/pending-assignments/page.tsx new file mode 100644 index 0000000..0afd8cd --- /dev/null +++ b/web/app/pending-assignments/page.tsx @@ -0,0 +1,34 @@ +"use client"; +import useApiServiceClient from "@/hooks/useApiServiceClient"; +import {useState} from "react"; +import useClientQuery from "@/hooks/useClientQuery"; +import {useTranslation} from "react-i18next"; +import {Container, Pagination, Stack, Title} from "@mantine/core"; +import AssignmentCard from "@/components/assignments/AssignmentCard"; + + +const PendingAssignmentsPage = () => { + + const api = useApiServiceClient(); + const [page, setPage] = useState(1); + const [assignments] = useClientQuery(() => api.getPendingAssignments(page), [page]); + const { t } = useTranslation("assignment"); + + return ( + + {t('assignment:titles.pending-assignments')} + + {(assignments?.assignments ?? []).map((assignment) => ( + + ))} + + + + ); +} + +export default PendingAssignmentsPage; diff --git a/web/app/pending-solutions/page.tsx b/web/app/pending-solutions/page.tsx new file mode 100644 index 0000000..75868b0 --- /dev/null +++ b/web/app/pending-solutions/page.tsx @@ -0,0 +1,64 @@ +"use client"; +import useApiServiceClient from "@/hooks/useApiServiceClient"; +import useClientQuery from "@/hooks/useClientQuery"; +import {Container, Pagination, Title} from "@mantine/core"; +import {useTranslation} from "react-i18next"; +import EntityList, {EntityListCol, EntityListRowAction} from "@/components/EntityList"; +import SolutionBadge from "@/components/solution/SolutionBadge"; +import {UserRoles} from "@/service/types/usernator"; +import {useRouter} from "next/navigation"; +import {useState} from "react"; + + +const PendingSolutions = () => { + + const api = useApiServiceClient(); + const [page, setPage] = useState(1); + const [solutions] = useClientQuery(() => api.getPendingSolutions(page), [page]); + const {t} = useTranslation(['common', 'solution']); + const router = useRouter(); + + const cols: EntityListCol[] = [ + { + field: "id", + label: t("cols.id"), + }, + { + field: "assignment", + label: t("solution:cols.assignment"), + getter: (row) => row.assignment.title, + }, + { + field: "approval_status", + label: t("solution:cols.approval-status"), + render: (value) => , + }, + ]; + + const rowActions: EntityListRowAction[] = [ + { + name: t("actions.view"), + onClick: (row) => router.push(`/solutions/${row.id}`), + color: undefined, + auth: [UserRoles.Tutor], + }, + ]; + + return ( + + {t('solution:titles.pending-solutions')} + + + + ) +} + +export default PendingSolutions; diff --git a/web/app/pending-wishes/page.tsx b/web/app/pending-wishes/page.tsx new file mode 100644 index 0000000..d047053 --- /dev/null +++ b/web/app/pending-wishes/page.tsx @@ -0,0 +1,68 @@ +"use client"; +import useApiServiceClient from "@/hooks/useApiServiceClient"; +import {useState} from "react"; +import useClientQuery from "@/hooks/useClientQuery"; +import {useTranslation} from "react-i18next"; +import {Container, Pagination, Title} from "@mantine/core"; +import EntityList, {EntityListCol, EntityListRowAction} from "@/components/EntityList"; +import {UserRoles} from "@/service/types/usernator"; +import {showNotification} from "@mantine/notifications"; + + +const PendingWishes = () => { + const api = useApiServiceClient(); + const [page, setPage] = useState(1); + const [wishes, refetch] = useClientQuery(() => api.getPendingWishes(page), [page]); + const {t} = useTranslation(['common', 'assignment']); + + const cols: EntityListCol[] = [ + { + field: "title", + label: t("fields.title"), + }, + { + field: "description", + label: t("fields.description"), + }, + { + field: 'group_id', + label: t("fields.group_id") + } + ]; + + const rowActions: EntityListRowAction[] = [ + { + auth: [UserRoles.Admin, UserRoles.Tutor], + name: t("actions.delete"), + onClick: async (row) => { + try { + await api.deleteAssignmentWish(row.group_id, row.id); + refetch(); + } catch (e: any) { + showNotification({ + title: t("messages.error"), + message: e?.message ?? "", + }); + } + }, + color: "red", + }, + ]; + return ( + + {t('assignment:titles.pending-wishes')} + + + + ) +} + +export default PendingWishes; diff --git a/web/app/solutions/page.tsx b/web/app/solutions/page.tsx index 2e0ec63..0a3c93e 100644 --- a/web/app/solutions/page.tsx +++ b/web/app/solutions/page.tsx @@ -30,12 +30,12 @@ const PersonalSolutionsPage = () => { }, { field: "assignment", - label: t("cols.assignment"), + label: t("solution:cols.assignment"), getter: (row) => row.assignment.title, }, { field: "approval_status", - label: t("cols.approval-status"), + label: t("solution:cols.approval-status"), render: (value) => , }, ]; diff --git a/web/public/locales/de/assignment.json b/web/public/locales/de/assignment.json index d33b0e0..af8e31c 100644 --- a/web/public/locales/de/assignment.json +++ b/web/public/locales/de/assignment.json @@ -23,7 +23,9 @@ "create-assignment": "Aufgabe erstellen", "update-assignment": "Aufgabe aktualisieren", "question-catalogue": "Fragenkatalog", - "create-wish": "Aufgabenwunsch erstellen" + "create-wish": "Aufgabenwunsch erstellen", + "pending-wishes": "Offene Wünsche", + "pending-assignments": "Offene Aufgaben" }, "messages": { "failed-solution-creation": "Fehler beim Erstellen der Lösung", diff --git a/web/public/locales/de/common.json b/web/public/locales/de/common.json index 075acb9..2bd0e86 100644 --- a/web/public/locales/de/common.json +++ b/web/public/locales/de/common.json @@ -28,6 +28,7 @@ "title": "Titel", "description": "Beschreibung", "test-file": "Testdatei", + "group_id": "Gruppen ID", "search": "Suche" }, "actions": { diff --git a/web/public/locales/de/routes.json b/web/public/locales/de/routes.json index 33ddf40..825ad54 100644 --- a/web/public/locales/de/routes.json +++ b/web/public/locales/de/routes.json @@ -7,5 +7,7 @@ "tutors": "Tutoren", "my-groups": "Meine Gruppen", "groups": "Gruppen", - "solutions": "Lösungen" + "solutions": "Lösungen", + "wishes": "Wünsche", + "assignments": "Aufgaben" } diff --git a/web/public/locales/de/solution.json b/web/public/locales/de/solution.json index 90667d6..4e60f04 100644 --- a/web/public/locales/de/solution.json +++ b/web/public/locales/de/solution.json @@ -19,7 +19,8 @@ }, "titles": { "create-comment": "Kommentar erstellen", - "your-comment": "Ihr Kommentar" + "your-comment": "Ihr Kommentar", + "pending-solutions": "Ausstehende Lösungen" }, "cols": { "assignment": "Aufgabe", diff --git a/web/public/locales/en/assignment.json b/web/public/locales/en/assignment.json index 75c98d6..0f38e69 100644 --- a/web/public/locales/en/assignment.json +++ b/web/public/locales/en/assignment.json @@ -23,7 +23,9 @@ "create-assignment": "Create assignment", "update-assignment": "Update assignment", "question-catalogue": "Question catalogue", - "create-wish": "Create assignment wish" + "create-wish": "Create assignment wish", + "pending-wishes": "Pending wishes", + "pending-assignments": "Pending assignments" }, "messages": { "failed-solution-creation": "Error creating solution", diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index f0d99dc..987623a 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -28,6 +28,7 @@ "title": "Title", "description": "Description", "test-file": "Test File", + "group_id": "Group ID", "search": "Search" }, "actions": { diff --git a/web/public/locales/en/routes.json b/web/public/locales/en/routes.json index 00bbc6a..f6a20a7 100644 --- a/web/public/locales/en/routes.json +++ b/web/public/locales/en/routes.json @@ -7,5 +7,7 @@ "tutors": "Tutors", "my-groups": "My Groups", "groups": "Groups", - "solutions": "Solutions" + "solutions": "Solutions", + "wishes": "Wishes", + "assignments": "Assignments" } diff --git a/web/public/locales/en/solution.json b/web/public/locales/en/solution.json index 0964527..c195fda 100644 --- a/web/public/locales/en/solution.json +++ b/web/public/locales/en/solution.json @@ -19,7 +19,8 @@ }, "titles": { "create-comment": "Create comment", - "your-comment": "Your comment" + "your-comment": "Your comment", + "pending-solutions": "Pending solutions" }, "cols": { "assignment": "Assignment", diff --git a/web/service/ApiService.ts b/web/service/ApiService.ts index ebceb6e..4556a41 100644 --- a/web/service/ApiService.ts +++ b/web/service/ApiService.ts @@ -325,6 +325,18 @@ class ApiService { await this.delete(`/tasky/groups/${groupId}`); } + public async getPendingSolutions(page: number): Promise { + return await this.get(`/tasky/tutor_solutions?page=${page}`); + } + + public async getPendingWishes(page: number): Promise { + return await this.get(`/tasky/tutor_assignment_wishes?page=${page}`); + } + + public async getPendingAssignments(page: number): Promise { + return await this.get(`/tasky/student_pending_assignments?page=${page}`); + } + public async createOrUpdateCodeTests( groupId: number, assignmentId: number, diff --git a/web/service/types/tasky.ts b/web/service/types/tasky.ts index f62f1b5..e01b014 100644 --- a/web/service/types/tasky.ts +++ b/web/service/types/tasky.ts @@ -66,6 +66,7 @@ export interface Assignment { runner_timeout: string | null; runner_cmd: string | null; completed: boolean | null; + group_id: number; } export interface AssignmentsResponse { diff --git a/web/static/routes.tsx b/web/static/routes.tsx index 1a42ebf..923749a 100644 --- a/web/static/routes.tsx +++ b/web/static/routes.tsx @@ -1,7 +1,8 @@ import { UserRoles } from "@/service/types/usernator"; import { + IconAssembly, IconDashboard, - IconFile, + IconFile, IconGift, IconSchool, IconUsersGroup, } from "@tabler/icons-react"; @@ -57,6 +58,27 @@ export const routes: Route[] = [ icon: , authRoles: [UserRoles.Student], }, + { + path: "/pending-solutions", + name: "solutions", + description: "All the pending solutions", + icon: , + authRoles: [UserRoles.Tutor], + }, + { + path: "/pending-wishes", + name: "wishes", + description: "All the pending wishes", + icon: , + authRoles: [UserRoles.Tutor], + }, + { + path: "/pending-assignments", + name: "assignments", + description: "All the assignments wishes", + icon: , + authRoles: [UserRoles.Student], + }, ]; export const publicRoutes = [