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