headless_lms_server/controllers/course_material/
exercises.rs1use crate::{
4 domain::{
5 authorization::skip_authorize,
6 models_requests::{self, GivePeerReviewClaim, JwtKey},
7 },
8 prelude::*,
9};
10use headless_lms_models::flagged_answers::FlaggedAnswer;
11use models::{
12 exercise_task_submissions::PeerOrSelfReviewsReceived,
13 exercises::CourseMaterialExercise,
14 flagged_answers::NewFlaggedAnswerWithToken,
15 library::{
16 grading::{StudentExerciseSlideSubmission, StudentExerciseSlideSubmissionResult},
17 peer_or_self_reviewing::{
18 CourseMaterialPeerOrSelfReviewData, CourseMaterialPeerOrSelfReviewSubmission,
19 },
20 },
21 user_exercise_states,
22};
23
24#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
25#[cfg_attr(feature = "ts_rs", derive(TS))]
26pub struct CourseMaterialPeerOrSelfReviewDataWithToken {
27 pub course_material_peer_or_self_review_data: CourseMaterialPeerOrSelfReviewData,
28 pub token: Option<String>,
29}
30
31#[instrument(skip(pool))]
39async fn get_exercise(
40 pool: web::Data<PgPool>,
41 exercise_id: web::Path<Uuid>,
42 user: Option<AuthUser>,
43) -> ControllerResult<web::Json<CourseMaterialExercise>> {
44 let mut conn = pool.acquire().await?;
45 let user_id = user.map(|u| u.id);
46 let mut course_material_exercise = models::exercises::get_course_material_exercise(
47 &mut conn,
48 user_id,
49 *exercise_id,
50 models_requests::fetch_service_info,
51 )
52 .await?;
53
54 let mut should_clear_grading_information = true;
55 if let Some(exam_id) = course_material_exercise.exercise.exam_id {
57 let user_enrollment =
58 models::exams::get_enrollment(&mut conn, exam_id, user_id.unwrap()).await?;
59
60 if let Some(enrollment) = user_enrollment {
61 if let Some(show_answers) = enrollment.show_exercise_answers {
62 if enrollment.is_teacher_testing && show_answers {
63 should_clear_grading_information = false;
64 }
65 }
66 }
67 }
68
69 if course_material_exercise.can_post_submission
70 && course_material_exercise.exercise.exam_id.is_some()
71 && should_clear_grading_information
72 {
73 course_material_exercise.clear_grading_information();
75 }
76
77 let score_given: f32 = if let Some(status) = &course_material_exercise.exercise_status {
78 status.score_given.unwrap_or(0.0)
79 } else {
80 0.0
81 };
82
83 let submission_count = course_material_exercise
84 .exercise_slide_submission_counts
85 .get(&course_material_exercise.current_exercise_slide.id)
86 .unwrap_or(&0);
87
88 let out_of_tries = course_material_exercise.exercise.limit_number_of_tries
89 && *submission_count as i32
90 >= course_material_exercise
91 .exercise
92 .max_tries_per_slide
93 .unwrap_or(i32::MAX);
94
95 let has_received_full_points = score_given
98 >= course_material_exercise.exercise.score_maximum as f32
99 || (score_given - course_material_exercise.exercise.score_maximum as f32).abs() < 0.0001;
100 if !has_received_full_points && !out_of_tries {
101 course_material_exercise.clear_model_solution_specs();
102 }
103 let token = skip_authorize();
104 token.authorized_ok(web::Json(course_material_exercise))
105}
106
107#[instrument(skip(pool))]
113async fn get_peer_review_for_exercise(
114 pool: web::Data<PgPool>,
115 exercise_id: web::Path<Uuid>,
116 user: AuthUser,
117 jwt_key: web::Data<JwtKey>,
118) -> ControllerResult<web::Json<CourseMaterialPeerOrSelfReviewDataWithToken>> {
119 let mut conn = pool.acquire().await?;
120 let course_material_peer_or_self_review_data =
121 models::peer_or_self_review_configs::get_course_material_peer_or_self_review_data(
122 &mut conn,
123 user.id,
124 *exercise_id,
125 models_requests::fetch_service_info,
126 )
127 .await?;
128 let token = authorize(
129 &mut conn,
130 Act::View,
131 Some(user.id),
132 Res::Exercise(*exercise_id),
133 )
134 .await?;
135 let give_peer_review_claim =
136 if let Some(to_review) = &course_material_peer_or_self_review_data.answer_to_review {
137 Some(
138 GivePeerReviewClaim::expiring_in_1_day(
139 to_review.exercise_slide_submission_id,
140 course_material_peer_or_self_review_data
141 .peer_or_self_review_config
142 .id,
143 )
144 .sign(&jwt_key),
145 )
146 } else {
147 None
148 };
149
150 let res = CourseMaterialPeerOrSelfReviewDataWithToken {
151 course_material_peer_or_self_review_data,
152 token: give_peer_review_claim,
153 };
154 token.authorized_ok(web::Json(res))
155}
156
157#[instrument(skip(pool))]
161async fn get_peer_reviews_received(
162 pool: web::Data<PgPool>,
163 params: web::Path<(Uuid, Uuid)>,
164 user: AuthUser,
165) -> ControllerResult<web::Json<PeerOrSelfReviewsReceived>> {
166 let mut conn = pool.acquire().await?;
167 let (exercise_id, exercise_slide_submission_id) = params.into_inner();
168 let peer_review_data = models::exercise_task_submissions::get_peer_reviews_received(
169 &mut conn,
170 exercise_id,
171 exercise_slide_submission_id,
172 user.id,
173 )
174 .await?;
175 let token = skip_authorize();
176 token.authorized_ok(web::Json(peer_review_data))
177}
178
179#[instrument(skip(pool))]
200async fn post_submission(
201 pool: web::Data<PgPool>,
202 jwt_key: web::Data<JwtKey>,
203 exercise_id: web::Path<Uuid>,
204 payload: web::Json<StudentExerciseSlideSubmission>,
205 user: AuthUser,
206) -> ControllerResult<web::Json<StudentExerciseSlideSubmissionResult>> {
207 let submission = payload.0;
208 let mut conn = pool.acquire().await?;
209 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
210 let token = authorize(
211 &mut conn,
212 Act::View,
213 Some(user.id),
214 Res::Exercise(exercise.id),
215 )
216 .await?;
217 let result = domain::exercises::process_submission(
218 &mut conn,
219 user.id,
220 exercise.clone(),
221 &submission,
222 jwt_key.into_inner(),
223 )
224 .await;
225 return match result {
226 Ok(res) => token.authorized_ok(web::Json(res)),
227 Err(err) => {
228 match models::rejected_exercise_slide_submissions::insert_rejected_exercise_slide_submission(
229 &mut conn,
230 &submission,
231 user.id,
232 )
233 .await {
234 Ok(_) => {
235 warn!(
236 "Submission was rejected but it was saved for debugging purposes. User id: {}, Exercise id: {}",
237 user.id, exercise.id
238 );
239 },
240 Err(_) => {
241 error!(
242 "Submission was rejected and saving it for debugging purposes failed. User id: {}, Exercise id: {}",
243 user.id, exercise.id
244 );
245 },
246 }
247 Err(err)
248 }
249 };
250}
251
252#[instrument(skip(pool))]
260async fn start_peer_or_self_review(
261 pool: web::Data<PgPool>,
262 exercise_id: web::Path<Uuid>,
263 user: AuthUser,
264) -> ControllerResult<web::Json<bool>> {
265 let mut conn = pool.acquire().await?;
266
267 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
268 let user_exercise_state =
269 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
270 let token = authorize(
271 &mut conn,
272 Act::View,
273 Some(user.id),
274 Res::Exercise(*exercise_id),
275 )
276 .await?;
277 models::library::peer_or_self_reviewing::start_peer_or_self_review_for_user(
278 &mut conn,
279 user_exercise_state,
280 &exercise,
281 )
282 .await?;
283
284 token.authorized_ok(web::Json(true))
285}
286
287#[instrument(skip(pool))]
292async fn submit_peer_or_self_review(
293 pool: web::Data<PgPool>,
294 exercise_id: web::Path<Uuid>,
295 payload: web::Json<CourseMaterialPeerOrSelfReviewSubmission>,
296 user: AuthUser,
297 jwt_key: web::Data<JwtKey>,
298) -> ControllerResult<web::Json<bool>> {
299 let mut conn = pool.acquire().await?;
300 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
301 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
304 if claim.exercise_slide_submission_id != payload.exercise_slide_submission_id
305 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
306 {
307 return Err(ControllerError::new(
308 ControllerErrorType::BadRequest,
309 "You are not allowed to review this answer.".to_string(),
310 None,
311 ));
312 }
313
314 let giver_user_exercise_state =
315 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
316 let exercise_slide_submission: models::exercise_slide_submissions::ExerciseSlideSubmission =
317 models::exercise_slide_submissions::get_by_id(
318 &mut conn,
319 payload.exercise_slide_submission_id,
320 )
321 .await?;
322
323 if let Some(receiver_course_instance_id) = exercise_slide_submission.course_instance_id {
324 let receiver_user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
325 &mut conn,
326 exercise_slide_submission.user_id,
327 exercise.id,
328 user_exercise_states::CourseInstanceOrExamId::Instance(receiver_course_instance_id),
329 )
330 .await?;
331 if let Some(receiver_user_exercise_state) = receiver_user_exercise_state {
332 models::library::peer_or_self_reviewing::create_peer_or_self_review_submission_for_user(
333 &mut conn,
334 &exercise,
335 giver_user_exercise_state,
336 receiver_user_exercise_state,
337 payload.0,
338 )
339 .await?;
340 } else {
341 warn!(
342 "No user exercise state found for receiver's exercise slide submission id: {}",
343 exercise_slide_submission.id
344 );
345 return Err(ControllerError::new(
346 ControllerErrorType::BadRequest,
347 "No user exercise state found for receiver's exercise slide submission."
348 .to_string(),
349 None,
350 ));
351 }
352 } else {
353 warn!(
354 "No course instance id found for receiver's exercise slide submission id: {}",
355 exercise_slide_submission.id
356 );
357 return Err(ControllerError::new(
358 ControllerErrorType::BadRequest,
359 "No course instance id found for receiver's exercise slide submission.".to_string(),
360 None,
361 ));
362 }
363 let token = skip_authorize();
364 token.authorized_ok(web::Json(true))
365}
366
367#[instrument(skip(pool))]
371async fn post_flag_answer_in_peer_review(
372 pool: web::Data<PgPool>,
373 payload: web::Json<NewFlaggedAnswerWithToken>,
374 user: AuthUser,
375 jwt_key: web::Data<JwtKey>,
376) -> ControllerResult<web::Json<FlaggedAnswer>> {
377 let mut conn = pool.acquire().await?;
378
379 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
380 if claim.exercise_slide_submission_id != payload.submission_id
381 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
382 {
383 return Err(ControllerError::new(
384 ControllerErrorType::BadRequest,
385 "You are not allowed to report this answer.".to_string(),
386 None,
387 ));
388 }
389
390 let insert_result =
391 models::flagged_answers::insert_flagged_answer_and_move_to_manual_review_if_needed(
392 &mut conn,
393 payload.into_inner(),
394 user.id,
395 )
396 .await?;
397
398 let token = skip_authorize();
399 token.authorized_ok(web::Json(insert_result))
400}
401
402pub fn _add_routes(cfg: &mut ServiceConfig) {
410 cfg.route("/{exercise_id}", web::get().to(get_exercise))
411 .route(
412 "/{exercise_id}/peer-or-self-reviews",
413 web::post().to(submit_peer_or_self_review),
414 )
415 .route(
416 "/{exercise_id}/peer-or-self-reviews/start",
417 web::post().to(start_peer_or_self_review),
418 )
419 .route(
420 "/{exercise_id}/peer-review",
421 web::get().to(get_peer_review_for_exercise),
422 )
423 .route(
424 "/{exercise_id}/exercise-slide-submission/{exercise_slide_submission_id}/peer-or-self-reviews-received",
425 web::get().to(get_peer_reviews_received),
426 )
427 .route(
428 "/{exercise_id}/submissions",
429 web::post().to(post_submission),
430 ).route(
431 "/{exercise_id}/flag-peer-review-answer",
432 web::post().to(post_flag_answer_in_peer_review),
433 );
434}