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 token.authorized_ok(web::Json(result))
226}
227
228#[instrument(skip(pool))]
236async fn start_peer_or_self_review(
237 pool: web::Data<PgPool>,
238 exercise_id: web::Path<Uuid>,
239 user: AuthUser,
240) -> ControllerResult<web::Json<bool>> {
241 let mut conn = pool.acquire().await?;
242
243 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
244 let user_exercise_state =
245 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
246 let token = authorize(
247 &mut conn,
248 Act::View,
249 Some(user.id),
250 Res::Exercise(*exercise_id),
251 )
252 .await?;
253 models::library::peer_or_self_reviewing::start_peer_or_self_review_for_user(
254 &mut conn,
255 user_exercise_state,
256 &exercise,
257 )
258 .await?;
259
260 token.authorized_ok(web::Json(true))
261}
262
263#[instrument(skip(pool))]
268async fn submit_peer_or_self_review(
269 pool: web::Data<PgPool>,
270 exercise_id: web::Path<Uuid>,
271 payload: web::Json<CourseMaterialPeerOrSelfReviewSubmission>,
272 user: AuthUser,
273 jwt_key: web::Data<JwtKey>,
274) -> ControllerResult<web::Json<bool>> {
275 let mut conn = pool.acquire().await?;
276 let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?;
277 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
280 if claim.exercise_slide_submission_id != payload.exercise_slide_submission_id
281 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
282 {
283 return Err(ControllerError::new(
284 ControllerErrorType::BadRequest,
285 "You are not allowed to review this answer.".to_string(),
286 None,
287 ));
288 }
289
290 let giver_user_exercise_state =
291 user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?;
292 let exercise_slide_submission: models::exercise_slide_submissions::ExerciseSlideSubmission =
293 models::exercise_slide_submissions::get_by_id(
294 &mut conn,
295 payload.exercise_slide_submission_id,
296 )
297 .await?;
298
299 if let Some(receiver_course_id) = exercise_slide_submission.course_id {
300 let receiver_user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
301 &mut conn,
302 exercise_slide_submission.user_id,
303 exercise.id,
304 CourseOrExamId::Course(receiver_course_id),
305 )
306 .await?;
307 if let Some(receiver_user_exercise_state) = receiver_user_exercise_state {
308 models::library::peer_or_self_reviewing::create_peer_or_self_review_submission_for_user(
309 &mut conn,
310 &exercise,
311 giver_user_exercise_state,
312 receiver_user_exercise_state,
313 payload.0,
314 )
315 .await?;
316 } else {
317 warn!(
318 "No user exercise state found for receiver's exercise slide submission id: {}",
319 exercise_slide_submission.id
320 );
321 return Err(ControllerError::new(
322 ControllerErrorType::BadRequest,
323 "No user exercise state found for receiver's exercise slide submission."
324 .to_string(),
325 None,
326 ));
327 }
328 } else {
329 warn!(
330 "No course instance id found for receiver's exercise slide submission id: {}",
331 exercise_slide_submission.id
332 );
333 return Err(ControllerError::new(
334 ControllerErrorType::BadRequest,
335 "No course instance id found for receiver's exercise slide submission.".to_string(),
336 None,
337 ));
338 }
339 let token = skip_authorize();
340 token.authorized_ok(web::Json(true))
341}
342
343#[instrument(skip(pool))]
347async fn post_flag_answer_in_peer_review(
348 pool: web::Data<PgPool>,
349 payload: web::Json<NewFlaggedAnswerWithToken>,
350 user: AuthUser,
351 jwt_key: web::Data<JwtKey>,
352) -> ControllerResult<web::Json<FlaggedAnswer>> {
353 let mut conn = pool.acquire().await?;
354
355 let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?;
356 if claim.exercise_slide_submission_id != payload.submission_id
357 || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id
358 {
359 return Err(ControllerError::new(
360 ControllerErrorType::BadRequest,
361 "You are not allowed to report this answer.".to_string(),
362 None,
363 ));
364 }
365
366 let insert_result =
367 models::flagged_answers::insert_flagged_answer_and_move_to_manual_review_if_needed(
368 &mut conn,
369 payload.into_inner(),
370 user.id,
371 )
372 .await?;
373
374 let token = skip_authorize();
375 token.authorized_ok(web::Json(insert_result))
376}
377
378pub fn _add_routes(cfg: &mut ServiceConfig) {
386 cfg.route("/{exercise_id}", web::get().to(get_exercise))
387 .route(
388 "/{exercise_id}/peer-or-self-reviews",
389 web::post().to(submit_peer_or_self_review),
390 )
391 .route(
392 "/{exercise_id}/peer-or-self-reviews/start",
393 web::post().to(start_peer_or_self_review),
394 )
395 .route(
396 "/{exercise_id}/peer-review",
397 web::get().to(get_peer_review_for_exercise),
398 )
399 .route(
400 "/{exercise_id}/exercise-slide-submission/{exercise_slide_submission_id}/peer-or-self-reviews-received",
401 web::get().to(get_peer_reviews_received),
402 )
403 .route(
404 "/{exercise_id}/submissions",
405 web::post().to(post_submission),
406 ).route(
407 "/{exercise_id}/flag-peer-review-answer",
408 web::post().to(post_flag_answer_in_peer_review),
409 );
410}