1use crate::{
2 exercise_slide_submissions::ExerciseSlideSubmission,
3 exercises::Exercise,
4 peer_or_self_review_configs::{self, PeerOrSelfReviewConfig},
5 peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission,
6 peer_or_self_review_questions::{self, PeerOrSelfReviewQuestion, PeerOrSelfReviewQuestionType},
7 peer_or_self_review_submissions::{self, PeerOrSelfReviewSubmission},
8 peer_review_queue_entries::PeerReviewQueueEntry,
9 prelude::*,
10 teacher_grading_decisions::{self, TeacherGradingDecision},
11 user_exercise_slide_states::{self, UserExerciseSlideStateGradingSummary},
12 user_exercise_states::UserExerciseState,
13};
14
15use super::{
16 UserExerciseStateUpdateAlreadyLoadedRequiredData,
17 UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation,
18 UserExerciseStateUpdateRequiredData, UserExerciseStateUpdateRequiredDataPeerReviewInformation,
19};
20
21pub(super) async fn load_required_data(
23 conn: &mut PgConnection,
24 user_exercise_state_id: Uuid,
25 already_loaded_internal_dependencies: UserExerciseStateUpdateAlreadyLoadedRequiredData,
26) -> ModelResult<UserExerciseStateUpdateRequiredData> {
27 info!("Loading required data for user_exercise_state update");
28 let UserExerciseStateUpdateAlreadyLoadedRequiredData {
29 exercise,
30 current_user_exercise_state,
31 peer_or_self_review_information,
32 latest_teacher_grading_decision,
33 user_exercise_slide_state_grading_summary,
34 } = already_loaded_internal_dependencies;
35
36 let loaded_user_exercise_state =
37 load_current_user_exercise_state(conn, current_user_exercise_state, user_exercise_state_id)
38 .await?;
39 let loaded_exercise = load_exercise(conn, exercise, &loaded_user_exercise_state).await?;
40
41 Ok(UserExerciseStateUpdateRequiredData {
42 peer_or_self_review_information: load_peer_or_self_review_information(
43 conn,
44 peer_or_self_review_information,
45 &loaded_user_exercise_state,
46 &loaded_exercise,
47 )
48 .await?,
49 exercise: loaded_exercise,
50 latest_teacher_grading_decision: load_latest_teacher_grading_decision(
51 conn,
52 latest_teacher_grading_decision,
53 &loaded_user_exercise_state,
54 )
55 .await?,
56 user_exercise_slide_state_grading_summary: load_user_exercise_slide_state_grading_summary(
57 conn,
58 user_exercise_slide_state_grading_summary,
59 &loaded_user_exercise_state,
60 )
61 .await?,
62 current_user_exercise_state: loaded_user_exercise_state,
63 })
64}
65
66async fn load_user_exercise_slide_state_grading_summary(
67 conn: &mut PgConnection,
68 user_exercise_slide_state_grading_summary: Option<UserExerciseSlideStateGradingSummary>,
69 loaded_user_exercise_state: &UserExerciseState,
70) -> ModelResult<UserExerciseSlideStateGradingSummary> {
71 if let Some(user_exercise_slide_state_grading_summary) =
72 user_exercise_slide_state_grading_summary
73 {
74 info!("Using already loaded user exercise slide state grading summary");
75 Ok(user_exercise_slide_state_grading_summary)
76 } else {
77 info!("Loading user exercise slide state grading summary");
78 user_exercise_slide_states::get_grading_summary_by_user_exercise_state_id(
79 conn,
80 loaded_user_exercise_state.id,
81 )
82 .await
83 }
84}
85
86async fn load_latest_teacher_grading_decision(
87 conn: &mut PgConnection,
88 latest_teacher_grading_decision: Option<Option<TeacherGradingDecision>>,
89 loaded_user_exercise_state: &UserExerciseState,
90) -> ModelResult<Option<TeacherGradingDecision>> {
91 if let Some(latest_teacher_grading_decision) = latest_teacher_grading_decision {
92 info!("Using already loaded latest teacher grading decision");
93 Ok(latest_teacher_grading_decision)
94 } else {
95 info!("Loading latest teacher grading decision");
96 Ok(teacher_grading_decisions::try_to_get_latest_grading_decision_by_user_exercise_state_id(conn, loaded_user_exercise_state.id).await?)
97 }
98}
99
100async fn load_current_user_exercise_state(
101 conn: &mut PgConnection,
102 already_loaded_user_exercise_state: Option<UserExerciseState>,
103 user_exercise_state_id: Uuid,
104) -> ModelResult<UserExerciseState> {
105 if let Some(user_exercise_state) = already_loaded_user_exercise_state {
106 info!("Using already loaded user exercise state");
107 Ok(user_exercise_state)
108 } else {
109 info!("Loading user exercise state");
110 Ok(crate::user_exercise_states::get_by_id(conn, user_exercise_state_id).await?)
111 }
112}
113
114async fn load_exercise(
115 conn: &mut PgConnection,
116 already_loaded_exercise: Option<Exercise>,
117 current_user_exercise_state: &UserExerciseState,
118) -> ModelResult<Exercise> {
119 if let Some(exercise) = already_loaded_exercise {
120 info!("Using already loaded exercise");
121 Ok(exercise)
122 } else {
123 info!("Loading exercise");
124 Ok(crate::exercises::get_by_id(conn, current_user_exercise_state.exercise_id).await?)
125 }
126}
127
128async fn load_peer_or_self_review_information(
129 conn: &mut PgConnection,
130 already_loaded_peer_or_self_review_information: Option<
131 UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation,
132 >,
133 loaded_user_exercise_state: &UserExerciseState,
134 loaded_exercise: &Exercise,
135) -> ModelResult<Option<UserExerciseStateUpdateRequiredDataPeerReviewInformation>> {
136 info!("Loading peer or self review information");
137 if loaded_exercise.needs_peer_review || loaded_exercise.needs_self_review {
138 if loaded_exercise.needs_peer_review {
139 info!("Exercise needs peer review");
140 }
141 if loaded_exercise.needs_self_review {
142 info!("Exercise needs self review");
143 }
144
145 let UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation {
147 given_peer_or_self_review_submissions,
148 given_self_review_submission,
149 latest_exercise_slide_submission,
150 latest_exercise_slide_submission_received_peer_or_self_review_question_submissions,
151 peer_review_queue_entry,
152 peer_or_self_review_config,
153 peer_or_self_review_questions,
154 } = already_loaded_peer_or_self_review_information.unwrap_or_default();
155
156 let loaded_latest_exercise_slide_submission = load_latest_exercise_slide_submission(
157 conn,
158 latest_exercise_slide_submission,
159 loaded_user_exercise_state,
160 )
161 .await?;
162
163 let loaded_peer_or_self_review_config =
164 load_peer_or_self_review_config(conn, peer_or_self_review_config, loaded_exercise)
165 .await?;
166
167 Ok(Some(
168 UserExerciseStateUpdateRequiredDataPeerReviewInformation {
169 given_peer_or_self_review_submissions: load_given_peer_or_self_review_submissions(
170 conn,
171 given_peer_or_self_review_submissions,
172 loaded_user_exercise_state,
173 )
174 .await?,
175 given_self_review_submission: load_given_self_review_submission(
176 conn,
177 given_self_review_submission,
178 loaded_user_exercise_state,
179 )
180 .await?,
181 latest_exercise_slide_submission_received_peer_or_self_review_question_submissions:
182 load_latest_exercise_slide_submission_received_peer_or_self_review_question_submissions(
183 conn,
184 latest_exercise_slide_submission_received_peer_or_self_review_question_submissions,
185 loaded_latest_exercise_slide_submission.id,
186 )
187 .await?,
188 peer_review_queue_entry: load_peer_review_queue_entry(
189 conn,
190 peer_review_queue_entry,
191 loaded_latest_exercise_slide_submission.id,
192 )
193 .await?,
194 peer_or_self_review_config: loaded_peer_or_self_review_config.clone(),
195 peer_or_self_review_questions: load_peer_or_self_review_questions(
196 conn,
197 peer_or_self_review_questions,
198 &loaded_peer_or_self_review_config,
199 )
200 .await?,
201 },
202 ))
203 } else {
204 info!("Exercise does not need peer or self review");
205 Ok(None)
207 }
208}
209
210async fn load_peer_or_self_review_config(
211 conn: &mut PgConnection,
212 already_loaded_peer_or_self_review_config: Option<
213 crate::peer_or_self_review_configs::PeerOrSelfReviewConfig,
214 >,
215 loaded_exercise: &Exercise,
216) -> ModelResult<PeerOrSelfReviewConfig> {
217 if let Some(prc) = already_loaded_peer_or_self_review_config {
218 info!("Using already loaded peer review config");
219 Ok(prc)
220 } else {
221 info!("Loading peer review config");
222 Ok(peer_or_self_review_configs::get_by_exercise_or_course_id(
223 conn,
224 loaded_exercise,
225 loaded_exercise.course_id.ok_or_else(|| {
226 ModelError::new(
227 ModelErrorType::InvalidRequest,
228 "Peer reviews work only on courses (and not, for example, on exams)"
229 .to_string(),
230 None,
231 )
232 })?,
233 )
234 .await?)
235 }
236}
237
238async fn load_peer_or_self_review_questions(
240 conn: &mut PgConnection,
241 already_loaded_peer_or_self_review_questions: Option<Vec<PeerOrSelfReviewQuestion>>,
242 loaded_peer_or_self_review_config: &PeerOrSelfReviewConfig,
243) -> ModelResult<Vec<PeerOrSelfReviewQuestion>> {
244 if let Some(prq) = already_loaded_peer_or_self_review_questions {
245 info!("Using already loaded peer review questions");
246 Ok(prq)
247 } else {
248 info!("Loading peer review questions");
249 let mut questions =
250 peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
251 conn,
252 loaded_peer_or_self_review_config.id,
253 )
254 .await?;
255
256 if !loaded_peer_or_self_review_config.points_are_all_or_nothing {
257 questions = normalize_weights(questions);
258 }
259 Ok(questions)
260 }
261}
262
263fn normalize_weights(
264 mut questions: Vec<PeerOrSelfReviewQuestion>,
265) -> Vec<PeerOrSelfReviewQuestion> {
266 info!("Normalizing peer review question weights to sum to 1");
267 questions.sort_by(|a, b| a.order_number.cmp(&b.order_number));
268 info!(
269 "Weights before normalization: {:?}",
270 questions.iter().map(|q| q.weight).collect::<Vec<_>>()
271 );
272 let (mut allowed_to_have_weight, mut not_allowed_to_have_weight) = questions
273 .into_iter()
274 .partition::<Vec<_>, _>(|q| q.question_type == PeerOrSelfReviewQuestionType::Scale);
275 let number_of_questions = allowed_to_have_weight.len();
276 let sum_of_weights = allowed_to_have_weight.iter().map(|q| q.weight).sum::<f32>();
277 if sum_of_weights < 0.000001 {
278 info!(
279 "All weights are zero, setting all weights to 1/number_of_questions so that they sum to 1."
280 );
281 for question in &mut allowed_to_have_weight {
282 question.weight = 1.0 / number_of_questions as f32;
283 }
284 } else {
285 for question in &mut allowed_to_have_weight {
286 question.weight /= sum_of_weights;
287 }
288 }
289 for question in &mut not_allowed_to_have_weight {
290 question.weight = 0.0;
291 }
292 let mut new_vec = not_allowed_to_have_weight;
293 new_vec.append(&mut allowed_to_have_weight);
294 new_vec.sort_by(|a, b| a.order_number.cmp(&b.order_number));
295 questions = new_vec;
296 info!(
297 "Weights after normalization: {:?}",
298 questions.iter().map(|q| q.weight).collect::<Vec<_>>()
299 );
300 questions
301}
302
303async fn load_peer_review_queue_entry(
304 conn: &mut PgConnection,
305 already_loaded_peer_review_queue_entry: Option<Option<PeerReviewQueueEntry>>,
306 latest_exercise_submission_id: Uuid,
307) -> ModelResult<Option<PeerReviewQueueEntry>> {
308 if let Some(prqe) = already_loaded_peer_review_queue_entry {
309 info!("Using already loaded peer review queue entry");
310 Ok(prqe)
311 } else {
312 info!("Loading peer review queue entry");
313 Ok(crate::peer_review_queue_entries::get_by_receiving_peer_reviews_exercise_slide_submission_id(conn, latest_exercise_submission_id ).await.optional()?)
315 }
316}
317
318async fn load_latest_exercise_slide_submission_received_peer_or_self_review_question_submissions(
319 conn: &mut PgConnection,
320 latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: Option<
321 Vec<PeerOrSelfReviewQuestionSubmission>,
322 >,
323 latest_exercise_slide_submission_id: Uuid,
324) -> ModelResult<Vec<PeerOrSelfReviewQuestionSubmission>> {
325 if let Some(
326 latest_exercise_slide_submission_received_peer_or_self_review_question_submissions,
327 ) = latest_exercise_slide_submission_received_peer_or_self_review_question_submissions
328 {
329 info!(
330 "Using already loaded latest exercise slide submission received peer review question submissions"
331 );
332 Ok(latest_exercise_slide_submission_received_peer_or_self_review_question_submissions)
333 } else {
334 info!("Loading latest exercise slide submission received peer review question submissions");
335 Ok(crate::peer_or_self_review_question_submissions::get_received_question_submissions_for_exercise_slide_submission(conn, latest_exercise_slide_submission_id).await?)
336 }
337}
338
339async fn load_latest_exercise_slide_submission(
340 conn: &mut PgConnection,
341 already_loaded_latest_exercise_slide_submission: Option<ExerciseSlideSubmission>,
342 loaded_user_exercise_state: &UserExerciseState,
343) -> ModelResult<ExerciseSlideSubmission> {
344 if let Some(latest_exercise_slide_submission) = already_loaded_latest_exercise_slide_submission
345 {
346 info!("Using already loaded latest exercise slide submission");
347 Ok(latest_exercise_slide_submission)
348 } else {
349 info!("Loading latest exercise slide submission");
350 let selected_exercise_slide_id = loaded_user_exercise_state.selected_exercise_slide_id.ok_or_else(|| ModelError::new(ModelErrorType::PreconditionFailed, "No selected exercise slide id found: presumably the user has not answered the exercise.".to_string(), None))?;
351 let latest_exercise_slide_submission =
353 crate::exercise_slide_submissions::get_users_latest_exercise_slide_submission(
354 conn,
355 selected_exercise_slide_id,
356 loaded_user_exercise_state.user_id,
357 )
358 .await?;
359 Ok(latest_exercise_slide_submission)
360 }
361}
362
363async fn load_given_peer_or_self_review_submissions(
364 conn: &mut PgConnection,
365 already_loaded_given_peer_or_self_review_submissions: Option<Vec<PeerOrSelfReviewSubmission>>,
366 loaded_user_exercise_state: &UserExerciseState,
367) -> ModelResult<Vec<PeerOrSelfReviewSubmission>> {
368 if let Some(given_peer_or_self_review_submissions) =
369 already_loaded_given_peer_or_self_review_submissions
370 {
371 info!("Using already loaded given peer review submissions");
372 Ok(given_peer_or_self_review_submissions)
373 } else {
374 info!("Loading given peer review submissions");
375 let course_instance_id =
376 loaded_user_exercise_state
377 .course_instance_id
378 .ok_or_else(|| {
379 ModelError::new(
380 ModelErrorType::InvalidRequest,
381 "Peer reviews work only on courses (and not, for example, on exams)"
382 .to_string(),
383 None,
384 )
385 })?;
386 Ok(peer_or_self_review_submissions::get_peer_reviews_given_by_user_and_course_instance_and_exercise(conn, loaded_user_exercise_state.user_id, course_instance_id, loaded_user_exercise_state.exercise_id).await?)
387 }
388}
389
390async fn load_given_self_review_submission(
391 conn: &mut PgConnection,
392 already_loaded_given_self_review_submission: Option<Option<PeerOrSelfReviewSubmission>>,
393 loaded_user_exercise_state: &UserExerciseState,
394) -> ModelResult<Option<PeerOrSelfReviewSubmission>> {
395 if let Some(given_self_review_submission) = already_loaded_given_self_review_submission {
396 info!("Using already loaded given self review submission");
397 Ok(given_self_review_submission)
398 } else if let Some(course_instance_id) = loaded_user_exercise_state.course_instance_id {
399 info!("Loading given self review submission");
400 Ok(
401 peer_or_self_review_submissions::get_self_review_submission_by_user_and_exercise(
402 conn,
403 loaded_user_exercise_state.user_id,
404 loaded_user_exercise_state.exercise_id,
405 course_instance_id,
406 )
407 .await?,
408 )
409 } else {
410 Err(ModelError::new(
411 ModelErrorType::PreconditionFailed,
412 "No course instance found: self review is only possible on courses".to_string(),
413 None,
414 ))
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 mod normalize_weights {
423 use super::*;
424
425 #[test]
426 fn test_normalize_weights() {
427 let peer_or_self_review_questions = vec![
428 PeerOrSelfReviewQuestion {
429 id: Uuid::new_v4(),
430 peer_or_self_review_config_id: Uuid::new_v4(),
431 question_type: PeerOrSelfReviewQuestionType::Scale,
432 order_number: 1,
433 weight: 0.3,
434 question: "Scale question".to_string(),
435 created_at: chrono::Utc::now(),
436 updated_at: chrono::Utc::now(),
437 deleted_at: None,
438 answer_required: true,
439 },
440 PeerOrSelfReviewQuestion {
441 id: Uuid::new_v4(),
442 peer_or_self_review_config_id: Uuid::new_v4(),
443 question_type: PeerOrSelfReviewQuestionType::Essay,
444 order_number: 2,
445 weight: 0.1,
446 question: "Text question".to_string(),
447 created_at: chrono::Utc::now(),
448 updated_at: chrono::Utc::now(),
449 deleted_at: None,
450 answer_required: true,
451 },
452 PeerOrSelfReviewQuestion {
453 id: Uuid::new_v4(),
454 peer_or_self_review_config_id: Uuid::new_v4(),
455 question_type: PeerOrSelfReviewQuestionType::Scale,
456 order_number: 3,
457 weight: 0.1,
458 question: "Scale question".to_string(),
459 created_at: chrono::Utc::now(),
460 updated_at: chrono::Utc::now(),
461 deleted_at: None,
462 answer_required: true,
463 },
464 ];
465 let normalized_peer_or_self_review_questions =
466 normalize_weights(peer_or_self_review_questions);
467 assert_eq!(normalized_peer_or_self_review_questions[0].weight, 0.75);
468 assert_eq!(normalized_peer_or_self_review_questions[1].weight, 0.0);
469 assert_eq!(normalized_peer_or_self_review_questions[2].weight, 0.25);
470 }
471 }
472}