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::{
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_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 let token = authorize(
220 &mut conn,
221 Act::View,
222 Some(user.id),
223 Res::Exercise(exercise.id),
224 )
225 .await?;
226 let result = domain::exercises::process_submission(
227 &mut conn,
228 user.id,
229 exercise.clone(),
230 &submission,
231 jwt_key.into_inner(),
232 )
233 .await?;
234 token.authorized_ok(web::Json(result))
235}
236
237#[instrument(skip(pool))]
245async fn start_peer_or_self_review(
246 pool: web::Data<PgPool>,
247 exercise_id: web::Path<Uuid>,
248 user: AuthUser,
249) -> ControllerResult<web::Json<bool>> {
250 let mut conn = pool.acquire().await?;
251
252 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
253 let user_exercise_state =
254 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
255 let token = authorize(
256 &mut conn,
257 Act::View,
258 Some(user.id),
259 Res::Exercise(*exercise_id),
260 )
261 .await?;
262 models::library::peer_or_self_reviewing::start_peer_or_self_review_for_user(
263 &mut conn,
264 user_exercise_state,
265 &exercise,
266 )
267 .await?;
268
269 token.authorized_ok(web::Json(true))
270}
271
272#[instrument(skip(pool))]
277async fn submit_peer_or_self_review(
278 pool: web::Data<PgPool>,
279 exercise_id: web::Path<Uuid>,
280 payload: web::Json<CourseMaterialPeerOrSelfReviewSubmission>,
281 user: AuthUser,
282 jwt_key: web::Data<JwtKey>,
283) -> ControllerResult<web::Json<bool>> {
284 let mut conn = pool.acquire().await?;
285 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
286 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
289 if claim.exercise_slide_submission_id != payload.exercise_slide_submission_id
290 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
291 {
292 return Err(ControllerError::new(
293 ControllerErrorType::BadRequest,
294 "You are not allowed to review this answer.".to_string(),
295 None,
296 ));
297 }
298
299 let giver_user_exercise_state =
300 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
301 let exercise_slide_submission: models::exercise_slide_submissions::ExerciseSlideSubmission =
302 models::exercise_slide_submissions::get_by_id(
303 &mut conn,
304 payload.exercise_slide_submission_id,
305 )
306 .await?;
307
308 if let Some(receiver_course_id) = exercise_slide_submission.course_id {
309 let receiver_user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
310 &mut conn,
311 exercise_slide_submission.user_id,
312 exercise.id,
313 CourseOrExamId::Course(receiver_course_id),
314 )
315 .await?;
316 if let Some(receiver_user_exercise_state) = receiver_user_exercise_state {
317 let mut tx = conn.begin().await?;
318
319 models::library::peer_or_self_reviewing::create_peer_or_self_review_submission_for_user(
320 &mut tx,
321 &exercise,
322 giver_user_exercise_state,
323 receiver_user_exercise_state,
324 payload.0,
325 )
326 .await?;
327
328 let updated_receiver_state = user_exercise_states::get_user_exercise_state_if_exists(
330 &mut tx,
331 exercise_slide_submission.user_id,
332 exercise.id,
333 CourseOrExamId::Course(receiver_course_id),
334 )
335 .await?
336 .ok_or_else(|| {
337 ModelError::new(
338 ModelErrorType::Generic,
339 "Receiver exercise state not found".to_string(),
340 None,
341 )
342 })?;
343
344 let peer_or_self_review_config =
345 peer_or_self_review_configs::get_by_exercise_or_course_id(
346 &mut tx,
347 &exercise,
348 exercise.get_course_id()?,
349 )
350 .await?;
351
352 let _ = models::library::peer_or_self_reviewing::reset_exercise_if_needed_if_zero_points_from_review(
353 &mut tx,
354 &peer_or_self_review_config,
355 &updated_receiver_state,
356 ).await?;
357
358 tx.commit().await?;
359 } else {
360 warn!(
361 "No user exercise state found for receiver's exercise slide submission id: {}",
362 exercise_slide_submission.id
363 );
364 return Err(ControllerError::new(
365 ControllerErrorType::BadRequest,
366 "No user exercise state found for receiver's exercise slide submission."
367 .to_string(),
368 None,
369 ));
370 }
371 } else {
372 warn!(
373 "No course instance id found for receiver's exercise slide submission id: {}",
374 exercise_slide_submission.id
375 );
376 return Err(ControllerError::new(
377 ControllerErrorType::BadRequest,
378 "No course instance id found for receiver's exercise slide submission.".to_string(),
379 None,
380 ));
381 }
382 let token = skip_authorize();
383 token.authorized_ok(web::Json(true))
384}
385
386#[instrument(skip(pool))]
390async fn post_flag_answer_in_peer_review(
391 pool: web::Data<PgPool>,
392 payload: web::Json<NewFlaggedAnswerWithToken>,
393 user: AuthUser,
394 jwt_key: web::Data<JwtKey>,
395) -> ControllerResult<web::Json<FlaggedAnswer>> {
396 let mut conn = pool.acquire().await?;
397
398 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
399 if claim.exercise_slide_submission_id != payload.submission_id
400 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
401 {
402 return Err(ControllerError::new(
403 ControllerErrorType::BadRequest,
404 "You are not allowed to report this answer.".to_string(),
405 None,
406 ));
407 }
408
409 let insert_result =
410 models::flagged_answers::insert_flagged_answer_and_move_to_manual_review_if_needed(
411 &mut conn,
412 payload.into_inner(),
413 user.id,
414 )
415 .await?;
416
417 let token = skip_authorize();
418 token.authorized_ok(web::Json(insert_result))
419}
420
421pub fn _add_routes(cfg: &mut ServiceConfig) {
429 cfg.route("/{exercise_id}", web::get().to(get_exercise))
430 .route(
431 "/{exercise_id}/peer-or-self-reviews",
432 web::post().to(submit_peer_or_self_review),
433 )
434 .route(
435 "/{exercise_id}/peer-or-self-reviews/start",
436 web::post().to(start_peer_or_self_review),
437 )
438 .route(
439 "/{exercise_id}/peer-review",
440 web::get().to(get_peer_review_for_exercise),
441 )
442 .route(
443 "/{exercise_id}/exercise-slide-submission/{exercise_slide_submission_id}/peer-or-self-reviews-received",
444 web::get().to(get_peer_reviews_received),
445 )
446 .route(
447 "/{exercise_id}/submissions",
448 web::post().to(post_submission),
449 ).route(
450 "/{exercise_id}/flag-peer-review-answer",
451 web::post().to(post_flag_answer_in_peer_review),
452 );
453}