Skip to main content

headless_lms_server/controllers/main_frontend/
exercise_slide_submissions.rs

1use crate::{domain::models_requests, prelude::*};
2use headless_lms_models::exercise_slide_submissions::ExerciseSlideSubmissionInfo;
3use models::{
4    teacher_grading_decisions::{
5        NewTeacherGradingDecision, TeacherDecisionType, TeacherGradingDecision,
6    },
7    user_exercise_states::UserExerciseState,
8};
9use utoipa::{OpenApi, ToSchema};
10
11#[derive(OpenApi)]
12#[openapi(paths(get_submission_info, get_user_exercise_state_info, add_teacher_grading))]
13pub(crate) struct MainFrontendExerciseSlideSubmissionsApiDoc;
14
15/**
16GET `/api/v0/main-frontend/exercise-slide-submissions/{submission_id}/info"`- Returns data necessary for rendering a submission.
17*/
18#[utoipa::path(
19    get,
20    path = "/{submission_id}/info",
21    operation_id = "getExerciseSlideSubmissionInfo",
22    tag = "exercise_slide_submissions",
23    params(
24        ("submission_id" = Uuid, Path, description = "Exercise slide submission id")
25    ),
26    responses(
27        (status = 200, description = "Exercise slide submission info", body = ExerciseSlideSubmissionInfo)
28    )
29)]
30#[instrument(skip(pool))]
31async fn get_submission_info(
32    submission_id: web::Path<Uuid>,
33    pool: web::Data<PgPool>,
34    user: AuthUser,
35) -> ControllerResult<web::Json<ExerciseSlideSubmissionInfo>> {
36    let mut conn = pool.acquire().await?;
37    let token = authorize(
38        &mut conn,
39        Act::Teach,
40        Some(user.id),
41        Res::ExerciseSlideSubmission(*submission_id),
42    )
43    .await?;
44
45    let submission_id_uuid = submission_id.into_inner();
46
47    // First get the submission to find the correct user_id
48    let submission =
49        models::exercise_slide_submissions::get_by_id(&mut conn, submission_id_uuid).await?;
50
51    let res = models::exercise_slide_submissions::get_exercise_slide_submission_info(
52        &mut conn,
53        submission_id_uuid,
54        submission.user_id,
55        models_requests::fetch_service_info,
56        true,
57    )
58    .await?;
59
60    token.authorized_ok(web::Json(res))
61}
62
63#[derive(Debug, Deserialize, ToSchema)]
64
65pub struct ExerciseStateIds {
66    exercise_id: Uuid,
67    user_id: Uuid,
68}
69/**
70GET `/api/v0/main-frontend/exercise-slide-submissions/{exam_id}/{exercise_id}/{user_id}/user-exercise-state-info`-
71*/
72#[utoipa::path(
73    get,
74    path = "/{exam_id}/user-exercise-state-info",
75    operation_id = "getExamUserExerciseStateInfo",
76    tag = "exercise_slide_submissions",
77    params(
78        ("exam_id" = Uuid, Path, description = "Exam id"),
79        ("exercise_id" = Uuid, Query, description = "Exercise id"),
80        ("user_id" = Uuid, Query, description = "User id")
81    ),
82    responses(
83        (status = 200, description = "User exercise state for the exam submission", body = UserExerciseState)
84    )
85)]
86#[instrument(skip(pool))]
87async fn get_user_exercise_state_info(
88    exam_id: web::Path<Uuid>,
89    pool: web::Data<PgPool>,
90    query_ids: web::Query<ExerciseStateIds>,
91    user: AuthUser,
92) -> ControllerResult<web::Json<UserExerciseState>> {
93    let mut conn = pool.acquire().await?;
94    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
95
96    let res = models::user_exercise_states::get_or_create_user_exercise_state(
97        &mut conn,
98        query_ids.user_id,
99        query_ids.exercise_id,
100        None,
101        Some(*exam_id),
102    )
103    .await?;
104    token.authorized_ok(web::Json(res))
105}
106
107/**
108PUT `/api/v0/main-frontend/exercise-slide-submissions/add_teacher_grading"` - Adds a new teacher grading decision, without updating user exercise state
109*/
110#[utoipa::path(
111    put,
112    path = "/add-teacher-grading-for-exam-submission",
113    operation_id = "addTeacherGradingForExamSubmission",
114    tag = "exercise_slide_submissions",
115    request_body = NewTeacherGradingDecision,
116    responses(
117        (status = 200, description = "Created teacher grading decision", body = TeacherGradingDecision)
118    )
119)]
120#[instrument(skip(pool))]
121async fn add_teacher_grading(
122    payload: web::Json<NewTeacherGradingDecision>,
123    pool: web::Data<PgPool>,
124    user: AuthUser,
125) -> ControllerResult<web::Json<TeacherGradingDecision>> {
126    let action = &payload.action;
127    let exercise_id = payload.exercise_id;
128    let user_exercise_state_id = payload.user_exercise_state_id;
129    let manual_points = payload.manual_points;
130    let justification = &payload.justification;
131    let mut conn = pool.acquire().await?;
132
133    let student_state =
134        models::user_exercise_states::get_by_id(&mut conn, user_exercise_state_id).await?;
135    if student_state.exercise_id != exercise_id {
136        return Err(controller_err!(
137            Forbidden,
138            "User exercise state does not belong to the requested exercise".to_string()
139        ));
140    }
141    let exercise =
142        models::exercises::get_non_deleted_by_id(&mut conn, student_state.exercise_id).await?;
143    if exercise.course_id != student_state.course_id || exercise.exam_id != student_state.exam_id {
144        return Err(controller_err!(
145            Forbidden,
146            "User exercise state does not match the requested exercise context".to_string()
147        ));
148    }
149
150    let token = authorize(
151        &mut conn,
152        Act::Edit,
153        Some(user.id),
154        Res::Exercise(student_state.exercise_id),
155    )
156    .await?;
157
158    let points_given;
159    if *action == TeacherDecisionType::CustomPoints {
160        let max_points = exercise.score_maximum as f32;
161
162        points_given = manual_points.unwrap_or(0.0);
163
164        if max_points < points_given {
165            return Err(ControllerError::new(
166                ControllerErrorType::BadRequest,
167                "Cannot give more points than maximum score".to_string(),
168                None,
169            ));
170        }
171    } else {
172        return Err(ControllerError::new(
173            ControllerErrorType::BadRequest,
174            "Invalid query".to_string(),
175            None,
176        ));
177    }
178
179    info!(
180        "Teacher took the following action: {:?}. Points given: {:?}.",
181        &action, points_given
182    );
183
184    let res = models::teacher_grading_decisions::upsert_by_state_id_and_exercise_id(
185        &mut conn,
186        user_exercise_state_id,
187        student_state.exercise_id,
188        *action,
189        points_given,
190        Some(user.id),
191        justification.clone(),
192        true,
193    )
194    .await?;
195
196    token.authorized_ok(web::Json(res))
197}
198
199pub fn _add_routes(cfg: &mut ServiceConfig) {
200    cfg.route("/{submission_id}/info", web::get().to(get_submission_info))
201        .route(
202            "/{exam_id}/user-exercise-state-info",
203            web::get().to(get_user_exercise_state_info),
204        )
205        .route(
206            "/add-teacher-grading-for-exam-submission",
207            web::put().to(add_teacher_grading),
208        );
209}