headless_lms_server/controllers/main_frontend/
exercise_slide_submissions.rs1use 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#[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 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#[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#[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}