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))]
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 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#[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#[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}