Skip to content

Commit

Permalink
Merge pull request #153 from MathisBurger/feature/tutor-solutions-view
Browse files Browse the repository at this point in the history
Direct views
  • Loading branch information
MathisBurger authored Nov 18, 2024
2 parents fbc1992 + ce604f5 commit 10c5979
Show file tree
Hide file tree
Showing 23 changed files with 365 additions and 20 deletions.
23 changes: 23 additions & 0 deletions tasky/src/models/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -155,4 +156,26 @@ impl AssignmentRepository {
.get_results::<i32>(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<Assignment> {
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::<Assignment>(conn)
.expect("Cannot fetch pending assignments for student")
}
}
15 changes: 15 additions & 0 deletions tasky/src/models/assignment_wish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssignmentWish> {
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::<AssignmentWish>(conn)
.expect("Cannot fetch tutor assignment wishes")
}
}
21 changes: 21 additions & 0 deletions tasky/src/models/solution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,25 @@ impl SolutionRepository {
.get_result::<Solution>(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<Solution> {
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::<Solution>(conn)
.expect("Cannot load pending solutions for tutor")
}
}
2 changes: 2 additions & 0 deletions tasky/src/response/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub struct MinifiedAssignmentResponse {
pub description: String,
pub language: AssignmentLanguage,
pub completed: Option<bool>,
pub group_id: i32,
}

/// A vec of assignments
Expand All @@ -83,6 +84,7 @@ impl Enrich<Assignment> for MinifiedAssignmentResponse {
description: from.description.clone(),
language: from.language.clone(),
completed: None,
group_id: from.group_id,
})
}
}
Expand Down
46 changes: 35 additions & 11 deletions tasky/src/routes/assignment.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Option<NaiveDateTime>, D::Error>
Expand Down Expand Up @@ -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<AppState>,
user: web::ReqData<UserData>,
pagination: web::Query<PaginationParams>,
) -> Result<HttpResponse, ApiError> {
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(
Expand Down
20 changes: 20 additions & 0 deletions tasky/src/routes/assignment_wish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<AppState>,
user: web::ReqData<UserData>,
pagination: web::Query<PaginationParams>,
) -> Result<HttpResponse, ApiError> {
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))
}
3 changes: 3 additions & 0 deletions tasky/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,20 @@ 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)
.service(solution::get_solutions_for_user)
.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)
Expand Down
24 changes: 24 additions & 0 deletions tasky/src/routes/solution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppState>,
user: web::ReqData<UserData>,
pagination: web::Query<PaginationParams>,
) -> Result<HttpResponse, ApiError> {
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(
Expand Down
34 changes: 34 additions & 0 deletions web/app/pending-assignments/page.tsx
Original file line number Diff line number Diff line change
@@ -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<number>(1);
const [assignments] = useClientQuery(() => api.getPendingAssignments(page), [page]);
const { t } = useTranslation("assignment");

return (
<Container fluid>
<Title mb={4}>{t('assignment:titles.pending-assignments')}</Title>
<Stack gap={20}>
{(assignments?.assignments ?? []).map((assignment) => (
<AssignmentCard assignment={assignment} groupId={assignment.group_id} key={assignment.id} />
))}
</Stack>
<Pagination
total={Math.ceil((assignments?.total ?? 0) / 50)}
value={page}
onChange={setPage}
/>
</Container>
);
}

export default PendingAssignmentsPage;
64 changes: 64 additions & 0 deletions web/app/pending-solutions/page.tsx
Original file line number Diff line number Diff line change
@@ -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<number>(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) => <SolutionBadge status={value as string} />,
},
];

const rowActions: EntityListRowAction[] = [
{
name: t("actions.view"),
onClick: (row) => router.push(`/solutions/${row.id}`),
color: undefined,
auth: [UserRoles.Tutor],
},
];

return (
<Container fluid>
<Title>{t('solution:titles.pending-solutions')}</Title>
<EntityList
cols={cols}
rowActions={rowActions}
rows={solutions?.solutions ?? []}
/>
<Pagination
total={Math.ceil((solutions?.total ?? 0) / 50)}
value={page}
onChange={setPage}
/>
</Container>
)
}

export default PendingSolutions;
Loading

0 comments on commit 10c5979

Please sign in to comment.