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_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}