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_enrollment =
60 models::exams::get_enrollment(&mut conn, exam_id, user_id.unwrap()).await?;
61
62 if let Some(enrollment) = user_enrollment {
63 if let Some(show_answers) = enrollment.show_exercise_answers {
64 if enrollment.is_teacher_testing && show_answers {
65 should_clear_grading_information = false;
66 }
67 }
68 }
69 }
70
71 if course_material_exercise.can_post_submission
72 && course_material_exercise.exercise.exam_id.is_some()
73 && should_clear_grading_information
74 {
75 course_material_exercise.clear_grading_information();
77 }
78
79 let score_given: f32 = if let Some(status) = &course_material_exercise.exercise_status {
80 status.score_given.unwrap_or(0.0)
81 } else {
82 0.0
83 };
84
85 let submission_count = course_material_exercise
86 .exercise_slide_submission_counts
87 .get(&course_material_exercise.current_exercise_slide.id)
88 .unwrap_or(&0);
89
90 let out_of_tries = course_material_exercise.exercise.limit_number_of_tries
91 && *submission_count as i32
92 >= course_material_exercise
93 .exercise
94 .max_tries_per_slide
95 .unwrap_or(i32::MAX);
96
97 let has_received_full_points = score_given
100 >= course_material_exercise.exercise.score_maximum as f32
101 || (score_given - course_material_exercise.exercise.score_maximum as f32).abs() < 0.0001;
102 if !has_received_full_points && !out_of_tries {
103 course_material_exercise.clear_model_solution_specs();
104 }
105 let token = skip_authorize();
106 token.authorized_ok(web::Json(course_material_exercise))
107}
108
109#[instrument(skip(pool))]
115async fn get_peer_review_for_exercise(
116 pool: web::Data<PgPool>,
117 exercise_id: web::Path<Uuid>,
118 user: AuthUser,
119 jwt_key: web::Data<JwtKey>,
120) -> ControllerResult<web::Json<CourseMaterialPeerOrSelfReviewDataWithToken>> {
121 let mut conn = pool.acquire().await?;
122 let course_material_peer_or_self_review_data =
123 models::peer_or_self_review_configs::get_course_material_peer_or_self_review_data(
124 &mut conn,
125 user.id,
126 *exercise_id,
127 models_requests::fetch_service_info,
128 )
129 .await?;
130 let token = authorize(
131 &mut conn,
132 Act::View,
133 Some(user.id),
134 Res::Exercise(*exercise_id),
135 )
136 .await?;
137 let give_peer_review_claim =
138 if let Some(to_review) = &course_material_peer_or_self_review_data.answer_to_review {
139 Some(
140 GivePeerReviewClaim::expiring_in_1_day(
141 to_review.exercise_slide_submission_id,
142 course_material_peer_or_self_review_data
143 .peer_or_self_review_config
144 .id,
145 )
146 .sign(&jwt_key),
147 )
148 } else {
149 None
150 };
151
152 let res = CourseMaterialPeerOrSelfReviewDataWithToken {
153 course_material_peer_or_self_review_data,
154 token: give_peer_review_claim,
155 };
156 token.authorized_ok(web::Json(res))
157}
158
159#[instrument(skip(pool))]
163async fn get_peer_reviews_received(
164 pool: web::Data<PgPool>,
165 params: web::Path<(Uuid, Uuid)>,
166 user: AuthUser,
167) -> ControllerResult<web::Json<PeerOrSelfReviewsReceived>> {
168 let mut conn = pool.acquire().await?;
169 let (exercise_id, exercise_slide_submission_id) = params.into_inner();
170 let peer_review_data = models::exercise_task_submissions::get_peer_reviews_received(
171 &mut conn,
172 exercise_id,
173 exercise_slide_submission_id,
174 user.id,
175 )
176 .await?;
177 let token = skip_authorize();
178 token.authorized_ok(web::Json(peer_review_data))
179}
180
181#[instrument(skip(pool))]
202async fn post_submission(
203 pool: web::Data<PgPool>,
204 jwt_key: web::Data<JwtKey>,
205 exercise_id: web::Path<Uuid>,
206 payload: web::Json<StudentExerciseSlideSubmission>,
207 user: AuthUser,
208) -> ControllerResult<web::Json<StudentExerciseSlideSubmissionResult>> {
209 let submission = payload.0;
210 let mut conn = pool.acquire().await?;
211 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
212 let token = authorize(
213 &mut conn,
214 Act::View,
215 Some(user.id),
216 Res::Exercise(exercise.id),
217 )
218 .await?;
219 let result = domain::exercises::process_submission(
220 &mut conn,
221 user.id,
222 exercise.clone(),
223 &submission,
224 jwt_key.into_inner(),
225 )
226 .await?;
227 token.authorized_ok(web::Json(result))
228}
229
230#[instrument(skip(pool))]
238async fn start_peer_or_self_review(
239 pool: web::Data<PgPool>,
240 exercise_id: web::Path<Uuid>,
241 user: AuthUser,
242) -> ControllerResult<web::Json<bool>> {
243 let mut conn = pool.acquire().await?;
244
245 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
246 let user_exercise_state =
247 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
248 let token = authorize(
249 &mut conn,
250 Act::View,
251 Some(user.id),
252 Res::Exercise(*exercise_id),
253 )
254 .await?;
255 models::library::peer_or_self_reviewing::start_peer_or_self_review_for_user(
256 &mut conn,
257 user_exercise_state,
258 &exercise,
259 )
260 .await?;
261
262 token.authorized_ok(web::Json(true))
263}
264
265#[instrument(skip(pool))]
270async fn submit_peer_or_self_review(
271 pool: web::Data<PgPool>,
272 exercise_id: web::Path<Uuid>,
273 payload: web::Json<CourseMaterialPeerOrSelfReviewSubmission>,
274 user: AuthUser,
275 jwt_key: web::Data<JwtKey>,
276) -> ControllerResult<web::Json<bool>> {
277 let mut conn = pool.acquire().await?;
278 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
279 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
282 if claim.exercise_slide_submission_id != payload.exercise_slide_submission_id
283 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
284 {
285 return Err(ControllerError::new(
286 ControllerErrorType::BadRequest,
287 "You are not allowed to review this answer.".to_string(),
288 None,
289 ));
290 }
291
292 let giver_user_exercise_state =
293 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
294 let exercise_slide_submission: models::exercise_slide_submissions::ExerciseSlideSubmission =
295 models::exercise_slide_submissions::get_by_id(
296 &mut conn,
297 payload.exercise_slide_submission_id,
298 )
299 .await?;
300
301 if let Some(receiver_course_id) = exercise_slide_submission.course_id {
302 let receiver_user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
303 &mut conn,
304 exercise_slide_submission.user_id,
305 exercise.id,
306 CourseOrExamId::Course(receiver_course_id),
307 )
308 .await?;
309 if let Some(receiver_user_exercise_state) = receiver_user_exercise_state {
310 let mut tx = conn.begin().await?;
311
312 models::library::peer_or_self_reviewing::create_peer_or_self_review_submission_for_user(
313 &mut tx,
314 &exercise,
315 giver_user_exercise_state,
316 receiver_user_exercise_state,
317 payload.0,
318 )
319 .await?;
320
321 let updated_receiver_state = user_exercise_states::get_user_exercise_state_if_exists(
323 &mut tx,
324 exercise_slide_submission.user_id,
325 exercise.id,
326 CourseOrExamId::Course(receiver_course_id),
327 )
328 .await?
329 .ok_or_else(|| {
330 ModelError::new(
331 ModelErrorType::Generic,
332 "Receiver exercise state not found".to_string(),
333 None,
334 )
335 })?;
336
337 let peer_or_self_review_config =
338 peer_or_self_review_configs::get_by_exercise_or_course_id(
339 &mut tx,
340 &exercise,
341 exercise.get_course_id()?,
342 )
343 .await?;
344
345 let _ = models::library::peer_or_self_reviewing::reset_exercise_if_needed_if_zero_points_from_review(
346 &mut tx,
347 &peer_or_self_review_config,
348 &updated_receiver_state,
349 ).await?;
350
351 tx.commit().await?;
352 } else {
353 warn!(
354 "No user exercise state found for receiver's exercise slide submission id: {}",
355 exercise_slide_submission.id
356 );
357 return Err(ControllerError::new(
358 ControllerErrorType::BadRequest,
359 "No user exercise state found for receiver's exercise slide submission."
360 .to_string(),
361 None,
362 ));
363 }
364 } else {
365 warn!(
366 "No course instance id found for receiver's exercise slide submission id: {}",
367 exercise_slide_submission.id
368 );
369 return Err(ControllerError::new(
370 ControllerErrorType::BadRequest,
371 "No course instance id found for receiver's exercise slide submission.".to_string(),
372 None,
373 ));
374 }
375 let token = skip_authorize();
376 token.authorized_ok(web::Json(true))
377}
378
379#[instrument(skip(pool))]
383async fn post_flag_answer_in_peer_review(
384 pool: web::Data<PgPool>,
385 payload: web::Json<NewFlaggedAnswerWithToken>,
386 user: AuthUser,
387 jwt_key: web::Data<JwtKey>,
388) -> ControllerResult<web::Json<FlaggedAnswer>> {
389 let mut conn = pool.acquire().await?;
390
391 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
392 if claim.exercise_slide_submission_id != payload.submission_id
393 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
394 {
395 return Err(ControllerError::new(
396 ControllerErrorType::BadRequest,
397 "You are not allowed to report this answer.".to_string(),
398 None,
399 ));
400 }
401
402 let insert_result =
403 models::flagged_answers::insert_flagged_answer_and_move_to_manual_review_if_needed(
404 &mut conn,
405 payload.into_inner(),
406 user.id,
407 )
408 .await?;
409
410 let token = skip_authorize();
411 token.authorized_ok(web::Json(insert_result))
412}
413
414pub fn _add_routes(cfg: &mut ServiceConfig) {
422 cfg.route("/{exercise_id}", web::get().to(get_exercise))
423 .route(
424 "/{exercise_id}/peer-or-self-reviews",
425 web::post().to(submit_peer_or_self_review),
426 )
427 .route(
428 "/{exercise_id}/peer-or-self-reviews/start",
429 web::post().to(start_peer_or_self_review),
430 )
431 .route(
432 "/{exercise_id}/peer-review",
433 web::get().to(get_peer_review_for_exercise),
434 )
435 .route(
436 "/{exercise_id}/exercise-slide-submission/{exercise_slide_submission_id}/peer-or-self-reviews-received",
437 web::get().to(get_peer_reviews_received),
438 )
439 .route(
440 "/{exercise_id}/submissions",
441 web::post().to(post_submission),
442 ).route(
443 "/{exercise_id}/flag-peer-review-answer",
444 web::post().to(post_flag_answer_in_peer_review),
445 );
446}