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