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
447    mod derive_new_user_exercise_state {
448        use chrono::TimeZone;
449
450        use crate::{
451            exercises::Exercise,
452            library::user_exercise_state_updater::UserExerciseStateUpdateRequiredDataPeerReviewInformation,
453            peer_or_self_review_configs::PeerOrSelfReviewConfig,
454            peer_or_self_review_submissions::PeerOrSelfReviewSubmission,
455            peer_review_queue_entries::PeerReviewQueueEntry,
456            user_exercise_slide_states::UserExerciseSlideStateGradingSummary,
457            user_exercise_states::UserExerciseState,
458        };
459
460        use super::*;
461
462        #[test]
463        fn updates_state_for_normal_exercise() {
464            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
465            let exercise = create_exercise(CourseOrExamId::Course(id), false, false, false);
466            let user_exercise_state = create_user_exercise_state(
467                &exercise,
468                None,
469                ActivityProgress::Initialized,
470                ReviewingStage::NotStarted,
471            );
472            let new_user_exercise_state =
473                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
474                    exercise: exercise.clone(),
475                    current_user_exercise_state: user_exercise_state,
476                    peer_or_self_review_information: None,
477                    latest_teacher_grading_decision: None,
478                    user_exercise_slide_state_grading_summary:
479                        UserExerciseSlideStateGradingSummary {
480                            score_given: Some(1.0),
481                            grading_progress: GradingProgress::FullyGraded,
482                        },
483                    chapter: None,
484                    course: exercise
485                        .course_id
486                        .map(create_course)
487                        .or_else(|| Some(create_course(id))),
488                })
489                .unwrap();
490            assert_results(
491                &new_user_exercise_state,
492                Some(1.0),
493                ActivityProgress::Completed,
494                // Exercises that don't have peer review new leave the not started stage
495                ReviewingStage::NotStarted,
496            );
497        }
498
499        #[test]
500        fn gives_automatic_points_for_chapter_locking_course_when_teacher_review_is_skipped() {
501            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
502            let mut exercise = create_exercise(CourseOrExamId::Course(id), false, false, false);
503            exercise.teacher_reviews_answer_after_locking = false;
504            let user_exercise_state = create_user_exercise_state(
505                &exercise,
506                None,
507                ActivityProgress::Initialized,
508                ReviewingStage::NotStarted,
509            );
510            let mut course = create_course(id);
511            course.chapter_locking_enabled = true;
512            let new_user_exercise_state =
513                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
514                    exercise: exercise.clone(),
515                    current_user_exercise_state: user_exercise_state,
516                    peer_or_self_review_information: None,
517                    latest_teacher_grading_decision: None,
518                    user_exercise_slide_state_grading_summary:
519                        UserExerciseSlideStateGradingSummary {
520                            score_given: Some(1.0),
521                            grading_progress: GradingProgress::FullyGraded,
522                        },
523                    chapter: None,
524                    course: Some(course),
525                })
526                .unwrap();
527            assert_results(
528                &new_user_exercise_state,
529                Some(1.0),
530                ActivityProgress::Completed,
531                ReviewingStage::NotStarted,
532            );
533        }
534
535        #[test]
536        fn suppresses_automatic_points_for_chapter_locking_course_when_teacher_review_is_required()
537        {
538            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
539            let exercise = create_exercise(CourseOrExamId::Course(id), false, false, false);
540            let user_exercise_state = create_user_exercise_state(
541                &exercise,
542                None,
543                ActivityProgress::Initialized,
544                ReviewingStage::NotStarted,
545            );
546            let mut course = create_course(id);
547            course.chapter_locking_enabled = true;
548            let new_user_exercise_state =
549                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
550                    exercise: exercise.clone(),
551                    current_user_exercise_state: user_exercise_state,
552                    peer_or_self_review_information: None,
553                    latest_teacher_grading_decision: None,
554                    user_exercise_slide_state_grading_summary:
555                        UserExerciseSlideStateGradingSummary {
556                            score_given: Some(1.0),
557                            grading_progress: GradingProgress::FullyGraded,
558                        },
559                    chapter: None,
560                    course: Some(course),
561                })
562                .unwrap();
563            assert_results(
564                &new_user_exercise_state,
565                None,
566                ActivityProgress::Completed,
567                ReviewingStage::NotStarted,
568            );
569        }
570
571        #[test]
572        fn doesnt_update_score_for_exercise_that_needs_to_be_peer_reviewed() {
573            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
574            let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
575            let user_exercise_state = create_user_exercise_state(
576                &exercise,
577                None,
578                ActivityProgress::Initialized,
579                ReviewingStage::NotStarted,
580            );
581            let new_user_exercise_state =
582                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
583                    exercise: exercise.clone(),
584                    current_user_exercise_state: user_exercise_state,
585                    peer_or_self_review_information: Some(
586                        UserExerciseStateUpdateRequiredDataPeerReviewInformation {
587                            given_peer_or_self_review_submissions: Vec::new(),
588                            given_self_review_submission: None,
589                            latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: Vec::new(),
590                            peer_review_queue_entry: None,
591                            peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
592                            peer_or_self_review_questions: Vec::new(),
593                        },
594                    ),
595                    latest_teacher_grading_decision: None,
596                        user_exercise_slide_state_grading_summary:
597                            UserExerciseSlideStateGradingSummary {
598                                score_given: Some(1.0),
599                                grading_progress: GradingProgress::FullyGraded,
600                            },
601                        chapter: None,
602                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
603                    })
604                    .unwrap();
605            assert_results(
606                &new_user_exercise_state,
607                None,
608                ActivityProgress::InProgress,
609                ReviewingStage::NotStarted,
610            );
611        }
612
613        mod automatically_accept_or_reject_by_average {
614            use super::*;
615
616            #[test]
617            fn peer_review_automatically_accept_or_reject_by_average_works_gives_full_points() {
618                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
619                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
620                let user_exercise_state = create_user_exercise_state(
621                    &exercise,
622                    None,
623                    ActivityProgress::Initialized,
624                    ReviewingStage::PeerReview,
625                );
626                let new_user_exercise_state =
627                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
628                        exercise: exercise.clone(),
629                        current_user_exercise_state: user_exercise_state,
630                        peer_or_self_review_information: Some(
631                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
632                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
633                                given_self_review_submission: None,
634                                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)],
635                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
636                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
637                                peer_or_self_review_questions: Vec::new(),
638                            },
639                        ),
640                        latest_teacher_grading_decision: None,
641                        user_exercise_slide_state_grading_summary:
642                            UserExerciseSlideStateGradingSummary {
643                                score_given: Some(1.0),
644                                grading_progress: GradingProgress::FullyGraded,
645                            },
646                        chapter: None,
647                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
648                    })
649                    .unwrap();
650                assert_results(
651                    &new_user_exercise_state,
652                    // The user passed peer review, so they deserve full points from the exercise
653                    Some(9000.0),
654                    ActivityProgress::Completed,
655                    ReviewingStage::ReviewedAndLocked,
656                );
657            }
658
659            #[test]
660            fn peer_review_automatically_accept_or_reject_by_average_works_gives_zero_points() {
661                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
662                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
663                let user_exercise_state = create_user_exercise_state(
664                    &exercise,
665                    None,
666                    ActivityProgress::Initialized,
667                    ReviewingStage::PeerReview,
668                );
669                let new_user_exercise_state =
670                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
671                        exercise: exercise.clone(),
672                        current_user_exercise_state: user_exercise_state,
673                        peer_or_self_review_information: Some(
674                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
675                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
676                                given_self_review_submission: None,
677                                // Average below 2.1
678                                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)],
679                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
680                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
681                                peer_or_self_review_questions: Vec::new(),
682                            },
683                        ),
684                        latest_teacher_grading_decision: None,
685                        user_exercise_slide_state_grading_summary:
686                            UserExerciseSlideStateGradingSummary {
687                                score_given: Some(1.0),
688                                grading_progress: GradingProgress::FullyGraded,
689                            },
690                        chapter: None,
691                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
692                    })
693                    .unwrap();
694                assert_results(
695                    &new_user_exercise_state,
696                    // The user failed peer review, so they get zero points
697                    Some(0.0),
698                    ActivityProgress::Completed,
699                    ReviewingStage::ReviewedAndLocked,
700                );
701            }
702        }
703
704        mod automatically_accept_or_manual_review_by_average {
705            use super::*;
706
707            #[test]
708            fn peer_review_automatically_accept_or_manual_review_by_average_works_gives_full_points()
709             {
710                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
711                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
712                let user_exercise_state = create_user_exercise_state(
713                    &exercise,
714                    None,
715                    ActivityProgress::Initialized,
716                    ReviewingStage::PeerReview,
717                );
718                let new_user_exercise_state =
719                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
720                        exercise: exercise.clone(),
721                        current_user_exercise_state: user_exercise_state,
722                        peer_or_self_review_information: Some(
723                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
724                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
725                                given_self_review_submission: None,
726                                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)],
727                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
728                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage),
729                                peer_or_self_review_questions: Vec::new(),
730                            },
731                        ),
732                        latest_teacher_grading_decision: None,
733                        user_exercise_slide_state_grading_summary:
734                            UserExerciseSlideStateGradingSummary {
735                                score_given: Some(1.0),
736                                grading_progress: GradingProgress::FullyGraded,
737                            },
738                        chapter: None,
739                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
740                    })
741                    .unwrap();
742                assert_results(
743                    &new_user_exercise_state,
744                    // The user passed peer review, so they deserve full points from the exercise
745                    Some(9000.0),
746                    ActivityProgress::Completed,
747                    ReviewingStage::ReviewedAndLocked,
748                );
749            }
750
751            #[test]
752            fn peer_review_automatically_accept_or_manual_review_by_average_works_puts_the_answer_to_manual_review()
753             {
754                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
755                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
756                let user_exercise_state = create_user_exercise_state(
757                    &exercise,
758                    None,
759                    ActivityProgress::Initialized,
760                    ReviewingStage::PeerReview,
761                );
762                let new_user_exercise_state =
763                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
764                        exercise: exercise.clone(),
765                        current_user_exercise_state: user_exercise_state,
766                        peer_or_self_review_information: Some(
767                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
768                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
769                                given_self_review_submission: None,
770                                // Average below 2.1
771                                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)],
772                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
773                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage),
774                                peer_or_self_review_questions: Vec::new(),
775                            },
776                        ),
777                        latest_teacher_grading_decision: None,
778                        user_exercise_slide_state_grading_summary:
779                            UserExerciseSlideStateGradingSummary {
780                                score_given: Some(1.0),
781                                grading_progress: GradingProgress::FullyGraded,
782                            },
783                        chapter: None,
784                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
785                    })
786                    .unwrap();
787                assert_results(
788                    &new_user_exercise_state,
789                    // Manual review, we won't give any points because the points are up to the teacher's descision in the review
790                    None,
791                    ActivityProgress::Completed,
792                    ReviewingStage::WaitingForManualGrading,
793                );
794            }
795        }
796
797        mod manual_review_everything {
798            use super::*;
799
800            #[test]
801            fn peer_review_manual_review_everything_works_does_not_give_full_points_to_passing_answer_and_puts_to_manual_review()
802             {
803                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
804                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
805                let user_exercise_state = create_user_exercise_state(
806                    &exercise,
807                    None,
808                    ActivityProgress::Initialized,
809                    ReviewingStage::PeerReview,
810                );
811                let new_user_exercise_state =
812                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
813                        exercise: exercise.clone(),
814                        current_user_exercise_state: user_exercise_state,
815                        peer_or_self_review_information: Some(
816                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
817                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
818                                given_self_review_submission: None,
819                                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)],
820                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
821                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::ManualReviewEverything),
822                                peer_or_self_review_questions: Vec::new(),
823                            },
824                        ),
825                        latest_teacher_grading_decision: None,
826                        user_exercise_slide_state_grading_summary:
827                            UserExerciseSlideStateGradingSummary {
828                                score_given: Some(1.0),
829                                grading_progress: GradingProgress::FullyGraded,
830                            },
831                        chapter: None,
832                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
833                    })
834                    .unwrap();
835                assert_results(
836                    &new_user_exercise_state,
837                    // Score will be given from the manual review
838                    None,
839                    ActivityProgress::Completed,
840                    ReviewingStage::WaitingForManualGrading,
841                );
842            }
843
844            #[test]
845            fn peer_review_manual_review_everything_works_puts_failing_answer_the_answer_to_manual_review()
846             {
847                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
848                let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true);
849                let user_exercise_state = create_user_exercise_state(
850                    &exercise,
851                    None,
852                    ActivityProgress::Initialized,
853                    ReviewingStage::PeerReview,
854                );
855                let new_user_exercise_state =
856                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
857                        exercise: exercise.clone(),
858                        current_user_exercise_state: user_exercise_state,
859                        peer_or_self_review_information: Some(
860                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
861                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
862                                given_self_review_submission: None,
863                                // Average below 2.1
864                                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)],
865                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
866                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::ManualReviewEverything),
867                                peer_or_self_review_questions: Vec::new(),
868                            },
869                        ),
870                        latest_teacher_grading_decision: None,
871                        user_exercise_slide_state_grading_summary:
872                            UserExerciseSlideStateGradingSummary {
873                                score_given: Some(1.0),
874                                grading_progress: GradingProgress::FullyGraded,
875                            },
876                        chapter: None,
877                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
878                    })
879                    .unwrap();
880                assert_results(
881                    &new_user_exercise_state,
882                    // Score will be given from the manual review
883                    None,
884                    ActivityProgress::Completed,
885                    ReviewingStage::WaitingForManualGrading,
886                );
887            }
888        }
889
890        mod calculate_peer_review_weighted_points {
891            use uuid::Uuid;
892
893            use crate::library::user_exercise_state_updater::state_deriver::{
894                calculate_peer_review_weighted_points,
895                tests::derive_new_user_exercise_state::{
896                    create_peer_review_question_essay, create_peer_review_question_scale,
897                    create_peer_review_question_submission_with_ids,
898                },
899            };
900
901            #[test]
902            fn calculate_peer_review_weighted_points_works() {
903                let q1_id = Uuid::parse_str("d42ecbc9-34ff-4549-aacf-1b8ac6e672c2").unwrap();
904                let q2_id = Uuid::parse_str("1a018bb2-023f-4f58-b5f1-b09d58b42ed8").unwrap();
905                let q3_id = Uuid::parse_str("9ab2df96-60a4-40c2-a097-900654f44700").unwrap();
906                let q4_id = Uuid::parse_str("4bed2265-3c8f-4387-83e9-76e2b673eea3").unwrap();
907                let e1_id = Uuid::parse_str("fd4e5f7e-e794-4993-954e-fbd2d8b04d6b").unwrap();
908
909                let s1_id = Uuid::parse_str("2795b352-d5ef-41c7-92f7-a60d90c62c91").unwrap();
910                let s2_id = Uuid::parse_str("e5c16a89-2a3f-4910-9b00-dd981cedcbcc").unwrap();
911                let s3_id = Uuid::parse_str("462a6493-a506-42e6-869d-10220b2885b8").unwrap();
912
913                let res = calculate_peer_review_weighted_points(
914                    &vec![
915                        create_peer_review_question_scale(q1_id, 0.25),
916                        create_peer_review_question_scale(q2_id, 0.25),
917                        create_peer_review_question_scale(q3_id, 0.25),
918                        create_peer_review_question_scale(q4_id, 0.25),
919                        // Extra one to check that ignoring questions works
920                        create_peer_review_question_essay(e1_id, 0.25),
921                    ],
922                    &vec![
923                        // First student
924                        create_peer_review_question_submission_with_ids(5.0, q1_id, s1_id),
925                        create_peer_review_question_submission_with_ids(4.0, q2_id, s1_id),
926                        create_peer_review_question_submission_with_ids(5.0, q3_id, s1_id),
927                        create_peer_review_question_submission_with_ids(5.0, q4_id, s1_id),
928                        // Second student
929                        create_peer_review_question_submission_with_ids(4.0, q1_id, s2_id),
930                        create_peer_review_question_submission_with_ids(2.0, q2_id, s2_id),
931                        create_peer_review_question_submission_with_ids(3.0, q3_id, s2_id),
932                        create_peer_review_question_submission_with_ids(4.0, q4_id, s2_id),
933                        // Third student
934                        create_peer_review_question_submission_with_ids(3.0, q1_id, s3_id),
935                        create_peer_review_question_submission_with_ids(2.0, q2_id, s3_id),
936                        create_peer_review_question_submission_with_ids(4.0, q3_id, s3_id),
937                        create_peer_review_question_submission_with_ids(4.0, q4_id, s3_id),
938                        // Extra one to check that ignoring questions works
939                        create_peer_review_question_submission_with_ids(3.0, e1_id, s3_id),
940                    ],
941                    4,
942                );
943                assert_eq!(res, 3.0);
944            }
945        }
946
947        mod self_review {
948            use super::*;
949
950            #[test]
951            fn if_self_review_enabled_does_not_put_answer_automatically_to_self_review() {
952                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
953                let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true);
954                let user_exercise_state = create_user_exercise_state(
955                    &exercise,
956                    None,
957                    ActivityProgress::Initialized,
958                    ReviewingStage::NotStarted,
959                );
960                let new_user_exercise_state =
961                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
962                        exercise: exercise.clone(),
963                        current_user_exercise_state: user_exercise_state,
964                        peer_or_self_review_information: Some(
965                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
966                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
967                                given_self_review_submission: None,
968                                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)],
969                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
970                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
971                                peer_or_self_review_questions: Vec::new(),
972                            },
973                        ),
974                        latest_teacher_grading_decision: None,
975                        user_exercise_slide_state_grading_summary:
976                            UserExerciseSlideStateGradingSummary {
977                                score_given: Some(1.0),
978                                grading_progress: GradingProgress::FullyGraded,
979                            },
980                        chapter: None,
981                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
982                    })
983                    .unwrap();
984                assert_results(
985                    &new_user_exercise_state,
986                    None,
987                    ActivityProgress::InProgress,
988                    ReviewingStage::NotStarted,
989                );
990            }
991
992            #[test]
993            fn if_peer_and_self_review_enabled_self_review_comes_after_peer_review() {
994                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
995                let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true);
996                let user_exercise_state = create_user_exercise_state(
997                    &exercise,
998                    None,
999                    ActivityProgress::Initialized,
1000                    ReviewingStage::PeerReview,
1001                );
1002                let new_user_exercise_state =
1003                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
1004                        exercise: exercise.clone(),
1005                        current_user_exercise_state: user_exercise_state,
1006                        peer_or_self_review_information: Some(
1007                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
1008                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
1009                                given_self_review_submission: None,
1010                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![],
1011                                peer_review_queue_entry: Some(create_peer_review_queue_entry(false)),
1012                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
1013                                peer_or_self_review_questions: Vec::new(),
1014                            },
1015                        ),
1016                        latest_teacher_grading_decision: None,
1017                        user_exercise_slide_state_grading_summary:
1018                            UserExerciseSlideStateGradingSummary {
1019                                score_given: Some(1.0),
1020                                grading_progress: GradingProgress::FullyGraded,
1021                            },
1022                        chapter: None,
1023                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
1024                    })
1025                    .unwrap();
1026                assert_results(
1027                    &new_user_exercise_state,
1028                    None,
1029                    ActivityProgress::InProgress,
1030                    ReviewingStage::SelfReview,
1031                );
1032            }
1033
1034            #[test]
1035            fn moves_out_of_self_review() {
1036                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1037                let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true);
1038                let user_exercise_state = create_user_exercise_state(
1039                    &exercise,
1040                    None,
1041                    ActivityProgress::Initialized,
1042                    ReviewingStage::SelfReview,
1043                );
1044                let new_user_exercise_state =
1045                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
1046                        exercise: exercise.clone(),
1047                        current_user_exercise_state: user_exercise_state,
1048                        peer_or_self_review_information: Some(
1049                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
1050                                given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()],
1051                                given_self_review_submission: Some(create_peer_review_submission()),
1052                                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)],
1053                                peer_review_queue_entry: Some(create_peer_review_queue_entry(true)),
1054                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
1055                                peer_or_self_review_questions: Vec::new(),
1056                            },
1057                        ),
1058                        latest_teacher_grading_decision: None,
1059                        user_exercise_slide_state_grading_summary:
1060                            UserExerciseSlideStateGradingSummary {
1061                                score_given: Some(1.0),
1062                                grading_progress: GradingProgress::FullyGraded,
1063                            },
1064                        chapter: None,
1065                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
1066                    })
1067                    .unwrap();
1068                assert_results(
1069                    &new_user_exercise_state,
1070                    Some(9000.0),
1071                    ActivityProgress::Completed,
1072                    ReviewingStage::ReviewedAndLocked,
1073                );
1074            }
1075
1076            // User has to start the self review themselves by clicking a button.
1077            #[test]
1078            fn does_not_move_to_self_review_if_self_review_but_no_peer_review() {
1079                let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1080                let exercise = create_exercise(CourseOrExamId::Course(id), false, true, true);
1081                let user_exercise_state = create_user_exercise_state(
1082                    &exercise,
1083                    None,
1084                    ActivityProgress::Initialized,
1085                    ReviewingStage::NotStarted,
1086                );
1087                let new_user_exercise_state =
1088                    derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
1089                        exercise: exercise.clone(),
1090                        current_user_exercise_state: user_exercise_state,
1091                        peer_or_self_review_information: Some(
1092                            UserExerciseStateUpdateRequiredDataPeerReviewInformation {
1093                                given_peer_or_self_review_submissions: vec![],
1094                                given_self_review_submission: None,
1095                                latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![],
1096                                peer_review_queue_entry: None,
1097                                peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
1098                                peer_or_self_review_questions: Vec::new(),
1099                            },
1100                        ),
1101                        latest_teacher_grading_decision: None,
1102                        user_exercise_slide_state_grading_summary:
1103                            UserExerciseSlideStateGradingSummary {
1104                                score_given: Some(1.0),
1105                                grading_progress: GradingProgress::FullyGraded,
1106                            },
1107                        chapter: None,
1108                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
1109                    })
1110                    .unwrap();
1111                assert_results(
1112                    &new_user_exercise_state,
1113                    None,
1114                    ActivityProgress::InProgress,
1115                    ReviewingStage::NotStarted,
1116                );
1117            }
1118        }
1119
1120        #[test]
1121        fn moves_out_of_self_review_if_self_review_but_no_peer_review() {
1122            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1123            let exercise = create_exercise(CourseOrExamId::Course(id), false, true, true);
1124            let user_exercise_state = create_user_exercise_state(
1125                &exercise,
1126                None,
1127                ActivityProgress::Initialized,
1128                ReviewingStage::SelfReview,
1129            );
1130            let new_user_exercise_state =
1131                derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData {
1132                    exercise: exercise.clone(),
1133                    current_user_exercise_state: user_exercise_state,
1134                    peer_or_self_review_information: Some(
1135                        UserExerciseStateUpdateRequiredDataPeerReviewInformation {
1136                            given_peer_or_self_review_submissions: vec![],
1137                            given_self_review_submission: Some(create_peer_review_submission()),
1138                            latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![],
1139                            peer_review_queue_entry: None,
1140                            peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage),
1141                            peer_or_self_review_questions: Vec::new(),
1142                        },
1143                    ),
1144                    latest_teacher_grading_decision: None,
1145                        user_exercise_slide_state_grading_summary:
1146                            UserExerciseSlideStateGradingSummary {
1147                                score_given: Some(1.0),
1148                                grading_progress: GradingProgress::FullyGraded,
1149                            },
1150                        chapter: None,
1151                        course: exercise.course_id.map(create_course).or_else(|| Some(create_course(id))),
1152                    })
1153                    .unwrap();
1154            assert_results(
1155                &new_user_exercise_state,
1156                None,
1157                ActivityProgress::Completed,
1158                ReviewingStage::WaitingForManualGrading,
1159            );
1160        }
1161
1162        fn assert_results(
1163            update: &UserExerciseStateUpdate,
1164            score_given: Option<f32>,
1165            activity_progress: ActivityProgress,
1166            reviewing_stage: ReviewingStage,
1167        ) {
1168            assert_eq!(update.score_given, score_given);
1169            assert_eq!(update.activity_progress, activity_progress);
1170            assert_eq!(update.reviewing_stage, reviewing_stage);
1171        }
1172
1173        fn create_exercise(
1174            course_or_exam_id: CourseOrExamId,
1175            needs_peer_review: bool,
1176            needs_self_review: bool,
1177            use_course_default_peer_or_self_review_config: bool,
1178        ) -> Exercise {
1179            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1180            let (course_id, exam_id) = course_or_exam_id.to_course_and_exam_ids();
1181            Exercise {
1182                id,
1183                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1184                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1185                name: "".to_string(),
1186                course_id,
1187                exam_id,
1188                page_id: id,
1189                chapter_id: None,
1190                deadline: None,
1191                deleted_at: None,
1192                score_maximum: 9000,
1193                order_number: 0,
1194                copied_from: None,
1195                max_tries_per_slide: None,
1196                limit_number_of_tries: false,
1197                needs_peer_review,
1198                use_course_default_peer_or_self_review_config,
1199                exercise_language_group_id: None,
1200                needs_self_review,
1201                teacher_reviews_answer_after_locking: true,
1202            }
1203        }
1204
1205        fn create_course(course_id: Uuid) -> Course {
1206            Course {
1207                id: course_id,
1208                slug: "test-course".to_string(),
1209                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1210                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1211                name: "Test Course".to_string(),
1212                description: None,
1213                organization_id: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(),
1214                deleted_at: None,
1215                language_code: "en".to_string(),
1216                copied_from: None,
1217                content_search_language: None,
1218                course_language_group_id: Uuid::parse_str("00000000-0000-0000-0000-000000000001")
1219                    .unwrap(),
1220                is_draft: false,
1221                is_test_mode: false,
1222                is_unlisted: false,
1223                base_module_completion_requires_n_submodule_completions: 0,
1224                can_add_chatbot: false,
1225                is_joinable_by_code_only: false,
1226                join_code: None,
1227                ask_marketing_consent: false,
1228                flagged_answers_threshold: None,
1229                flagged_answers_skip_manual_review_and_allow_retry: false,
1230                closed_at: None,
1231                closed_additional_message: None,
1232                closed_course_successor_id: None,
1233                chapter_locking_enabled: false,
1234            }
1235        }
1236
1237        fn create_peer_or_self_review_config(
1238            processing_strategy: PeerReviewProcessingStrategy,
1239        ) -> PeerOrSelfReviewConfig {
1240            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1241            PeerOrSelfReviewConfig {
1242                id,
1243                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1244                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1245                deleted_at: None,
1246                course_id: id,
1247                exercise_id: None,
1248                peer_reviews_to_give: 3,
1249                peer_reviews_to_receive: 2,
1250                accepting_threshold: 2.1,
1251                processing_strategy,
1252                reset_answer_if_zero_points_from_review: false,
1253                manual_review_cutoff_in_days: 21,
1254                points_are_all_or_nothing: true,
1255                review_instructions: None,
1256            }
1257        }
1258
1259        fn create_peer_review_question_submission(
1260            number_data: f32,
1261        ) -> PeerOrSelfReviewQuestionSubmission {
1262            PeerOrSelfReviewQuestionSubmission {
1263                id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a").unwrap(),
1264                created_at: Utc::now(),
1265                updated_at: Utc::now(),
1266                deleted_at: None,
1267                peer_or_self_review_question_id: Uuid::parse_str(
1268                    "b853bbd7-feee-4447-ab14-c9622e565ea1",
1269                )
1270                .unwrap(),
1271                peer_or_self_review_submission_id: Uuid::parse_str(
1272                    "be4061b5-b468-4f50-93b0-cf3bf9de9a13",
1273                )
1274                .unwrap(),
1275                text_data: None,
1276                number_data: Some(number_data),
1277            }
1278        }
1279
1280        fn create_peer_review_question_submission_with_ids(
1281            number_data: f32,
1282            peer_or_self_review_question_id: Uuid,
1283            peer_or_self_review_submission_id: Uuid,
1284        ) -> PeerOrSelfReviewQuestionSubmission {
1285            PeerOrSelfReviewQuestionSubmission {
1286                id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a").unwrap(),
1287                created_at: Utc::now(),
1288                updated_at: Utc::now(),
1289                deleted_at: None,
1290                peer_or_self_review_question_id,
1291                peer_or_self_review_submission_id,
1292                text_data: None,
1293                number_data: Some(number_data),
1294            }
1295        }
1296
1297        fn create_peer_review_question_scale(id: Uuid, weight: f32) -> PeerOrSelfReviewQuestion {
1298            PeerOrSelfReviewQuestion {
1299                id,
1300                weight,
1301                created_at: Utc::now(),
1302                updated_at: Utc::now(),
1303                deleted_at: None,
1304                peer_or_self_review_config_id: Uuid::parse_str(
1305                    "bf923ea4-a637-4d97-b78b-6f843d76120a",
1306                )
1307                .unwrap(),
1308                order_number: 1,
1309                question: "A question".to_string(),
1310                question_type: PeerOrSelfReviewQuestionType::Scale,
1311                answer_required: true,
1312            }
1313        }
1314
1315        fn create_peer_review_question_essay(id: Uuid, weight: f32) -> PeerOrSelfReviewQuestion {
1316            PeerOrSelfReviewQuestion {
1317                id,
1318                weight,
1319                created_at: Utc::now(),
1320                updated_at: Utc::now(),
1321                deleted_at: None,
1322                peer_or_self_review_config_id: Uuid::parse_str(
1323                    "bf923ea4-a637-4d97-b78b-6f843d76120a",
1324                )
1325                .unwrap(),
1326                order_number: 1,
1327                question: "A question".to_string(),
1328                question_type: PeerOrSelfReviewQuestionType::Essay,
1329                answer_required: true,
1330            }
1331        }
1332
1333        fn create_peer_review_submission() -> PeerOrSelfReviewSubmission {
1334            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1335            PeerOrSelfReviewSubmission {
1336                id,
1337                created_at: Utc::now(),
1338                updated_at: Utc::now(),
1339                deleted_at: None,
1340                user_id: id,
1341                exercise_id: id,
1342                course_id: id,
1343                peer_or_self_review_config_id: id,
1344                exercise_slide_submission_id: id,
1345            }
1346        }
1347
1348        fn create_peer_review_queue_entry(
1349            received_enough_peer_reviews: bool,
1350        ) -> PeerReviewQueueEntry {
1351            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1352            PeerReviewQueueEntry {
1353                id,
1354                created_at: Utc::now(),
1355                updated_at: Utc::now(),
1356                deleted_at: None,
1357                user_id: id,
1358                exercise_id: id,
1359                course_id: id,
1360                receiving_peer_reviews_exercise_slide_submission_id: id,
1361                received_enough_peer_reviews,
1362                peer_review_priority: 100,
1363                removed_from_queue_for_unusual_reason: false,
1364            }
1365        }
1366
1367        fn create_user_exercise_state(
1368            exercise: &Exercise,
1369            score_given: Option<f32>,
1370            activity_progress: ActivityProgress,
1371            reviewing_stage: ReviewingStage,
1372        ) -> UserExerciseState {
1373            let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
1374            UserExerciseState {
1375                id,
1376                user_id: id,
1377                exercise_id: exercise.id,
1378                course_id: exercise.course_id,
1379                exam_id: exercise.exam_id,
1380                created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1381                updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
1382                deleted_at: None,
1383                score_given,
1384                grading_progress: GradingProgress::NotReady,
1385                activity_progress,
1386                reviewing_stage,
1387                selected_exercise_slide_id: None,
1388            }
1389        }
1390    }
1391}