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::{CourseInstanceOrExamId, 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_instance_or_exam_id, last_try) =
27 resolve_course_instance_or_exam_id_and_verify_that_user_can_submit(
28 conn,
29 user_id,
30 &exercise,
31 submission.exercise_slide_id,
32 )
33 .await?;
34
35 let user_exercise_state = models::user_exercise_states::get_user_exercise_state_if_exists(
37 conn,
38 user_id,
39 exercise.id,
40 course_instance_or_exam_id,
41 )
42 .await?
43 .ok_or_else(|| {
44 ControllerError::new(
45 ControllerErrorType::Unauthorized,
46 "Missing exercise state.".to_string(),
47 None,
48 )
49 })?;
50
51 let mut exercise_with_user_state = ExerciseWithUserState::new(exercise, user_exercise_state)?;
52 let mut result = models::library::grading::grade_user_submission(
53 conn,
54 &mut exercise_with_user_state,
55 submission,
56 GradingPolicy::Default,
57 models_requests::fetch_service_info,
58 models_requests::make_grading_request_sender(jwt_key),
59 )
60 .await?;
61
62 if exercise_with_user_state.is_exam_exercise() {
63 result.clear_grading_information();
65 }
66
67 let score_given = if let Some(exercise_status) = &result.exercise_status {
68 exercise_status.score_given.unwrap_or(0.0)
69 } else {
70 0.0
71 };
72
73 let has_received_full_points = score_given
76 >= exercise_with_user_state.exercise().score_maximum as f32
77 || (score_given - exercise_with_user_state.exercise().score_maximum as f32).abs() < 0.0001;
78 if !has_received_full_points && !last_try {
79 result.clear_model_solution_specs();
80 }
81 Ok(result)
82}
83
84async fn enforce_deadline(
86 conn: &mut PgConnection,
87 exercise: &Exercise,
88) -> Result<(), ControllerError> {
89 let chapter_option_future: OptionFuture<_> = exercise
90 .chapter_id
91 .map(|id| models::chapters::get_chapter(conn, id))
92 .into();
93 let chapter = chapter_option_future.await.transpose()?;
94
95 if let Some(deadline) = exercise
97 .deadline
98 .or_else(|| chapter.and_then(|c| c.deadline))
99 {
100 if Utc::now() + Duration::seconds(1) >= deadline {
101 return Err(ControllerError::new(
102 ControllerErrorType::BadRequest,
103 "Exercise deadline passed.".to_string(),
104 None,
105 ));
106 }
107 }
108
109 Ok(())
110}
111
112async fn resolve_course_instance_or_exam_id_and_verify_that_user_can_submit(
115 conn: &mut PgConnection,
116 user_id: Uuid,
117 exercise: &Exercise,
118 slide_id: Uuid,
119) -> Result<(CourseInstanceOrExamId, bool), ControllerError> {
120 let mut last_try = false;
121 let course_instance_id_or_exam_id: CourseInstanceOrExamId = if let Some(course_id) =
122 exercise.course_id
123 {
124 let settings = models::user_course_settings::get_user_course_settings_by_course_id(
127 conn, user_id, course_id,
128 )
129 .await?;
130 if let Some(settings) = settings {
131 let token = authorize(conn, Act::View, Some(user_id), Res::Course(course_id)).await?;
132 token.authorized_ok(CourseInstanceOrExamId::Instance(
133 settings.current_course_instance_id,
134 ))
135 } else {
136 Err(ControllerError::new(
137 ControllerErrorType::Unauthorized,
138 "User is not enrolled on this course.".to_string(),
139 None,
140 ))
141 }
142 } else if let Some(exam_id) = exercise.exam_id {
143 if models::exams::verify_exam_submission_can_be_made(conn, exam_id, user_id).await? {
145 let token = authorize(conn, Act::View, Some(user_id), Res::Exam(exam_id)).await?;
146 token.authorized_ok(CourseInstanceOrExamId::Exam(exam_id))
147 } else {
148 Err(ControllerError::new(
149 ControllerErrorType::Unauthorized,
150 "Submissions for this exam are no longer accepted.".to_string(),
151 None,
152 ))
153 }
154 } else {
155 Err(ControllerError::new(
157 ControllerErrorType::InternalServerError,
158 "Exam doesn't belong to either a course nor exam.".to_string(),
159 None,
160 ))
161 }?
162 .data;
163 if exercise.limit_number_of_tries {
164 if let Some(max_tries_per_slide) = exercise.max_tries_per_slide {
165 let slide_id_to_submissions_count =
167 models::exercise_slide_submissions::get_exercise_slide_submission_counts_for_exercise_user(
168 conn,
169 exercise.id,
170 course_instance_id_or_exam_id,
171 user_id,
172 )
173 .await?;
174
175 let count = slide_id_to_submissions_count.get(&slide_id).unwrap_or(&0);
176 if count >= &(max_tries_per_slide as i64) {
177 return Err(ControllerError::new(
178 ControllerErrorType::BadRequest,
179 "You've ran out of tries.".to_string(),
180 None,
181 ));
182 }
183 if count + 1 >= (max_tries_per_slide as i64) {
184 last_try = true;
185 }
186 }
187 }
188 Ok((course_instance_id_or_exam_id, last_try))
189}