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#[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 !input_data.exercise.needs_peer_review && !input_data.exercise.needs_self_review {
102 if slide_grading_progress == GradingProgress::NotReady {
103 return ActivityProgress::Initialized;
105 }
106 return ActivityProgress::Completed;
108 };
109 if new_reviewing_stage == &ReviewingStage::NotStarted {
111 if slide_grading_progress == GradingProgress::NotReady {
112 return ActivityProgress::Initialized;
114 }
115 return ActivityProgress::InProgress;
117 }
118 if new_reviewing_stage == &ReviewingStage::PeerReview
119 || new_reviewing_stage == &ReviewingStage::SelfReview
120 {
121 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 if let Some(teacher_grading_decision) = &input_data.latest_teacher_grading_decision {
135 return Some(teacher_grading_decision.score_given);
136 };
137
138 if let Some(course) = &input_data.course
141 && course.chapter_locking_enabled
142 && input_data.exercise.teacher_reviews_answer_after_locking
143 {
144 return None;
146 }
147 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 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 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 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 return None;
210 }
211
212 if input_data.current_user_exercise_state.reviewing_stage == ReviewingStage::NotStarted {
213 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 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 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 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 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 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 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 return Some(PeerOrSelfReviewOpinion {
281 score_given: None,
282 reviewing_stage: ReviewingStage::WaitingForManualGrading,
283 });
284 } else {
285 return Some(PeerOrSelfReviewOpinion {
287 score_given: None,
288 reviewing_stage: ReviewingStage::SelfReview,
289 });
290 }
291 }
292
293 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 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 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 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 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 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 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 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 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 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 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 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 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 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 create_peer_review_question_essay(e1_id, 0.25),
926 ],
927 &vec![
928 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 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 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 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 #[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}