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