headless_lms_server/domain/
exercises.rs1use std::sync::Arc;
2
3use crate::{
4 domain::models_requests::{self, JwtKey},
5 prelude::*,
6};
7use chrono::{Duration, Utc};
8use futures_util::future::OptionFuture;
9use models::{
10 exercises::Exercise,
11 library::grading::{
12 GradingPolicy, StudentExerciseSlideSubmission, StudentExerciseSlideSubmissionResult,
13 },
14 user_exercise_states::ExerciseWithUserState,
15};
16
17pub async fn process_submission(
18 conn: &mut PgConnection,
19 user_id: Uuid,
20 exercise: Exercise,
21 submission: &StudentExerciseSlideSubmission,
22 jwt_key: Arc<JwtKey>,
23) -> Result<StudentExerciseSlideSubmissionResult, ControllerError> {
24 enforce_deadline(conn, &exercise).await?;
25
26 let (course_or_exam_id, last_try) = resolve_course_or_exam_id_and_verify_that_user_can_submit(
27 conn,
28 user_id,
29 &exercise,
30 submission.exercise_slide_id,
31 )
32 .await?;
33
34 let user_exercise_state = models::user_exercise_states::get_user_exercise_state_if_exists(
36 conn,
37 user_id,
38 exercise.id,
39 course_or_exam_id,
40 )
41 .await?
42 .ok_or_else(|| {
43 ControllerError::new(
44 ControllerErrorType::Unauthorized,
45 "Missing exercise state.".to_string(),
46 None,
47 )
48 })?;
49
50 let mut exercise_with_user_state = ExerciseWithUserState::new(exercise, user_exercise_state)?;
51 let mut result = models::library::grading::grade_user_submission(
52 conn,
53 &mut exercise_with_user_state,
54 submission,
55 GradingPolicy::Default,
56 models_requests::fetch_service_info,
57 models_requests::make_grading_request_sender(jwt_key),
58 )
59 .await?;
60
61 if exercise_with_user_state.is_exam_exercise() {
62 result.clear_grading_information();
64 }
65
66 let score_given = if let Some(exercise_status) = &result.exercise_status {
67 exercise_status.score_given.unwrap_or(0.0)
68 } else {
69 0.0
70 };
71
72 let has_received_full_points = score_given
75 >= exercise_with_user_state.exercise().score_maximum as f32
76 || (score_given - exercise_with_user_state.exercise().score_maximum as f32).abs() < 0.0001;
77 if !has_received_full_points && !last_try {
78 result.clear_model_solution_specs();
79 }
80 Ok(result)
81}
82
83async fn enforce_deadline(
85 conn: &mut PgConnection,
86 exercise: &Exercise,
87) -> Result<(), ControllerError> {
88 let chapter_option_future: OptionFuture<_> = exercise
89 .chapter_id
90 .map(|id| models::chapters::get_chapter(conn, id))
91 .into();
92 let chapter = chapter_option_future.await.transpose()?;
93
94 if let Some(deadline) = exercise
96 .deadline
97 .or_else(|| chapter.and_then(|c| c.deadline))
98 {
99 if Utc::now() + Duration::seconds(1) >= deadline {
100 return Err(ControllerError::new(
101 ControllerErrorType::BadRequest,
102 "Exercise deadline passed.".to_string(),
103 None,
104 ));
105 }
106 }
107
108 Ok(())
109}
110
111async fn resolve_course_or_exam_id_and_verify_that_user_can_submit(
114 conn: &mut PgConnection,
115 user_id: Uuid,
116 exercise: &Exercise,
117 slide_id: Uuid,
118) -> Result<(CourseOrExamId, bool), ControllerError> {
119 let mut last_try = false;
120 let course_id_or_exam_id: CourseOrExamId = if let Some(course_id) = exercise.course_id {
121 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
124 conn, user_id, course_id,
125 )
126 .await?;
127 if let Some(settings) = settings {
128 let token = authorize(conn, Act::View, Some(user_id), Res::Course(course_id)).await?;
129 token.authorized_ok(CourseOrExamId::Course(settings.current_course_id))
130 } else {
131 Err(ControllerError::new(
132 ControllerErrorType::Unauthorized,
133 "User is not enrolled on this course.".to_string(),
134 None,
135 ))
136 }
137 } else if let Some(exam_id) = exercise.exam_id {
138 if models::exams::verify_exam_submission_can_be_made(conn, exam_id, user_id).await? {
140 let token = authorize(conn, Act::View, Some(user_id), Res::Exam(exam_id)).await?;
141 token.authorized_ok(CourseOrExamId::Exam(exam_id))
142 } else {
143 Err(ControllerError::new(
144 ControllerErrorType::Unauthorized,
145 "Submissions for this exam are no longer accepted.".to_string(),
146 None,
147 ))
148 }
149 } else {
150 Err(ControllerError::new(
152 ControllerErrorType::InternalServerError,
153 "Exam doesn't belong to either a course nor exam.".to_string(),
154 None,
155 ))
156 }?
157 .data;
158 if exercise.limit_number_of_tries {
159 if let Some(max_tries_per_slide) = exercise.max_tries_per_slide {
160 let slide_id_to_submissions_count =
162 models::exercise_slide_submissions::get_exercise_slide_submission_counts_for_exercise_user(
163 conn,
164 exercise.id,
165 course_id_or_exam_id,
166 user_id,
167 )
168 .await?;
169
170 let count = slide_id_to_submissions_count.get(&slide_id).unwrap_or(&0);
171 if count >= &(max_tries_per_slide as i64) {
172 tracing::error!(
173 user_id = %user_id,
174 exercise_id = %exercise.id,
175 slide_id = %slide_id,
176 course_or_exam_id = ?course_id_or_exam_id,
177 current_try_count = %count,
178 max_tries_per_slide = %max_tries_per_slide,
179 limit_number_of_tries = %exercise.limit_number_of_tries,
180 "User has run out of tries for exercise slide submission"
181 );
182 return Err(ControllerError::new(
183 ControllerErrorType::BadRequest,
184 "You've ran out of tries.".to_string(),
185 None,
186 ));
187 }
188 if count + 1 >= (max_tries_per_slide as i64) {
189 last_try = true;
190 }
191 }
192 }
193 Ok((course_id_or_exam_id, last_try))
194}