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