headless_lms_models/library/user_exercise_state_updater/
state_deriver.rs

1use headless_lms_utils::numbers::f32_to_two_decimals;
2use itertools::Itertools;
3
4use crate::{
5    exercises::{ActivityProgress, GradingProgress},
6    library::user_exercise_state_updater::validation::validate_input,
7    peer_or_self_review_configs::PeerReviewProcessingStrategy,
8    peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission,
9    peer_or_self_review_questions::{PeerOrSelfReviewQuestion, PeerOrSelfReviewQuestionType},
10    prelude::*,
11    user_exercise_states::{ReviewingStage, UserExerciseStateUpdate},
12};
13
14use super::UserExerciseStateUpdateRequiredData;
15
16/// What the peer review thinks the state should be changed to
17#[derive(Debug)]
18struct PeerOrSelfReviewOpinion {
19    score_given: Option<f32>,
20    reviewing_stage: ReviewingStage,
21}
22
23pub(super) fn derive_new_user_exercise_state(
24    input_data: UserExerciseStateUpdateRequiredData,
25) -> ModelResult<UserExerciseStateUpdate> {
26    info!("Deriving new user_exercise_state");
27
28    validate_input(&input_data)?;
29
30    let peer_or_self_review_opinion = get_peer_or_self_review_opinion(&input_data);
31    let new_reviewing_stage = derive_new_reviewing_stage(&input_data, &peer_or_self_review_opinion);
32    let reviewing_stage_changed =
33        input_data.current_user_exercise_state.reviewing_stage != new_reviewing_stage;
34
35    if reviewing_stage_changed {
36        info!(
37            "UserExerciseState {} changed reviewing_stage from {:?} to {:?}",
38            input_data.current_user_exercise_state.id,
39            input_data.current_user_exercise_state,
40            new_reviewing_stage
41        );
42    }
43
44    let new_score_given = derive_new_score_given(
45        &input_data,
46        &new_reviewing_stage,
47        &peer_or_self_review_opinion,
48    )
49    .map(f32_to_two_decimals);
50
51    if input_data.current_user_exercise_state.score_given != new_score_given {
52        info!(
53            "UserExerciseState {} changed score_given from {:?} to {:?}",
54            input_data.current_user_exercise_state.id,
55            input_data.current_user_exercise_state.score_given,
56            new_score_given
57        );
58    }
59
60    let new_activity_progress = derive_new_activity_progress(&input_data, &new_reviewing_stage);
61
62    if input_data.current_user_exercise_state.activity_progress != new_activity_progress {
63        info!(
64            "UserExerciseState {} changed activity_progress from {:?} to {:?}",
65            input_data.current_user_exercise_state.id,
66            input_data.current_user_exercise_state.activity_progress,
67            new_activity_progress
68        );
69    }
70
71    let new_grading_progress = input_data
72        .user_exercise_slide_state_grading_summary
73        .grading_progress;
74
75    if input_data.current_user_exercise_state.grading_progress != new_grading_progress {
76        info!(
77            "UserExerciseState {} changed grading_progress from {:?} to {:?}",
78            input_data.current_user_exercise_state.id,
79            input_data.current_user_exercise_state.grading_progress,
80            new_grading_progress
81        );
82    }
83
84    Ok(UserExerciseStateUpdate {
85        id: input_data.current_user_exercise_state.id,
86        score_given: new_score_given,
87        activity_progress: new_activity_progress,
88        reviewing_stage: new_reviewing_stage,
89        grading_progress: new_grading_progress,
90    })
91}
92
93fn derive_new_activity_progress(
94    input_data: &UserExerciseStateUpdateRequiredData,
95    new_reviewing_stage: &ReviewingStage,
96) -> ActivityProgress {
97    let slide_grading_progress = input_data
98        .user_exercise_slide_state_grading_summary
99        .grading_progress;
100    // If no peer review or no self review are needed, the activity is completed as soon as the user has submitted the exercise
101    if !input_data.exercise.needs_peer_review && !input_data.exercise.needs_self_review {
102        if slide_grading_progress == GradingProgress::NotReady {
103            // The user has not submitted the exercise
104            return ActivityProgress::Initialized;
105        }
106        // The user has submitted the exercise
107        return ActivityProgress::Completed;
108    };
109    // The exercise needs peer review the activity is complete once the user has done everything they have to do
110    if new_reviewing_stage == &ReviewingStage::NotStarted {
111        if slide_grading_progress == GradingProgress::NotReady {
112            // The user has not submitted the exercise
113            return ActivityProgress::Initialized;
114        }
115        // The user has submitted the exercise
116        return ActivityProgress::InProgress;
117    }
118    if new_reviewing_stage == &ReviewingStage::PeerReview
119        || new_reviewing_stage == &ReviewingStage::SelfReview
120    {
121        // The student still has to give more reviews -- their activity is not complete yet.
122        return ActivityProgress::InProgress;
123    };
124
125    ActivityProgress::Completed
126}
127
128fn derive_new_score_given(
129    input_data: &UserExerciseStateUpdateRequiredData,
130    new_reviewing_stage: &ReviewingStage,
131    peer_or_self_review_opinion: &Option<PeerOrSelfReviewOpinion>,
132) -> Option<f32> {
133    // Teacher grading decisions always override everything else
134    if let Some(teacher_grading_decision) = &input_data.latest_teacher_grading_decision {
135        return Some(teacher_grading_decision.score_given);
136    };
137
138    // If chapter_locking_enabled is enabled for the course, don't give automatic points
139    // Points will only be given after teacher manual review
140    if let Some(course) = &input_data.course
141        && course.chapter_locking_enabled
142    {
143        // Don't give automatic points - wait for teacher manual review
144        return None;
145    }
146    // We want to give or remove points only when the peer review/self review completes. If the answer receives reviews after this, we won't take away or we won't give more points.
147    // If would be confusing for the student if we afterwards changed the peer review outcome due to an additional review. That's why we haved the locked state. If the state is and stays locked, the score won't be changed.
148    if input_data.current_user_exercise_state.reviewing_stage == ReviewingStage::ReviewedAndLocked
149        && new_reviewing_stage == &ReviewingStage::ReviewedAndLocked
150        && input_data.current_user_exercise_state.score_given.is_some()
151    {
152        return input_data.current_user_exercise_state.score_given;
153    }
154    if let Some(peer_or_self_review_opinion) = peer_or_self_review_opinion
155        && (input_data.exercise.needs_peer_review || input_data.exercise.needs_self_review)
156    {
157        return peer_or_self_review_opinion.score_given;
158    }
159    // Peer reviews are not enabled, we'll just give the points according to the automated grading
160    // No need to consider the UserPointsUpdateStrategy here because it's already used when updating the user_exercise_slide_state. The user_exercise_state is just taking its data from there (and other sources).
161    input_data
162        .user_exercise_slide_state_grading_summary
163        .score_given
164}
165
166fn derive_new_reviewing_stage(
167    input_data: &UserExerciseStateUpdateRequiredData,
168    peer_or_self_review_opinion: &Option<PeerOrSelfReviewOpinion>,
169) -> ReviewingStage {
170    // Teacher grading decisions always override everything else
171    if let Some(_teacher_grading_decision) = &input_data.latest_teacher_grading_decision {
172        return ReviewingStage::ReviewedAndLocked;
173    };
174    let user_exercise_state = &input_data.current_user_exercise_state;
175    if input_data.exercise.needs_peer_review || input_data.exercise.needs_self_review {
176        peer_or_self_review_opinion
177            .as_ref()
178            .map(|o| o.reviewing_stage)
179            .unwrap_or_else(|| input_data.current_user_exercise_state.reviewing_stage)
180    } else {
181        // Valid states for exercises without peer review are `ReviewingStage::NotStarted` or `ReviewingStage::ReviewedAndLocked`.
182        // If the state is one of those, we'll keep it but if the state is something not allowed, we'll reset it to the default.
183        // Most states need to stay in the ReviewingStage::NotStarted stage
184        if user_exercise_state.reviewing_stage == ReviewingStage::NotStarted
185            || user_exercise_state.reviewing_stage == ReviewingStage::ReviewedAndLocked
186        {
187            user_exercise_state.reviewing_stage
188        } else {
189            warn!(reviewing_stage = ?user_exercise_state.reviewing_stage, "Reviewing stage was in invalid state for an exercise without peer review. Resetting to ReviewingStage::NotStarted.");
190            ReviewingStage::NotStarted
191        }
192    }
193}
194
195#[instrument(skip(input_data))]
196fn get_peer_or_self_review_opinion(
197    input_data: &UserExerciseStateUpdateRequiredData,
198) -> Option<PeerOrSelfReviewOpinion> {
199    if !input_data.exercise.needs_peer_review && !input_data.exercise.needs_self_review {
200        // Peer review or self review is not enabled, no opinion
201        return None;
202    }
203
204    if input_data.current_user_exercise_state.reviewing_stage == ReviewingStage::NotStarted {
205        // The user has not started a peer review or a self review, so our opinion is that the user should not receive any points yet.
206        return Some(PeerOrSelfReviewOpinion {
207            score_given: None,
208            reviewing_stage: ReviewingStage::NotStarted,
209        });
210    }
211
212    let score_maximum = input_data.exercise.score_maximum;
213    if let Some(info) = &input_data.peer_or_self_review_information {
214        if input_data.exercise.needs_peer_review {
215            let given_enough_peer_reviews = info.given_peer_or_self_review_submissions.len() as i32
216                >= info.peer_or_self_review_config.peer_reviews_to_give;
217            // Received enough peer reviews is cached to the queue entry, lets use it here to make sure its value has been kept up-to-date.
218            let received_enough_peer_reviews = info
219                .peer_review_queue_entry
220                .as_ref()
221                .map(|o| o.received_enough_peer_reviews)
222                .unwrap_or(false);
223
224            if !given_enough_peer_reviews {
225                // Keeps the state in Intialized or PeerReview
226                return Some(PeerOrSelfReviewOpinion {
227                    score_given: None,
228                    reviewing_stage: input_data.current_user_exercise_state.reviewing_stage,
229                });
230            } else if !received_enough_peer_reviews {
231                // Has given enough but has not received enough: the student has to wait until others have reviewed their answer more
232
233                // Handle the case where the answer is waiting for manual review but is still receiving peer reviews
234                if input_data.current_user_exercise_state.reviewing_stage
235                    == ReviewingStage::WaitingForManualGrading
236                {
237                    return Some(PeerOrSelfReviewOpinion {
238                        score_given: None,
239                        reviewing_stage: ReviewingStage::WaitingForManualGrading,
240                    });
241                }
242
243                if input_data.exercise.needs_self_review
244                    && info.given_self_review_submission.is_none()
245                {
246                    // Student has given enough peer reviews but has not self reviewed yet.
247                    return Some(PeerOrSelfReviewOpinion {
248                        score_given: None,
249                        reviewing_stage: ReviewingStage::SelfReview,
250                    });
251                }
252
253                return Some(PeerOrSelfReviewOpinion {
254                    score_given: None,
255                    reviewing_stage: ReviewingStage::WaitingForPeerReviews,
256                });
257            }
258        }
259
260        // Given and received enough peer reviews
261        if input_data.exercise.needs_self_review {
262            if input_data.exercise.needs_peer_review {
263                if info.given_self_review_submission.is_none() {
264                    // Student has given and received enough peer reviews but has not self reviewed yet.
265                    return Some(PeerOrSelfReviewOpinion {
266                        score_given: None,
267                        reviewing_stage: ReviewingStage::SelfReview,
268                    });
269                }
270            } else if info.given_self_review_submission.is_some() {
271                // Student has given a self review and there is no peer review. There is no way to determine a score for the student automatically, so we'll give the answer to the teacher to review.
272                return Some(PeerOrSelfReviewOpinion {
273                    score_given: None,
274                    reviewing_stage: ReviewingStage::WaitingForManualGrading,
275                });
276            } else {
277                // Student has not given a self review yet.
278                return Some(PeerOrSelfReviewOpinion {
279                    score_given: None,
280                    reviewing_stage: ReviewingStage::SelfReview,
281                });
282            }
283        }
284
285        // Users have given and received enough peer reviews, time to consider how we're doing the grading
286        match info.peer_or_self_review_config.processing_strategy {
287            PeerReviewProcessingStrategy::AutomaticallyGradeByAverage => {
288                let avg = calculate_average_received_peer_review_score(
289                    &info
290                        .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions,
291                );
292                if !info.peer_or_self_review_config.points_are_all_or_nothing {
293                    let score_given = calculate_peer_review_weighted_points(
294                        &info.peer_or_self_review_questions,
295                        &info
296                            .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions,
297                        score_maximum,
298                    );
299                    Some(PeerOrSelfReviewOpinion {
300                        score_given: Some(score_given),
301                        reviewing_stage: ReviewingStage::ReviewedAndLocked,
302                    })
303                } else if avg < info.peer_or_self_review_config.accepting_threshold {
304                    info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Automatically giving zero points because average is below the threshold");
305                    Some(PeerOrSelfReviewOpinion {
306                        score_given: Some(0.0),
307                        reviewing_stage: ReviewingStage::ReviewedAndLocked,
308                    })
309                } else {
310                    info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Automatically giving the points since the average is above the threshold");
311                    Some(PeerOrSelfReviewOpinion {
312                        score_given: Some(score_maximum as f32),
313                        reviewing_stage: ReviewingStage::ReviewedAndLocked,
314                    })
315                }
316            }
317            PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage => {
318                let avg = calculate_average_received_peer_review_score(
319                    &info
320                        .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions,
321                );
322                if avg < info.peer_or_self_review_config.accepting_threshold {
323                    info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Not giving points because average is below the threshold. The answer should be moved to manual review.");
324                    Some(PeerOrSelfReviewOpinion {
325                        score_given: None,
326                        reviewing_stage: ReviewingStage::WaitingForManualGrading,
327                    })
328                } else if !info.peer_or_self_review_config.points_are_all_or_nothing {
329                    let score_given = calculate_peer_review_weighted_points(
330                        &info.peer_or_self_review_questions,
331                        &info
332                            .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions,
333                        score_maximum,
334                    );
335                    Some(PeerOrSelfReviewOpinion {
336                        score_given: Some(score_given),
337                        reviewing_stage: ReviewingStage::ReviewedAndLocked,
338                    })
339                } else {
340                    info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Automatically giving the points since the average is above the threshold");
341                    Some(PeerOrSelfReviewOpinion {
342                        score_given: Some(score_maximum as f32),
343                        reviewing_stage: ReviewingStage::ReviewedAndLocked,
344                    })
345                }
346            }
347            PeerReviewProcessingStrategy::ManualReviewEverything => {
348                info!(peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Not giving points because the teacher reviews all answers manually");
349                Some(PeerOrSelfReviewOpinion {
350                    score_given: None,
351                    reviewing_stage: ReviewingStage::WaitingForManualGrading,
352                })
353            }
354        }
355    } else {
356        // Even though the exercise needs peer review, the peer review has not been configured. The safest thing to do here is to consider peer review as not complete
357        warn!("Peer review is enabled in the exercise but no peer_or_self_review_config found");
358        None
359    }
360}
361
362fn calculate_average_received_peer_review_score(
363    peer_or_self_review_question_submissions: &[PeerOrSelfReviewQuestionSubmission],
364) -> f32 {
365    let answers_considered = peer_or_self_review_question_submissions
366        .iter()
367        .filter_map(|prqs| {
368            if prqs.deleted_at.is_some() {
369                return None;
370            }
371            prqs.number_data
372        })
373        .collect::<Vec<_>>();
374    if answers_considered.is_empty() {
375        warn!(
376            "No peer review question submissions for this answer with number data. Assuming score is 0."
377        );
378        return 0.0;
379    }
380    answers_considered.iter().sum::<f32>() / answers_considered.len() as f32
381}
382
383fn calculate_peer_review_weighted_points(
384    peer_or_self_review_questions: &[PeerOrSelfReviewQuestion],
385    received_peer_or_self_review_question_submissions: &[PeerOrSelfReviewQuestionSubmission],
386    score_maximum: i32,
387) -> f32 {
388    // Weights should be sum to 1. This should be guranteed by the data loader.
389    let questions_considered_for_weighted_points = peer_or_self_review_questions
390        .iter()
391        .filter(|prq| prq.question_type == PeerOrSelfReviewQuestionType::Scale)
392        .collect::<Vec<_>>();
393    let question_submissions_considered_for_weighted_points =
394        received_peer_or_self_review_question_submissions
395            .iter()
396            .filter(|prqs| {
397                questions_considered_for_weighted_points
398                    .iter()
399                    .any(|prq| prq.id == prqs.peer_or_self_review_question_id)
400            })
401            .collect::<Vec<_>>();
402    let number_of_submissions = question_submissions_considered_for_weighted_points
403        .iter()
404        .map(|prqs| prqs.peer_or_self_review_submission_id)
405        .unique()
406        .count();
407    let grouped = question_submissions_considered_for_weighted_points
408        .iter()
409        .chunk_by(|prqs| prqs.peer_or_self_review_submission_id);
410
411    let weighted_score_by_submission = grouped
412        .into_iter()
413        .map(
414            |(_peer_or_self_review_submission_id, peer_review_question_answers)| {
415                peer_review_question_answers
416                    .filter_map(|prqs| {
417                        questions_considered_for_weighted_points
418                            .iter()
419                            .find(|prq| prq.id == prqs.peer_or_self_review_question_id)
420                            .map(|question| question.weight * prqs.number_data.unwrap_or_default())
421                    })
422                    .sum::<f32>()
423            },
424        )
425        .collect::<Vec<_>>();
426    let average_weighted_score =
427        weighted_score_by_submission.iter().sum::<f32>() / number_of_submissions as f32;
428    info!(
429        "Average weighted score is {} ({:?})",
430        average_weighted_score, weighted_score_by_submission
431    );
432    // Always 5 because the students answer from 1-5.
433    let number_of_answer_options = 5.0;
434
435    average_weighted_score / number_of_answer_options * score_maximum as f32
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use crate::courses::Course;
442
443    mod derive_new_user_exercise_state {
444        use chrono::TimeZone;
445
446        use crate::{
447            exercises::Exercise,
448            library::user_exercise_state_updater::UserExerciseStateUpdateRequiredDataPeerReviewInformation,
449            peer_or_self_review_configs::PeerOrSelfReviewConfig,
450            peer_or_self_review_submissions::PeerOrSelfReviewSubmission,
451            peer_review_queue_entries::PeerReviewQueueEntry,
452            user_exercise_slide_states::UserExerciseSlideStateGradingSummary,
453            user_exercise_states::UserExerciseState,
454        };
455
456        use super::*;
457
458        #[test]
459        fn updates_state_for_normal_exercise() {
460            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
461            let exercise = create_exercise(CourseOrExamId::Course(id), false, false, false);
462            let user_exercise_state = create_user_exercise_state(
463                &exercise,
464                None,
465                ActivityProgress::Initialized,
466                ReviewingStage::NotStarted,
467            );
468            let new_user_exercise_state =
469                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
470                    exercise: exercise.clone(),
471                    current_user_exercise_state: user_exercise_state,
472                    peer_or_self_review_information: None,
473                    latest_teacher_grading_decision: None,
474                    user_exercise_slide_state_grading_summary:
475                        UserExerciseSlideStateGradingSummary {
476                            score_given: Some(1.0),
477                            grading_progress: GradingProgress::FullyGraded,
478                        },
479                    chapter: None,
480                    course: exercise
481                        .course_id
482                        .map(create_course)
483                        .or_else(|| Some(create_course(id))),
484                })
485                .unwrap();
486            assert_results(
487                &new_user_exercise_state,
488                Some(1.0),
489                ActivityProgress::Completed,
490                // Exercises that don't have peer review new leave the not started stage
491                ReviewingStage::NotStarted,
492            );
493        }
494
495        #[test]
496        fn doesnt_update_score_for_exercise_that_needs_to_be_peer_reviewed() {
497            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
498            let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
499            let user_exercise_state = create_user_exercise_state(
500                &exercise,
501                None,
502                ActivityProgress::Initialized,
503                ReviewingStage::NotStarted,
504            );
505            let new_user_exercise_state =
506                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
507                    exercise: exercise.clone(),
508                    current_user_exercise_state: user_exercise_state,
509                    peer_or_self_review_information: Some(
510                        UserExerciseStateUpdateRequiredDataPeerReviewInformation {
511                            given_peer_or_self_review_submissions: Vec::new(),
512                            given_self_review_submission: None,
513                            latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: Vec::new(),
514                            peer_review_queue_entry: None,
515                            peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
516                            peer_or_self_review_questions: Vec::new(),
517                        },
518                    ),
519                    latest_teacher_grading_decision: None,
520                        user_exercise_slide_state_grading_summary:
521                            UserExerciseSlideStateGradingSummary {
522                                score_given: Some(1.0),
523                                grading_progress: GradingProgress::FullyGraded,
524                            },
525                        chapter: None,
526                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
527                    })
528                    .unwrap();
529            assert_results(
530                &new_user_exercise_state,
531                None,
532                ActivityProgress::InProgress,
533                ReviewingStage::NotStarted,
534            );
535        }
536
537        mod automatically_accept_or_reject_by_average {
538            use super::*;
539
540            #[test]
541            fn peer_review_automatically_accept_or_reject_by_average_works_gives_full_points() {
542                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
543                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
544                let user_exercise_state = create_user_exercise_state(
545                    &exercise,
546                    None,
547                    ActivityProgress::Initialized,
548                    ReviewingStage::PeerReview,
549                );
550                let new_user_exercise_state =
551                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
552                        exercise: exercise.clone(),
553                        current_user_exercise_state: user_exercise_state,
554                        peer_or_self_review_information: Some(
555                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
556                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
557                                given_self_review_submission: None,
558                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)],
559                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
560                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
561                                peer_or_self_review_questions: Vec::new(),
562                            },
563                        ),
564                        latest_teacher_grading_decision: None,
565                        user_exercise_slide_state_grading_summary:
566                            UserExerciseSlideStateGradingSummary {
567                                score_given: Some(1.0),
568                                grading_progress: GradingProgress::FullyGraded,
569                            },
570                        chapter: None,
571                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
572                    })
573                    .unwrap();
574                assert_results(
575                    &new_user_exercise_state,
576                    // The user passed peer review, so they deserve full points from the exercise
577                    Some(9000.0),
578                    ActivityProgress::Completed,
579                    ReviewingStage::ReviewedAndLocked,
580                );
581            }
582
583            #[test]
584            fn peer_review_automatically_accept_or_reject_by_average_works_gives_zero_points() {
585                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
586                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
587                let user_exercise_state = create_user_exercise_state(
588                    &exercise,
589                    None,
590                    ActivityProgress::Initialized,
591                    ReviewingStage::PeerReview,
592                );
593                let new_user_exercise_state =
594                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
595                        exercise: exercise.clone(),
596                        current_user_exercise_state: user_exercise_state,
597                        peer_or_self_review_information: Some(
598                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
599                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
600                                given_self_review_submission: None,
601                                // Average below 2.1
602                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)],
603                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
604                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
605                                peer_or_self_review_questions: Vec::new(),
606                            },
607                        ),
608                        latest_teacher_grading_decision: None,
609                        user_exercise_slide_state_grading_summary:
610                            UserExerciseSlideStateGradingSummary {
611                                score_given: Some(1.0),
612                                grading_progress: GradingProgress::FullyGraded,
613                            },
614                        chapter: None,
615                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
616                    })
617                    .unwrap();
618                assert_results(
619                    &new_user_exercise_state,
620                    // The user failed peer review, so they get zero points
621                    Some(0.0),
622                    ActivityProgress::Completed,
623                    ReviewingStage::ReviewedAndLocked,
624                );
625            }
626        }
627
628        mod automatically_accept_or_manual_review_by_average {
629            use super::*;
630
631            #[test]
632            fn peer_review_automatically_accept_or_manual_review_by_average_works_gives_full_points()
633             {
634                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
635                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
636                let user_exercise_state = create_user_exercise_state(
637                    &exercise,
638                    None,
639                    ActivityProgress::Initialized,
640                    ReviewingStage::PeerReview,
641                );
642                let new_user_exercise_state =
643                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
644                        exercise: exercise.clone(),
645                        current_user_exercise_state: user_exercise_state,
646                        peer_or_self_review_information: Some(
647                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
648                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
649                                given_self_review_submission: None,
650                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)],
651                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
652                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage),
653                                peer_or_self_review_questions: Vec::new(),
654                            },
655                        ),
656                        latest_teacher_grading_decision: None,
657                        user_exercise_slide_state_grading_summary:
658                            UserExerciseSlideStateGradingSummary {
659                                score_given: Some(1.0),
660                                grading_progress: GradingProgress::FullyGraded,
661                            },
662                        chapter: None,
663                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
664                    })
665                    .unwrap();
666                assert_results(
667                    &new_user_exercise_state,
668                    // The user passed peer review, so they deserve full points from the exercise
669                    Some(9000.0),
670                    ActivityProgress::Completed,
671                    ReviewingStage::ReviewedAndLocked,
672                );
673            }
674
675            #[test]
676            fn peer_review_automatically_accept_or_manual_review_by_average_works_puts_the_answer_to_manual_review()
677             {
678                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
679                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
680                let user_exercise_state = create_user_exercise_state(
681                    &exercise,
682                    None,
683                    ActivityProgress::Initialized,
684                    ReviewingStage::PeerReview,
685                );
686                let new_user_exercise_state =
687                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
688                        exercise: exercise.clone(),
689                        current_user_exercise_state: user_exercise_state,
690                        peer_or_self_review_information: Some(
691                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
692                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
693                                given_self_review_submission: None,
694                                // Average below 2.1
695                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)],
696                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
697                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage),
698                                peer_or_self_review_questions: Vec::new(),
699                            },
700                        ),
701                        latest_teacher_grading_decision: None,
702                        user_exercise_slide_state_grading_summary:
703                            UserExerciseSlideStateGradingSummary {
704                                score_given: Some(1.0),
705                                grading_progress: GradingProgress::FullyGraded,
706                            },
707                        chapter: None,
708                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
709                    })
710                    .unwrap();
711                assert_results(
712                    &new_user_exercise_state,
713                    // Manual review, we won't give any points because the points are up to the teacher's descision in the review
714                    None,
715                    ActivityProgress::Completed,
716                    ReviewingStage::WaitingForManualGrading,
717                );
718            }
719        }
720
721        mod manual_review_everything {
722            use super::*;
723
724            #[test]
725            fn peer_review_manual_review_everything_works_does_not_give_full_points_to_passing_answer_and_puts_to_manual_review()
726             {
727                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
728                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
729                let user_exercise_state = create_user_exercise_state(
730                    &exercise,
731                    None,
732                    ActivityProgress::Initialized,
733                    ReviewingStage::PeerReview,
734                );
735                let new_user_exercise_state =
736                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
737                        exercise: exercise.clone(),
738                        current_user_exercise_state: user_exercise_state,
739                        peer_or_self_review_information: Some(
740                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
741                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
742                                given_self_review_submission: None,
743                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)],
744                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
745                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::ManualReviewEverything),
746                                peer_or_self_review_questions: Vec::new(),
747                            },
748                        ),
749                        latest_teacher_grading_decision: None,
750                        user_exercise_slide_state_grading_summary:
751                            UserExerciseSlideStateGradingSummary {
752                                score_given: Some(1.0),
753                                grading_progress: GradingProgress::FullyGraded,
754                            },
755                        chapter: None,
756                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
757                    })
758                    .unwrap();
759                assert_results(
760                    &new_user_exercise_state,
761                    // Score will be given from the manual review
762                    None,
763                    ActivityProgress::Completed,
764                    ReviewingStage::WaitingForManualGrading,
765                );
766            }
767
768            #[test]
769            fn peer_review_manual_review_everything_works_puts_failing_answer_the_answer_to_manual_review()
770             {
771                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
772                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
773                let user_exercise_state = create_user_exercise_state(
774                    &exercise,
775                    None,
776                    ActivityProgress::Initialized,
777                    ReviewingStage::PeerReview,
778                );
779                let new_user_exercise_state =
780                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
781                        exercise: exercise.clone(),
782                        current_user_exercise_state: user_exercise_state,
783                        peer_or_self_review_information: Some(
784                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
785                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
786                                given_self_review_submission: None,
787                                // Average below 2.1
788                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)],
789                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
790                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::ManualReviewEverything),
791                                peer_or_self_review_questions: Vec::new(),
792                            },
793                        ),
794                        latest_teacher_grading_decision: None,
795                        user_exercise_slide_state_grading_summary:
796                            UserExerciseSlideStateGradingSummary {
797                                score_given: Some(1.0),
798                                grading_progress: GradingProgress::FullyGraded,
799                            },
800                        chapter: None,
801                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
802                    })
803                    .unwrap();
804                assert_results(
805                    &new_user_exercise_state,
806                    // Score will be given from the manual review
807                    None,
808                    ActivityProgress::Completed,
809                    ReviewingStage::WaitingForManualGrading,
810                );
811            }
812        }
813
814        mod calculate_peer_review_weighted_points {
815            use uuid::Uuid;
816
817            use crate::library::user_exercise_state_updater::state_deriver::{
818                calculate_peer_review_weighted_points,
819                tests::derive_new_user_exercise_state::{
820                    create_peer_review_question_essay, create_peer_review_question_scale,
821                    create_peer_review_question_submission_with_ids,
822                },
823            };
824
825            #[test]
826            fn calculate_peer_review_weighted_points_works() {
827                let q1_id = Uuid::parse_str("d42ecbc9-34ff-4549-aacf-1b8ac6e672c2").unwrap();
828                let q2_id = Uuid::parse_str("1a018bb2-023f-4f58-b5f1-b09d58b42ed8").unwrap();
829                let q3_id = Uuid::parse_str("9ab2df96-60a4-40c2-a097-900654f44700").unwrap();
830                let q4_id = Uuid::parse_str("4bed2265-3c8f-4387-83e9-76e2b673eea3").unwrap();
831                let e1_id = Uuid::parse_str("fd4e5f7e-e794-4993-954e-fbd2d8b04d6b").unwrap();
832
833                let s1_id = Uuid::parse_str("2795b352-d5ef-41c7-92f7-a60d90c62c91").unwrap();
834                let s2_id = Uuid::parse_str("e5c16a89-2a3f-4910-9b00-dd981cedcbcc").unwrap();
835                let s3_id = Uuid::parse_str("462a6493-a506-42e6-869d-10220b2885b8").unwrap();
836
837                let res = calculate_peer_review_weighted_points(
838                    &vec![
839                        create_peer_review_question_scale(q1_id, 0.25),
840                        create_peer_review_question_scale(q2_id, 0.25),
841                        create_peer_review_question_scale(q3_id, 0.25),
842                        create_peer_review_question_scale(q4_id, 0.25),
843                        // Extra one to check that ignoring questions works
844                        create_peer_review_question_essay(e1_id, 0.25),
845                    ],
846                    &vec![
847                        // First student
848                        create_peer_review_question_submission_with_ids(5.0, q1_id, s1_id),
849                        create_peer_review_question_submission_with_ids(4.0, q2_id, s1_id),
850                        create_peer_review_question_submission_with_ids(5.0, q3_id, s1_id),
851                        create_peer_review_question_submission_with_ids(5.0, q4_id, s1_id),
852                        // Second student
853                        create_peer_review_question_submission_with_ids(4.0, q1_id, s2_id),
854                        create_peer_review_question_submission_with_ids(2.0, q2_id, s2_id),
855                        create_peer_review_question_submission_with_ids(3.0, q3_id, s2_id),
856                        create_peer_review_question_submission_with_ids(4.0, q4_id, s2_id),
857                        // Third student
858                        create_peer_review_question_submission_with_ids(3.0, q1_id, s3_id),
859                        create_peer_review_question_submission_with_ids(2.0, q2_id, s3_id),
860                        create_peer_review_question_submission_with_ids(4.0, q3_id, s3_id),
861                        create_peer_review_question_submission_with_ids(4.0, q4_id, s3_id),
862                        // Extra one to check that ignoring questions works
863                        create_peer_review_question_submission_with_ids(3.0, e1_id, s3_id),
864                    ],
865                    4,
866                );
867                assert_eq!(res, 3.0);
868            }
869        }
870
871        mod self_review {
872            use super::*;
873
874            #[test]
875            fn if_self_review_enabled_does_not_put_answer_automatically_to_self_review() {
876                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
877                let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true);
878                let user_exercise_state = create_user_exercise_state(
879                    &exercise,
880                    None,
881                    ActivityProgress::Initialized,
882                    ReviewingStage::NotStarted,
883                );
884                let new_user_exercise_state =
885                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
886                        exercise: exercise.clone(),
887                        current_user_exercise_state: user_exercise_state,
888                        peer_or_self_review_information: Some(
889                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
890                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
891                                given_self_review_submission: None,
892                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)],
893                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
894                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
895                                peer_or_self_review_questions: Vec::new(),
896                            },
897                        ),
898                        latest_teacher_grading_decision: None,
899                        user_exercise_slide_state_grading_summary:
900                            UserExerciseSlideStateGradingSummary {
901                                score_given: Some(1.0),
902                                grading_progress: GradingProgress::FullyGraded,
903                            },
904                        chapter: None,
905                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
906                    })
907                    .unwrap();
908                assert_results(
909                    &new_user_exercise_state,
910                    None,
911                    ActivityProgress::InProgress,
912                    ReviewingStage::NotStarted,
913                );
914            }
915
916            #[test]
917            fn if_peer_and_self_review_enabled_self_review_comes_after_peer_review() {
918                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
919                let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true);
920                let user_exercise_state = create_user_exercise_state(
921                    &exercise,
922                    None,
923                    ActivityProgress::Initialized,
924                    ReviewingStage::PeerReview,
925                );
926                let new_user_exercise_state =
927                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
928                        exercise: exercise.clone(),
929                        current_user_exercise_state: user_exercise_state,
930                        peer_or_self_review_information: Some(
931                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
932                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
933                                given_self_review_submission: None,
934                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![],
935                                peer_review_queue_entry: Some(create_peer_review_queue_entry(false)),
936                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
937                                peer_or_self_review_questions: Vec::new(),
938                            },
939                        ),
940                        latest_teacher_grading_decision: None,
941                        user_exercise_slide_state_grading_summary:
942                            UserExerciseSlideStateGradingSummary {
943                                score_given: Some(1.0),
944                                grading_progress: GradingProgress::FullyGraded,
945                            },
946                        chapter: None,
947                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
948                    })
949                    .unwrap();
950                assert_results(
951                    &new_user_exercise_state,
952                    None,
953                    ActivityProgress::InProgress,
954                    ReviewingStage::SelfReview,
955                );
956            }
957
958            #[test]
959            fn moves_out_of_self_review() {
960                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
961                let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true);
962                let user_exercise_state = create_user_exercise_state(
963                    &exercise,
964                    None,
965                    ActivityProgress::Initialized,
966                    ReviewingStage::SelfReview,
967                );
968                let new_user_exercise_state =
969                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
970                        exercise: exercise.clone(),
971                        current_user_exercise_state: user_exercise_state,
972                        peer_or_self_review_information: Some(
973                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
974                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
975                                given_self_review_submission: Some(create_peer_review_submission()),
976                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)],
977                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
978                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
979                                peer_or_self_review_questions: Vec::new(),
980                            },
981                        ),
982                        latest_teacher_grading_decision: None,
983                        user_exercise_slide_state_grading_summary:
984                            UserExerciseSlideStateGradingSummary {
985                                score_given: Some(1.0),
986                                grading_progress: GradingProgress::FullyGraded,
987                            },
988                        chapter: None,
989                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
990                    })
991                    .unwrap();
992                assert_results(
993                    &new_user_exercise_state,
994                    Some(9000.0),
995                    ActivityProgress::Completed,
996                    ReviewingStage::ReviewedAndLocked,
997                );
998            }
999
1000            // User has to start the self review themselves by clicking a button.
1001            #[test]
1002            fn does_not_move_to_self_review_if_self_review_but_no_peer_review() {
1003                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1004                let exercise = create_exercise(CourseOrExamId::Course(id), false, true, true);
1005                let user_exercise_state = create_user_exercise_state(
1006                    &exercise,
1007                    None,
1008                    ActivityProgress::Initialized,
1009                    ReviewingStage::NotStarted,
1010                );
1011                let new_user_exercise_state =
1012                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
1013                        exercise: exercise.clone(),
1014                        current_user_exercise_state: user_exercise_state,
1015                        peer_or_self_review_information: Some(
1016                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
1017                                given_peer_or_self_review_submissions: vec![],
1018                                given_self_review_submission: None,
1019                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![],
1020                                peer_review_queue_entry: None,
1021                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
1022                                peer_or_self_review_questions: Vec::new(),
1023                            },
1024                        ),
1025                        latest_teacher_grading_decision: None,
1026                        user_exercise_slide_state_grading_summary:
1027                            UserExerciseSlideStateGradingSummary {
1028                                score_given: Some(1.0),
1029                                grading_progress: GradingProgress::FullyGraded,
1030                            },
1031                        chapter: None,
1032                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
1033                    })
1034                    .unwrap();
1035                assert_results(
1036                    &new_user_exercise_state,
1037                    None,
1038                    ActivityProgress::InProgress,
1039                    ReviewingStage::NotStarted,
1040                );
1041            }
1042        }
1043
1044        #[test]
1045        fn moves_out_of_self_review_if_self_review_but_no_peer_review() {
1046            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1047            let exercise = create_exercise(CourseOrExamId::Course(id), false, true, true);
1048            let user_exercise_state = create_user_exercise_state(
1049                &exercise,
1050                None,
1051                ActivityProgress::Initialized,
1052                ReviewingStage::SelfReview,
1053            );
1054            let new_user_exercise_state =
1055                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
1056                    exercise: exercise.clone(),
1057                    current_user_exercise_state: user_exercise_state,
1058                    peer_or_self_review_information: Some(
1059                        UserExerciseStateUpdateRequiredDataPeerReviewInformation {
1060                            given_peer_or_self_review_submissions: vec![],
1061                            given_self_review_submission: Some(create_peer_review_submission()),
1062                            latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![],
1063                            peer_review_queue_entry: None,
1064                            peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
1065                            peer_or_self_review_questions: Vec::new(),
1066                        },
1067                    ),
1068                    latest_teacher_grading_decision: None,
1069                        user_exercise_slide_state_grading_summary:
1070                            UserExerciseSlideStateGradingSummary {
1071                                score_given: Some(1.0),
1072                                grading_progress: GradingProgress::FullyGraded,
1073                            },
1074                        chapter: None,
1075                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
1076                    })
1077                    .unwrap();
1078            assert_results(
1079                &new_user_exercise_state,
1080                None,
1081                ActivityProgress::Completed,
1082                ReviewingStage::WaitingForManualGrading,
1083            );
1084        }
1085
1086        fn assert_results(
1087            update: &UserExerciseStateUpdate,
1088            score_given: Option<f32>,
1089            activity_progress: ActivityProgress,
1090            reviewing_stage: ReviewingStage,
1091        ) {
1092            assert_eq!(update.score_given, score_given);
1093            assert_eq!(update.activity_progress, activity_progress);
1094            assert_eq!(update.reviewing_stage, reviewing_stage);
1095        }
1096
1097        fn create_exercise(
1098            course_or_exam_id: CourseOrExamId,
1099            needs_peer_review: bool,
1100            needs_self_review: bool,
1101            use_course_default_peer_or_self_review_config: bool,
1102        ) -> Exercise {
1103            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1104            let (course_id, exam_id) = course_or_exam_id.to_course_and_exam_ids();
1105            Exercise {
1106                id,
1107                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1108                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1109                name: "".to_string(),
1110                course_id,
1111                exam_id,
1112                page_id: id,
1113                chapter_id: None,
1114                deadline: None,
1115                deleted_at: None,
1116                score_maximum: 9000,
1117                order_number: 0,
1118                copied_from: None,
1119                max_tries_per_slide: None,
1120                limit_number_of_tries: false,
1121                needs_peer_review,
1122                use_course_default_peer_or_self_review_config,
1123                exercise_language_group_id: None,
1124                needs_self_review,
1125            }
1126        }
1127
1128        fn create_course(course_id: Uuid) -> Course {
1129            Course {
1130                id: course_id,
1131                slug: "test-course".to_string(),
1132                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1133                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1134                name: "Test Course".to_string(),
1135                description: None,
1136                organization_id: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(),
1137                deleted_at: None,
1138                language_code: "en".to_string(),
1139                copied_from: None,
1140                content_search_language: None,
1141                course_language_group_id: Uuid::parse_str("00000000-0000-0000-0000-000000000001")
1142                    .unwrap(),
1143                is_draft: false,
1144                is_test_mode: false,
1145                is_unlisted: false,
1146                base_module_completion_requires_n_submodule_completions: 0,
1147                can_add_chatbot: false,
1148                is_joinable_by_code_only: false,
1149                join_code: None,
1150                ask_marketing_consent: false,
1151                flagged_answers_threshold: None,
1152                flagged_answers_skip_manual_review_and_allow_retry: false,
1153                closed_at: None,
1154                closed_additional_message: None,
1155                closed_course_successor_id: None,
1156                chapter_locking_enabled: false,
1157            }
1158        }
1159
1160        fn create_peer_or_self_review_config(
1161            processing_strategy: PeerReviewProcessingStrategy,
1162        ) -> PeerOrSelfReviewConfig {
1163            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1164            PeerOrSelfReviewConfig {
1165                id,
1166                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1167                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1168                deleted_at: None,
1169                course_id: id,
1170                exercise_id: None,
1171                peer_reviews_to_give: 3,
1172                peer_reviews_to_receive: 2,
1173                accepting_threshold: 2.1,
1174                processing_strategy,
1175                reset_answer_if_zero_points_from_review: false,
1176                manual_review_cutoff_in_days: 21,
1177                points_are_all_or_nothing: true,
1178                review_instructions: None,
1179            }
1180        }
1181
1182        fn create_peer_review_question_submission(
1183            number_data: f32,
1184        ) -> PeerOrSelfReviewQuestionSubmission {
1185            PeerOrSelfReviewQuestionSubmission {
1186                id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a").unwrap(),
1187                created_at: Utc::now(),
1188                updated_at: Utc::now(),
1189                deleted_at: None,
1190                peer_or_self_review_question_id: Uuid::parse_str(
1191                    "b853bbd7-feee-4447-ab14-c9622e565ea1",
1192                )
1193                .unwrap(),
1194                peer_or_self_review_submission_id: Uuid::parse_str(
1195                    "be4061b5-b468-4f50-93b0-cf3bf9de9a13",
1196                )
1197                .unwrap(),
1198                text_data: None,
1199                number_data: Some(number_data),
1200            }
1201        }
1202
1203        fn create_peer_review_question_submission_with_ids(
1204            number_data: f32,
1205            peer_or_self_review_question_id: Uuid,
1206            peer_or_self_review_submission_id: Uuid,
1207        ) -> PeerOrSelfReviewQuestionSubmission {
1208            PeerOrSelfReviewQuestionSubmission {
1209                id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a").unwrap(),
1210                created_at: Utc::now(),
1211                updated_at: Utc::now(),
1212                deleted_at: None,
1213                peer_or_self_review_question_id,
1214                peer_or_self_review_submission_id,
1215                text_data: None,
1216                number_data: Some(number_data),
1217            }
1218        }
1219
1220        fn create_peer_review_question_scale(id: Uuid, weight: f32) -> PeerOrSelfReviewQuestion {
1221            PeerOrSelfReviewQuestion {
1222                id,
1223                weight,
1224                created_at: Utc::now(),
1225                updated_at: Utc::now(),
1226                deleted_at: None,
1227                peer_or_self_review_config_id: Uuid::parse_str(
1228                    "bf923ea4-a637-4d97-b78b-6f843d76120a",
1229                )
1230                .unwrap(),
1231                order_number: 1,
1232                question: "A question".to_string(),
1233                question_type: PeerOrSelfReviewQuestionType::Scale,
1234                answer_required: true,
1235            }
1236        }
1237
1238        fn create_peer_review_question_essay(id: Uuid, weight: f32) -> PeerOrSelfReviewQuestion {
1239            PeerOrSelfReviewQuestion {
1240                id,
1241                weight,
1242                created_at: Utc::now(),
1243                updated_at: Utc::now(),
1244                deleted_at: None,
1245                peer_or_self_review_config_id: Uuid::parse_str(
1246                    "bf923ea4-a637-4d97-b78b-6f843d76120a",
1247                )
1248                .unwrap(),
1249                order_number: 1,
1250                question: "A question".to_string(),
1251                question_type: PeerOrSelfReviewQuestionType::Essay,
1252                answer_required: true,
1253            }
1254        }
1255
1256        fn create_peer_review_submission() -> PeerOrSelfReviewSubmission {
1257            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1258            PeerOrSelfReviewSubmission {
1259                id,
1260                created_at: Utc::now(),
1261                updated_at: Utc::now(),
1262                deleted_at: None,
1263                user_id: id,
1264                exercise_id: id,
1265                course_id: id,
1266                peer_or_self_review_config_id: id,
1267                exercise_slide_submission_id: id,
1268            }
1269        }
1270
1271        fn create_peer_review_queue_entry(
1272            received_enough_peer_reviews: bool,
1273        ) -> PeerReviewQueueEntry {
1274            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1275            PeerReviewQueueEntry {
1276                id,
1277                created_at: Utc::now(),
1278                updated_at: Utc::now(),
1279                deleted_at: None,
1280                user_id: id,
1281                exercise_id: id,
1282                course_id: id,
1283                receiving_peer_reviews_exercise_slide_submission_id: id,
1284                received_enough_peer_reviews,
1285                peer_review_priority: 100,
1286                removed_from_queue_for_unusual_reason: false,
1287            }
1288        }
1289
1290        fn create_user_exercise_state(
1291            exercise: &Exercise,
1292            score_given: Option<f32>,
1293            activity_progress: ActivityProgress,
1294            reviewing_stage: ReviewingStage,
1295        ) -> UserExerciseState {
1296            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1297            UserExerciseState {
1298                id,
1299                user_id: id,
1300                exercise_id: exercise.id,
1301                course_id: exercise.course_id,
1302                exam_id: exercise.exam_id,
1303                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1304                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1305                deleted_at: None,
1306                score_given,
1307                grading_progress: GradingProgress::NotReady,
1308                activity_progress,
1309                reviewing_stage,
1310                selected_exercise_slide_id: None,
1311            }
1312        }
1313    }
1314}