headless_lms_models/library/user_exercise_state_updater/
data_loader.rs

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