Skip to main content

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