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