headless_lms_models/library/user_exercise_state_updater/
data_loader.rs

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
21/// 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.
22pub(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        // Destruct the contents of already_loaded_peer_or_self_review_information so that we can use the fields of the parent struct independently
146        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        // Peer review disabled for the exercise, no need to load any information related to peer reviews.
206        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
238/** Loads peer review config and normalizes weights, if necessary */
239async 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        // 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.
314        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        // Received peer reviews are only considered for the latest submission.
352        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_id = loaded_user_exercise_state.course_id.ok_or_else(|| {
376            ModelError::new(
377                ModelErrorType::InvalidRequest,
378                "Peer reviews work only on courses (and not, for example, on exams)".to_string(),
379                None,
380            )
381        })?;
382        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?)
383    }
384}
385
386async fn load_given_self_review_submission(
387    conn: &mut PgConnection,
388    already_loaded_given_self_review_submission: Option<Option<PeerOrSelfReviewSubmission>>,
389    loaded_user_exercise_state: &UserExerciseState,
390) -> ModelResult<Option<PeerOrSelfReviewSubmission>> {
391    if let Some(given_self_review_submission) = already_loaded_given_self_review_submission {
392        info!("Using already loaded given self review submission");
393        Ok(given_self_review_submission)
394    } else if let Some(course_id) = loaded_user_exercise_state.course_id {
395        info!("Loading given self review submission");
396        Ok(
397            peer_or_self_review_submissions::get_self_review_submission_by_user_and_exercise(
398                conn,
399                loaded_user_exercise_state.user_id,
400                loaded_user_exercise_state.exercise_id,
401                course_id,
402            )
403            .await?,
404        )
405    } else {
406        Err(ModelError::new(
407            ModelErrorType::PreconditionFailed,
408            "No course instance found: self review is only possible on courses".to_string(),
409            None,
410        ))
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    mod normalize_weights {
419        use super::*;
420
421        #[test]
422        fn test_normalize_weights() {
423            let peer_or_self_review_questions = vec![
424                PeerOrSelfReviewQuestion {
425                    id: Uuid::new_v4(),
426                    peer_or_self_review_config_id: Uuid::new_v4(),
427                    question_type: PeerOrSelfReviewQuestionType::Scale,
428                    order_number: 1,
429                    weight: 0.3,
430                    question: "Scale question".to_string(),
431                    created_at: chrono::Utc::now(),
432                    updated_at: chrono::Utc::now(),
433                    deleted_at: None,
434                    answer_required: true,
435                },
436                PeerOrSelfReviewQuestion {
437                    id: Uuid::new_v4(),
438                    peer_or_self_review_config_id: Uuid::new_v4(),
439                    question_type: PeerOrSelfReviewQuestionType::Essay,
440                    order_number: 2,
441                    weight: 0.1,
442                    question: "Text question".to_string(),
443                    created_at: chrono::Utc::now(),
444                    updated_at: chrono::Utc::now(),
445                    deleted_at: None,
446                    answer_required: true,
447                },
448                PeerOrSelfReviewQuestion {
449                    id: Uuid::new_v4(),
450                    peer_or_self_review_config_id: Uuid::new_v4(),
451                    question_type: PeerOrSelfReviewQuestionType::Scale,
452                    order_number: 3,
453                    weight: 0.1,
454                    question: "Scale question".to_string(),
455                    created_at: chrono::Utc::now(),
456                    updated_at: chrono::Utc::now(),
457                    deleted_at: None,
458                    answer_required: true,
459                },
460            ];
461            let normalized_peer_or_self_review_questions =
462                normalize_weights(peer_or_self_review_questions);
463            assert_eq!(normalized_peer_or_self_review_questions[0].weight, 0.75);
464            assert_eq!(normalized_peer_or_self_review_questions[1].weight, 0.0);
465            assert_eq!(normalized_peer_or_self_review_questions[2].weight, 0.25);
466        }
467    }
468}