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