1use crate::{
4 domain::{
5 authorization::skip_authorize,
6 models_requests::{self, GivePeerReviewClaim, JwtKey},
7 },
8 prelude::*,
9};
10use headless_lms_models::{
11 ModelError, ModelErrorType, flagged_answers::FlaggedAnswer, peer_or_self_review_configs,
12};
13use models::{
14 exercise_task_submissions::PeerOrSelfReviewsReceived,
15 exercises::CourseMaterialExercise,
16 flagged_answers::NewFlaggedAnswerWithToken,
17 library::{
18 grading::{StudentExerciseSlideSubmission, StudentExerciseSlideSubmissionResult},
19 peer_or_self_reviewing::{
20 CourseMaterialPeerOrSelfReviewData, CourseMaterialPeerOrSelfReviewSubmission,
21 },
22 },
23 user_chapter_locking_statuses, user_exercise_states,
24};
25
26#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
27#[cfg_attr(feature = "ts_rs", derive(TS))]
28pub struct CourseMaterialPeerOrSelfReviewDataWithToken {
29 pub course_material_peer_or_self_review_data: CourseMaterialPeerOrSelfReviewData,
30 pub token: Option<String>,
31}
32
33#[instrument(skip(pool))]
41async fn get_exercise(
42 pool: web::Data<PgPool>,
43 exercise_id: web::Path<Uuid>,
44 user: Option<AuthUser>,
45) -> ControllerResult<web::Json<CourseMaterialExercise>> {
46 let mut conn = pool.acquire().await?;
47 let user_id = user.map(|u| u.id);
48 let mut course_material_exercise = models::exercises::get_course_material_exercise(
49 &mut conn,
50 user_id,
51 *exercise_id,
52 models_requests::fetch_service_info,
53 )
54 .await?;
55
56 let mut should_clear_grading_information = true;
57 if let Some(exam_id) = course_material_exercise.exercise.exam_id {
59 let user_id_for_exam = user_id.ok_or_else(|| {
60 ControllerError::new(
61 ControllerErrorType::Unauthorized,
62 "User must be authenticated to view exam exercises".to_string(),
63 None,
64 )
65 })?;
66 let user_enrollment =
67 models::exams::get_enrollment(&mut conn, exam_id, user_id_for_exam).await?;
68
69 if let Some(enrollment) = user_enrollment
70 && let Some(show_answers) = enrollment.show_exercise_answers
71 && enrollment.is_teacher_testing
72 && show_answers
73 {
74 should_clear_grading_information = false;
75 }
76 }
77
78 if course_material_exercise.can_post_submission
79 && course_material_exercise.exercise.exam_id.is_some()
80 && should_clear_grading_information
81 {
82 course_material_exercise.clear_grading_information();
84 }
85
86 let score_given: f32 = if let Some(status) = &course_material_exercise.exercise_status {
87 status.score_given.unwrap_or(0.0)
88 } else {
89 0.0
90 };
91
92 let submission_count = course_material_exercise
93 .exercise_slide_submission_counts
94 .get(&course_material_exercise.current_exercise_slide.id)
95 .unwrap_or(&0);
96
97 let out_of_tries = course_material_exercise.exercise.limit_number_of_tries
98 && *submission_count as i32
99 >= course_material_exercise
100 .exercise
101 .max_tries_per_slide
102 .unwrap_or(i32::MAX);
103
104 let has_received_full_points = score_given
107 >= course_material_exercise.exercise.score_maximum as f32
108 || (score_given - course_material_exercise.exercise.score_maximum as f32).abs() < 0.0001;
109 if !has_received_full_points && !out_of_tries {
110 course_material_exercise.clear_model_solution_specs();
111 }
112 let token = skip_authorize();
113 token.authorized_ok(web::Json(course_material_exercise))
114}
115
116#[instrument(skip(pool))]
122async fn get_peer_review_for_exercise(
123 pool: web::Data<PgPool>,
124 exercise_id: web::Path<Uuid>,
125 user: AuthUser,
126 jwt_key: web::Data<JwtKey>,
127) -> ControllerResult<web::Json<CourseMaterialPeerOrSelfReviewDataWithToken>> {
128 let mut conn = pool.acquire().await?;
129 let course_material_peer_or_self_review_data =
130 models::peer_or_self_review_configs::get_course_material_peer_or_self_review_data(
131 &mut conn,
132 user.id,
133 *exercise_id,
134 models_requests::fetch_service_info,
135 )
136 .await?;
137 let token = authorize(
138 &mut conn,
139 Act::View,
140 Some(user.id),
141 Res::Exercise(*exercise_id),
142 )
143 .await?;
144 let give_peer_review_claim =
145 if let Some(to_review) = &course_material_peer_or_self_review_data.answer_to_review {
146 Some(
147 GivePeerReviewClaim::expiring_in_1_day(
148 to_review.exercise_slide_submission_id,
149 course_material_peer_or_self_review_data
150 .peer_or_self_review_config
151 .id,
152 )
153 .sign(&jwt_key),
154 )
155 } else {
156 None
157 };
158
159 let res = CourseMaterialPeerOrSelfReviewDataWithToken {
160 course_material_peer_or_self_review_data,
161 token: give_peer_review_claim,
162 };
163 token.authorized_ok(web::Json(res))
164}
165
166#[instrument(skip(pool))]
170async fn get_peer_reviews_received(
171 pool: web::Data<PgPool>,
172 params: web::Path<(Uuid, Uuid)>,
173 user: AuthUser,
174) -> ControllerResult<web::Json<PeerOrSelfReviewsReceived>> {
175 let mut conn = pool.acquire().await?;
176 let (exercise_id, exercise_slide_submission_id) = params.into_inner();
177 let peer_review_data = models::exercise_task_submissions::get_peer_reviews_received(
178 &mut conn,
179 exercise_id,
180 exercise_slide_submission_id,
181 user.id,
182 )
183 .await?;
184 let token = skip_authorize();
185 token.authorized_ok(web::Json(peer_review_data))
186}
187
188#[instrument(skip(pool))]
209async fn post_submission(
210 pool: web::Data<PgPool>,
211 jwt_key: web::Data<JwtKey>,
212 exercise_id: web::Path<Uuid>,
213 payload: web::Json<StudentExerciseSlideSubmission>,
214 user: AuthUser,
215) -> ControllerResult<web::Json<StudentExerciseSlideSubmissionResult>> {
216 let submission = payload.0;
217 let mut conn = pool.acquire().await?;
218 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
219
220 if let Some(chapter_id) = exercise.chapter_id {
221 let course_id = models::chapters::get_course_id(&mut conn, chapter_id).await?;
222 let is_accessible = user_chapter_locking_statuses::is_chapter_accessible(
223 &mut conn, user.id, chapter_id, course_id,
224 )
225 .await?;
226 if !is_accessible {
227 return Err(ControllerError::new(
228 ControllerErrorType::Forbidden,
229 "Complete and lock the previous chapter to unlock exercises in this chapter."
230 .to_string(),
231 None,
232 ));
233 }
234 let exercises_locked = user_chapter_locking_statuses::is_chapter_exercises_locked(
235 &mut conn, user.id, chapter_id, course_id,
236 )
237 .await?;
238 if exercises_locked {
239 return Err(ControllerError::new(
240 ControllerErrorType::Forbidden,
241 "The current chapter is locked, and you can no longer submit exercises."
242 .to_string(),
243 None,
244 ));
245 }
246 }
247
248 let token = authorize(
249 &mut conn,
250 Act::View,
251 Some(user.id),
252 Res::Exercise(exercise.id),
253 )
254 .await?;
255 let result = domain::exercises::process_submission(
256 &mut conn,
257 user.id,
258 exercise.clone(),
259 &submission,
260 jwt_key.into_inner(),
261 )
262 .await?;
263 token.authorized_ok(web::Json(result))
264}
265
266#[instrument(skip(pool))]
274async fn start_peer_or_self_review(
275 pool: web::Data<PgPool>,
276 exercise_id: web::Path<Uuid>,
277 user: AuthUser,
278) -> ControllerResult<web::Json<bool>> {
279 let mut conn = pool.acquire().await?;
280
281 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
282
283 if let Some(chapter_id) = exercise.chapter_id {
284 let course_id = models::chapters::get_course_id(&mut conn, chapter_id).await?;
285 let is_accessible = user_chapter_locking_statuses::is_chapter_accessible(
286 &mut conn, user.id, chapter_id, course_id,
287 )
288 .await?;
289 if !is_accessible {
290 return Err(ControllerError::new(
291 ControllerErrorType::Forbidden,
292 "Complete and lock the previous chapter to unlock exercises in this chapter."
293 .to_string(),
294 None,
295 ));
296 }
297 let exercises_locked = user_chapter_locking_statuses::is_chapter_exercises_locked(
298 &mut conn, user.id, chapter_id, course_id,
299 )
300 .await?;
301 if exercises_locked {
302 return Err(ControllerError::new(
303 ControllerErrorType::Forbidden,
304 "The current chapter is locked, and you can no longer submit exercises."
305 .to_string(),
306 None,
307 ));
308 }
309 }
310
311 let user_exercise_state =
312 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
313 let token = authorize(
314 &mut conn,
315 Act::View,
316 Some(user.id),
317 Res::Exercise(*exercise_id),
318 )
319 .await?;
320 models::library::peer_or_self_reviewing::start_peer_or_self_review_for_user(
321 &mut conn,
322 user_exercise_state,
323 &exercise,
324 )
325 .await?;
326
327 token.authorized_ok(web::Json(true))
328}
329
330#[instrument(skip(pool))]
335async fn submit_peer_or_self_review(
336 pool: web::Data<PgPool>,
337 exercise_id: web::Path<Uuid>,
338 payload: web::Json<CourseMaterialPeerOrSelfReviewSubmission>,
339 user: AuthUser,
340 jwt_key: web::Data<JwtKey>,
341) -> ControllerResult<web::Json<bool>> {
342 let mut conn = pool.acquire().await?;
343 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
344
345 if let Some(chapter_id) = exercise.chapter_id {
346 let course_id = models::chapters::get_course_id(&mut conn, chapter_id).await?;
347 let is_accessible = user_chapter_locking_statuses::is_chapter_accessible(
348 &mut conn, user.id, chapter_id, course_id,
349 )
350 .await?;
351 if !is_accessible {
352 return Err(ControllerError::new(
353 ControllerErrorType::Forbidden,
354 "Complete and lock the previous chapter to unlock exercises in this chapter."
355 .to_string(),
356 None,
357 ));
358 }
359 let exercises_locked = user_chapter_locking_statuses::is_chapter_exercises_locked(
360 &mut conn, user.id, chapter_id, course_id,
361 )
362 .await?;
363 if exercises_locked {
364 return Err(ControllerError::new(
365 ControllerErrorType::Forbidden,
366 "The current chapter is locked, and you can no longer submit exercises."
367 .to_string(),
368 None,
369 ));
370 }
371 }
372
373 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
376 if claim.exercise_slide_submission_id != payload.exercise_slide_submission_id
377 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
378 {
379 return Err(ControllerError::new(
380 ControllerErrorType::BadRequest,
381 "You are not allowed to review this answer.".to_string(),
382 None,
383 ));
384 }
385
386 let giver_user_exercise_state =
387 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
388 let exercise_slide_submission: models::exercise_slide_submissions::ExerciseSlideSubmission =
389 models::exercise_slide_submissions::get_by_id(
390 &mut conn,
391 payload.exercise_slide_submission_id,
392 )
393 .await?;
394
395 if let Some(receiver_course_id) = exercise_slide_submission.course_id {
396 let receiver_user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
397 &mut conn,
398 exercise_slide_submission.user_id,
399 exercise.id,
400 CourseOrExamId::Course(receiver_course_id),
401 )
402 .await?;
403 if let Some(receiver_user_exercise_state) = receiver_user_exercise_state {
404 let mut tx = conn.begin().await?;
405
406 models::library::peer_or_self_reviewing::create_peer_or_self_review_submission_for_user(
407 &mut tx,
408 &exercise,
409 giver_user_exercise_state,
410 receiver_user_exercise_state,
411 payload.0,
412 )
413 .await?;
414
415 let updated_receiver_state = user_exercise_states::get_user_exercise_state_if_exists(
417 &mut tx,
418 exercise_slide_submission.user_id,
419 exercise.id,
420 CourseOrExamId::Course(receiver_course_id),
421 )
422 .await?
423 .ok_or_else(|| {
424 ModelError::new(
425 ModelErrorType::Generic,
426 "Receiver exercise state not found".to_string(),
427 None,
428 )
429 })?;
430
431 let peer_or_self_review_config =
432 peer_or_self_review_configs::get_by_exercise_or_course_id(
433 &mut tx,
434 &exercise,
435 exercise.get_course_id()?,
436 )
437 .await?;
438
439 let _ = models::library::peer_or_self_reviewing::reset_exercise_if_needed_if_zero_points_from_review(
440 &mut tx,
441 &peer_or_self_review_config,
442 &updated_receiver_state,
443 ).await?;
444
445 tx.commit().await?;
446 } else {
447 warn!(
448 "No user exercise state found for receiver's exercise slide submission id: {}",
449 exercise_slide_submission.id
450 );
451 return Err(ControllerError::new(
452 ControllerErrorType::BadRequest,
453 "No user exercise state found for receiver's exercise slide submission."
454 .to_string(),
455 None,
456 ));
457 }
458 } else {
459 warn!(
460 "No course instance id found for receiver's exercise slide submission id: {}",
461 exercise_slide_submission.id
462 );
463 return Err(ControllerError::new(
464 ControllerErrorType::BadRequest,
465 "No course instance id found for receiver's exercise slide submission.".to_string(),
466 None,
467 ));
468 }
469 let token = skip_authorize();
470 token.authorized_ok(web::Json(true))
471}
472
473#[instrument(skip(pool))]
477async fn post_flag_answer_in_peer_review(
478 pool: web::Data<PgPool>,
479 payload: web::Json<NewFlaggedAnswerWithToken>,
480 user: AuthUser,
481 jwt_key: web::Data<JwtKey>,
482) -> ControllerResult<web::Json<FlaggedAnswer>> {
483 let mut conn = pool.acquire().await?;
484
485 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
486 if claim.exercise_slide_submission_id != payload.submission_id
487 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
488 {
489 return Err(ControllerError::new(
490 ControllerErrorType::BadRequest,
491 "You are not allowed to report this answer.".to_string(),
492 None,
493 ));
494 }
495
496 let insert_result =
497 models::flagged_answers::insert_flagged_answer_and_move_to_manual_review_if_needed(
498 &mut conn,
499 payload.into_inner(),
500 user.id,
501 )
502 .await?;
503
504 let token = skip_authorize();
505 token.authorized_ok(web::Json(insert_result))
506}
507
508pub fn _add_routes(cfg: &mut ServiceConfig) {
516 cfg.route("/{exercise_id}", web::get().to(get_exercise))
517 .route(
518 "/{exercise_id}/peer-or-self-reviews",
519 web::post().to(submit_peer_or_self_review),
520 )
521 .route(
522 "/{exercise_id}/peer-or-self-reviews/start",
523 web::post().to(start_peer_or_self_review),
524 )
525 .route(
526 "/{exercise_id}/peer-review",
527 web::get().to(get_peer_review_for_exercise),
528 )
529 .route(
530 "/{exercise_id}/exercise-slide-submission/{exercise_slide_submission_id}/peer-or-self-reviews-received",
531 web::get().to(get_peer_reviews_received),
532 )
533 .route(
534 "/{exercise_id}/submissions",
535 web::post().to(post_submission),
536 ).route(
537 "/{exercise_id}/flag-peer-review-answer",
538 web::post().to(post_flag_answer_in_peer_review),
539 );
540}