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