1use std::collections::HashMap;
2
3use chrono::Duration;
4use futures::future::BoxFuture;
5use rand::{rng, seq::SliceRandom};
6use url::Url;
7
8use crate::{
9 exercise_service_info::ExerciseServiceInfoApi,
10 exercise_slide_submissions::{self, ExerciseSlideSubmission},
11 exercise_task_submissions,
12 exercise_tasks::CourseMaterialExerciseTask,
13 exercises::Exercise,
14 peer_or_self_review_configs::{self, PeerOrSelfReviewConfig},
15 peer_or_self_review_question_submissions,
16 peer_or_self_review_questions::{self, PeerOrSelfReviewQuestion},
17 peer_or_self_review_submissions,
18 peer_review_queue_entries::{self, PeerReviewQueueEntry},
19 prelude::*,
20 user_exercise_states::{self, ReviewingStage, UserExerciseState},
21};
22
23use super::user_exercise_state_updater::{
24 self, UserExerciseStateUpdateAlreadyLoadedRequiredData,
25 UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation,
26};
27
28const MAX_PEER_REVIEW_CANDIDATES: i64 = 10;
29
30pub async fn start_peer_or_self_review_for_user(
32 conn: &mut PgConnection,
33 user_exercise_state: UserExerciseState,
34 exercise: &Exercise,
35) -> ModelResult<()> {
36 if user_exercise_state.reviewing_stage != ReviewingStage::NotStarted {
37 return Err(ModelError::new(
38 ModelErrorType::PreconditionFailed,
39 "Cannot start peer or self review anymore.".to_string(),
40 None,
41 ));
42 }
43 if !exercise.needs_peer_review && !exercise.needs_self_review {
44 return Err(ModelError::new(
45 ModelErrorType::PreconditionFailed,
46 "Exercise does not need peer or self review.".to_string(),
47 None,
48 ));
49 }
50 let new_reviewing_stage = if exercise.needs_peer_review {
51 ReviewingStage::PeerReview
52 } else {
53 ReviewingStage::SelfReview
54 };
55
56 let _user_exercise_state = user_exercise_states::update_exercise_progress(
57 conn,
58 user_exercise_state.id,
59 new_reviewing_stage,
60 )
61 .await?;
62 Ok(())
63}
64
65#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
66#[cfg_attr(feature = "ts_rs", derive(TS))]
67pub struct CourseMaterialPeerOrSelfReviewSubmission {
68 pub exercise_slide_submission_id: Uuid,
69 pub peer_or_self_review_config_id: Uuid,
70 pub peer_review_question_answers: Vec<CourseMaterialPeerOrSelfReviewQuestionAnswer>,
71 pub token: String,
72}
73
74#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
75#[cfg_attr(feature = "ts_rs", derive(TS))]
76pub struct CourseMaterialPeerOrSelfReviewQuestionAnswer {
77 pub peer_or_self_review_question_id: Uuid,
78 pub text_data: Option<String>,
79 pub number_data: Option<f32>,
80}
81
82pub async fn create_peer_or_self_review_submission_for_user(
83 conn: &mut PgConnection,
84 exercise: &Exercise,
85 giver_exercise_state: UserExerciseState,
86 receiver_exercise_state: UserExerciseState,
87 peer_review_submission: CourseMaterialPeerOrSelfReviewSubmission,
88) -> ModelResult<UserExerciseState> {
89 let is_self_review = giver_exercise_state.user_id == receiver_exercise_state.user_id;
90
91 if is_self_review
92 && (!exercise.needs_self_review
93 || giver_exercise_state.reviewing_stage != ReviewingStage::SelfReview)
94 {
95 return Err(ModelError::new(
96 ModelErrorType::PreconditionFailed,
97 "Self review not allowed.".to_string(),
98 None,
99 ));
100 }
101 if !is_self_review
102 && (!exercise.needs_peer_review
103 || giver_exercise_state.reviewing_stage == ReviewingStage::NotStarted)
104 {
105 return Err(ModelError::new(
106 ModelErrorType::PreconditionFailed,
107 "Peer review not allowed.".to_string(),
108 None,
109 ));
110 }
111
112 let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id(
113 conn,
114 exercise,
115 exercise.get_course_id()?,
116 )
117 .await?;
118 let sanitized_answers = validate_and_sanitize_peer_review_submission_answers(
119 peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id_as_map(
120 conn,
121 peer_or_self_review_config.id,
122 )
123 .await?,
124 peer_review_submission.peer_review_question_answers,
125 )?;
126
127 let mut tx = conn.begin().await?;
128
129 let peer_reviews_given_before_this_review: i32 =
130 peer_or_self_review_submissions::get_users_submission_count_for_exercise_and_course_instance(
131 &mut tx,
132 giver_exercise_state.user_id,
133 giver_exercise_state.exercise_id,
134 giver_exercise_state.get_course_id()?,
135 )
136 .await?
137 .try_into()?;
138 let peer_reviews_given = peer_reviews_given_before_this_review + 1;
139
140 if !is_self_review {
141 let unacceptable_amount_of_peer_reviews =
142 std::cmp::max(peer_or_self_review_config.peer_reviews_to_give, 1) * 15;
143 let suspicious_amount_of_peer_reviews = std::cmp::max(
144 std::cmp::max(peer_or_self_review_config.peer_reviews_to_give, 1) * 2,
145 4,
146 );
147 if peer_reviews_given > unacceptable_amount_of_peer_reviews {
149 return Err(ModelError::new(
150 ModelErrorType::PreconditionFailed,
151 "You have given too many peer reviews to this exercise".to_string(),
152 None,
153 ));
154 }
155 if peer_reviews_given > suspicious_amount_of_peer_reviews {
157 let last_submission_time =
159 peer_or_self_review_submissions::get_last_time_user_submitted_peer_review(
160 &mut tx,
161 giver_exercise_state.user_id,
162 giver_exercise_state.exercise_id,
163 giver_exercise_state.get_course_id()?,
164 )
165 .await?;
166
167 if let Some(last_submission_time) = last_submission_time {
168 let diff = peer_reviews_given - suspicious_amount_of_peer_reviews;
169 let coefficient = diff.clamp(1, 10);
170 if Utc::now() - Duration::seconds(30 * coefficient as i64) < last_submission_time {
172 return Err(ModelError::new(
173 ModelErrorType::InvalidRequest,
174 "You are submitting too fast. Try again later.".to_string(),
175 None,
176 ));
177 }
178 }
179 }
180 }
181 let peer_or_self_review_submission_id = peer_or_self_review_submissions::insert(
182 &mut tx,
183 PKeyPolicy::Generate,
184 giver_exercise_state.user_id,
185 giver_exercise_state.exercise_id,
186 giver_exercise_state.get_course_id()?,
187 peer_or_self_review_config.id,
188 peer_review_submission.exercise_slide_submission_id,
189 )
190 .await?;
191 for answer in sanitized_answers {
192 peer_or_self_review_question_submissions::insert(
193 &mut tx,
194 PKeyPolicy::Generate,
195 answer.peer_or_self_review_question_id,
196 peer_or_self_review_submission_id,
197 answer.text_data,
198 answer.number_data,
199 )
200 .await?;
201 }
202
203 if !is_self_review && peer_reviews_given >= peer_or_self_review_config.peer_reviews_to_give {
204 let users_latest_submission =
206 exercise_slide_submissions::get_users_latest_exercise_slide_submission(
207 &mut tx,
208 giver_exercise_state.get_selected_exercise_slide_id()?,
209 giver_exercise_state.user_id,
210 )
211 .await?;
212 let peer_reviews_received: i32 =
213 peer_or_self_review_submissions::count_peer_or_self_review_submissions_for_exercise_slide_submission(
214 &mut tx,
215 users_latest_submission.id,
216 &[giver_exercise_state.user_id],
217 )
218 .await?
219 .try_into()?;
220 let _peer_review_queue_entry = peer_review_queue_entries::upsert_peer_review_priority(
221 &mut tx,
222 giver_exercise_state.user_id,
223 giver_exercise_state.exercise_id,
224 giver_exercise_state.get_course_id()?,
225 peer_reviews_given,
226 users_latest_submission.id,
227 peer_reviews_received >= peer_or_self_review_config.peer_reviews_to_receive,
228 )
229 .await?;
230 }
231
232 let giver_exercise_state =
233 user_exercise_state_updater::update_user_exercise_state(&mut tx, giver_exercise_state.id)
234 .await?;
235
236 let exercise_slide_submission = exercise_slide_submissions::get_by_id(
237 &mut tx,
238 peer_review_submission.exercise_slide_submission_id,
239 )
240 .await?;
241 let receiver_peer_review_queue_entry =
242 peer_review_queue_entries::get_by_receiving_peer_reviews_exercise_slide_submission_id(
243 &mut tx,
244 exercise_slide_submission.id,
245 )
246 .await
247 .optional()?;
248 if let Some(entry) = receiver_peer_review_queue_entry {
249 if entry.user_id != giver_exercise_state.user_id {
251 update_peer_review_receiver_exercise_status(
252 &mut tx,
253 exercise,
254 &peer_or_self_review_config,
255 entry,
256 )
257 .await?;
258 }
259 }
260 crate::offered_answers_to_peer_review_temporary::delete_saved_submissions_for_user(
262 &mut tx,
263 exercise.id,
264 giver_exercise_state.user_id,
265 )
266 .await?;
267 tx.commit().await?;
268
269 Ok(giver_exercise_state)
270}
271
272fn validate_and_sanitize_peer_review_submission_answers(
274 peer_or_self_review_questions: HashMap<Uuid, PeerOrSelfReviewQuestion>,
275 peer_review_submission_question_answers: Vec<CourseMaterialPeerOrSelfReviewQuestionAnswer>,
276) -> ModelResult<Vec<CourseMaterialPeerOrSelfReviewQuestionAnswer>> {
277 let valid_peer_review_question_answers: Vec<_> = peer_review_submission_question_answers
279 .into_iter()
280 .filter(|answer| {
281 peer_or_self_review_questions.contains_key(&answer.peer_or_self_review_question_id)
282 })
283 .collect();
284
285 let answered_question_ids: std::collections::HashSet<_> = valid_peer_review_question_answers
287 .iter()
288 .map(|answer| answer.peer_or_self_review_question_id)
289 .collect();
290
291 let has_unanswered_required_questions = peer_or_self_review_questions
293 .iter()
294 .any(|(id, question)| question.answer_required && !answered_question_ids.contains(id));
295
296 if has_unanswered_required_questions {
297 Err(ModelError::new(
298 ModelErrorType::PreconditionFailed,
299 "All required questions need to be answered.".to_string(),
300 None,
301 ))
302 } else {
303 Ok(valid_peer_review_question_answers)
305 }
306}
307
308async fn update_peer_review_receiver_exercise_status(
309 conn: &mut PgConnection,
310 exercise: &Exercise,
311 peer_review: &PeerOrSelfReviewConfig,
312 peer_review_queue_entry: PeerReviewQueueEntry,
313) -> ModelResult<()> {
314 let peer_reviews_received =
315 peer_or_self_review_submissions::count_peer_or_self_review_submissions_for_exercise_slide_submission(
316 conn,
317 peer_review_queue_entry.receiving_peer_reviews_exercise_slide_submission_id,
318 &[peer_review_queue_entry.user_id],
319 )
320 .await?;
321 if peer_reviews_received >= peer_review.peer_reviews_to_receive.try_into()? {
322 let peer_review_queue_entry =
324 peer_review_queue_entries::update_received_enough_peer_reviews(
325 conn,
326 peer_review_queue_entry.id,
327 true,
328 )
329 .await?;
330 let user_exercise_state = user_exercise_states::get_user_exercise_state_if_exists(
331 conn,
332 peer_review_queue_entry.user_id,
333 peer_review_queue_entry.exercise_id,
334 CourseOrExamId::Course(peer_review_queue_entry.course_id),
335 )
336 .await?;
337 if let Some(user_exercise_state) = user_exercise_state {
338 let received_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_received_question_submissions_for_exercise_slide_submission(conn, peer_review_queue_entry.receiving_peer_reviews_exercise_slide_submission_id).await?;
339 let _updated_user_exercise_state =
340 user_exercise_state_updater::update_user_exercise_state_with_some_already_loaded_data(
341 conn,
342 user_exercise_state.id,
343 UserExerciseStateUpdateAlreadyLoadedRequiredData {
344 current_user_exercise_state: Some(user_exercise_state),
345 exercise: Some(exercise.clone()),
346 peer_or_self_review_information: Some(UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation {
347 peer_review_queue_entry: Some(Some(peer_review_queue_entry)),
348 latest_exercise_slide_submission_received_peer_or_self_review_question_submissions:
349 Some(received_peer_or_self_review_question_submissions),
350 ..Default::default()
351 }),
352 ..Default::default()
353 },
354 )
355 .await?;
356 }
357 }
358 Ok(())
359}
360
361#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
362#[cfg_attr(feature = "ts_rs", derive(TS))]
363pub struct CourseMaterialPeerOrSelfReviewData {
364 pub answer_to_review: Option<CourseMaterialPeerOrSelfReviewDataAnswerToReview>,
366 pub peer_or_self_review_config: PeerOrSelfReviewConfig,
367 pub peer_or_self_review_questions: Vec<PeerOrSelfReviewQuestion>,
368 #[cfg_attr(feature = "ts_rs", ts(type = "number"))]
369 pub num_peer_reviews_given: i64,
370}
371
372#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
373#[cfg_attr(feature = "ts_rs", derive(TS))]
374pub struct CourseMaterialPeerOrSelfReviewDataAnswerToReview {
375 pub exercise_slide_submission_id: Uuid,
376 pub course_material_exercise_tasks: Vec<CourseMaterialExerciseTask>,
378}
379
380pub async fn try_to_select_exercise_slide_submission_for_peer_review(
387 conn: &mut PgConnection,
388 exercise: &Exercise,
389 reviewer_user_exercise_state: &UserExerciseState,
390 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
391) -> ModelResult<CourseMaterialPeerOrSelfReviewData> {
392 let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id(
393 conn,
394 exercise,
395 exercise.get_course_id()?,
396 )
397 .await?;
398
399 let course_id = exercise.get_course_id()?;
400
401 if let Some(saved_exercise_slide_submission_to_review) = crate::offered_answers_to_peer_review_temporary::try_to_restore_previously_given_exercise_slide_submission(&mut *conn, exercise.id, reviewer_user_exercise_state.user_id, course_id).await? {
403 let data = get_course_material_peer_or_self_review_data(
404 conn,
405 &peer_or_self_review_config,
406 &Some(saved_exercise_slide_submission_to_review),
407 reviewer_user_exercise_state.user_id,
408 exercise.id,
409 fetch_service_info,
410 )
411 .await?;
412
413 return Ok(data)
414 }
415
416 let mut excluded_exercise_slide_submission_ids =
417 peer_or_self_review_submissions::get_users_submission_ids_for_exercise_and_course_instance(
418 conn,
419 reviewer_user_exercise_state.user_id,
420 reviewer_user_exercise_state.exercise_id,
421 course_id,
422 )
423 .await?;
424 let reported_submissions =
425 crate::flagged_answers::get_flagged_answers_submission_ids_by_flaggers_id(
426 conn,
427 reviewer_user_exercise_state.user_id,
428 )
429 .await?;
430 excluded_exercise_slide_submission_ids.extend(reported_submissions);
431
432 let candidate_submission_id = try_to_select_peer_review_candidate_from_queue(
433 conn,
434 reviewer_user_exercise_state.exercise_id,
435 reviewer_user_exercise_state.user_id,
436 &excluded_exercise_slide_submission_ids,
437 )
438 .await?;
439 let exercise_slide_submission_to_review = match candidate_submission_id {
440 Some(exercise_slide_submission) => {
441 crate::offered_answers_to_peer_review_temporary::save_given_exercise_slide_submission(
442 &mut *conn,
443 exercise_slide_submission.id,
444 exercise.id,
445 reviewer_user_exercise_state.user_id,
446 course_id,
447 )
448 .await?;
449 Some(exercise_slide_submission)
450 }
451 None => {
452 exercise_slide_submissions::try_to_get_random_filtered_by_user_and_submissions(
455 conn,
456 reviewer_user_exercise_state.exercise_id,
457 reviewer_user_exercise_state.user_id,
458 &excluded_exercise_slide_submission_ids,
459 )
460 .await?
461 }
462 };
463 let data = get_course_material_peer_or_self_review_data(
464 conn,
465 &peer_or_self_review_config,
466 &exercise_slide_submission_to_review,
467 reviewer_user_exercise_state.user_id,
468 exercise.id,
469 fetch_service_info,
470 )
471 .await?;
472
473 Ok(data)
474}
475
476pub async fn select_own_submission_for_self_review(
478 conn: &mut PgConnection,
479 exercise: &Exercise,
480 reviewer_user_exercise_state: &UserExerciseState,
481 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
482) -> ModelResult<CourseMaterialPeerOrSelfReviewData> {
483 let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id(
484 conn,
485 exercise,
486 exercise.get_course_id()?,
487 )
488 .await?;
489 let exercise_slide_submission =
490 exercise_slide_submissions::get_users_latest_exercise_slide_submission(
491 conn,
492 reviewer_user_exercise_state.get_selected_exercise_slide_id()?,
493 reviewer_user_exercise_state.user_id,
494 )
495 .await?;
496 let data = get_course_material_peer_or_self_review_data(
497 conn,
498 &peer_or_self_review_config,
499 &Some(exercise_slide_submission),
500 reviewer_user_exercise_state.user_id,
501 exercise.id,
502 fetch_service_info,
503 )
504 .await?;
505
506 Ok(data)
507}
508
509async fn try_to_select_peer_review_candidate_from_queue(
510 conn: &mut PgConnection,
511 exercise_id: Uuid,
512 excluded_user_id: Uuid,
513 excluded_exercise_slide_submission_ids: &[Uuid],
514) -> ModelResult<Option<ExerciseSlideSubmission>> {
515 const MAX_ATTEMPTS: u32 = 10;
516 let mut attempts = 0;
517
518 while attempts < MAX_ATTEMPTS {
520 attempts += 1;
521 let maybe_submission = try_to_select_peer_review_candidate_from_queue_impl(
522 conn,
523 exercise_id,
524 excluded_user_id,
525 excluded_exercise_slide_submission_ids,
526 )
527 .await?;
528
529 if let Some((ess_id, selected_submission_needs_peer_review)) = maybe_submission {
530 if excluded_exercise_slide_submission_ids.contains(&ess_id) {
531 warn!(exercise_slide_submission_id = %ess_id, "Selected exercise slide submission that should have been excluded from the selection process. Trying again.");
532 continue;
533 }
534
535 let ess = exercise_slide_submissions::get_by_id(conn, ess_id)
536 .await
537 .optional()?;
538 if let Some(ess) = ess {
539 if ess.course_id.is_none() {
541 warn!(exercise_slide_submission_id = %ess_id, "Selected exercise slide submission that doesn't have a course_id. Skipping it.");
542 continue;
543 };
544 if ess.deleted_at.is_none() {
545 let peer_review_queue_entry = peer_review_queue_entries::get_by_receiving_peer_reviews_exercise_slide_submission_id(conn, ess_id).await?;
547 if !selected_submission_needs_peer_review {
549 return Ok(Some(ess));
550 }
551 if peer_review_queue_entry.deleted_at.is_none()
552 && !peer_review_queue_entry.removed_from_queue_for_unusual_reason
553 {
554 return Ok(Some(ess));
555 } else {
556 if attempts == MAX_ATTEMPTS {
557 warn!(exercise_slide_submission_id = %ess_id, deleted_at = ?peer_review_queue_entry.deleted_at, removed_from_queue = %peer_review_queue_entry.removed_from_queue_for_unusual_reason, "Max attempts reached, returning submission despite being removed from queue");
558 return Ok(Some(ess));
559 }
560 warn!(exercise_slide_submission_id = %ess_id, deleted_at = ?peer_review_queue_entry.deleted_at, removed_from_queue = %peer_review_queue_entry.removed_from_queue_for_unusual_reason, "Selected exercise slide submission that was removed from the peer review queue. Trying again.");
561 continue;
562 }
563 }
564 } else {
565 warn!(exercise_slide_submission_id = %ess_id, "Selected exercise slide submission that was deleted. The peer review queue entry should've been deleted too! Deleting it now.");
568 peer_review_queue_entries::delete_by_receiving_peer_reviews_exercise_slide_submission_id(
569 conn, ess_id,
570 ).await?;
571 info!("Deleting done, trying to select a new peer review candidate");
572 }
573 } else {
574 return Ok(None);
576 }
577 }
578
579 warn!("Maximum attempts ({MAX_ATTEMPTS}) reached without finding a valid submission");
580 Ok(None)
581}
582
583async fn try_to_select_peer_review_candidate_from_queue_impl(
585 conn: &mut PgConnection,
586 exercise_id: Uuid,
587 excluded_user_id: Uuid,
588 excluded_exercise_slide_submission_ids: &[Uuid],
589) -> ModelResult<Option<(Uuid, bool)>> {
590 let mut rng = rng();
591 let mut candidates = peer_review_queue_entries::get_many_that_need_peer_reviews_by_exercise_id_and_review_priority(conn,
593 exercise_id,
594 excluded_user_id,
595 excluded_exercise_slide_submission_ids,
596 MAX_PEER_REVIEW_CANDIDATES,
597 ).await?;
598 candidates.shuffle(&mut rng);
599 match candidates.into_iter().next() {
600 Some(candidate) => Ok(Some((
601 candidate.receiving_peer_reviews_exercise_slide_submission_id,
602 true,
603 ))),
604 None => {
605 let mut candidates = peer_review_queue_entries::get_any_including_not_needing_review(
607 conn,
608 exercise_id,
609 excluded_user_id,
610 excluded_exercise_slide_submission_ids,
611 MAX_PEER_REVIEW_CANDIDATES,
612 )
613 .await?;
614 candidates.shuffle(&mut rng);
615 Ok(candidates.into_iter().next().map(|entry| {
616 (
617 entry.receiving_peer_reviews_exercise_slide_submission_id,
618 false,
619 )
620 }))
621 }
622 }
623}
624
625async fn get_course_material_peer_or_self_review_data(
626 conn: &mut PgConnection,
627 peer_or_self_review_config: &PeerOrSelfReviewConfig,
628 exercise_slide_submission: &Option<ExerciseSlideSubmission>,
629 reviewer_user_id: Uuid,
630 exercise_id: Uuid,
631 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
632) -> ModelResult<CourseMaterialPeerOrSelfReviewData> {
633 let peer_or_self_review_questions =
634 peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
635 conn,
636 peer_or_self_review_config.id,
637 )
638 .await?;
639 let num_peer_reviews_given =
640 peer_or_self_review_submissions::get_num_peer_reviews_given_by_user_and_course_instance_and_exercise(
641 conn,
642 reviewer_user_id,
643 peer_or_self_review_config.course_id,
644 exercise_id,
645 )
646 .await?;
647
648 let answer_to_review = match exercise_slide_submission {
649 Some(exercise_slide_submission) => {
650 let exercise_slide_submission_id = exercise_slide_submission.id;
651 let course_material_exercise_tasks = exercise_task_submissions::get_exercise_task_submission_info_by_exercise_slide_submission_id(
652 conn,
653 exercise_slide_submission_id,
654 reviewer_user_id,
655 fetch_service_info,
656 false
657 ).await?;
658 Some(CourseMaterialPeerOrSelfReviewDataAnswerToReview {
659 exercise_slide_submission_id,
660 course_material_exercise_tasks,
661 })
662 }
663 None => None,
664 };
665
666 Ok(CourseMaterialPeerOrSelfReviewData {
667 answer_to_review,
668 peer_or_self_review_config: peer_or_self_review_config.clone(),
669 peer_or_self_review_questions,
670 num_peer_reviews_given,
671 })
672}
673
674#[instrument(skip(conn))]
675pub async fn update_peer_review_queue_reviews_received(
676 conn: &mut PgConnection,
677 course_id: Uuid,
678) -> ModelResult<()> {
679 let mut tx = conn.begin().await?;
680 info!("Updating peer review queue reviews received");
681 let exercises = crate::exercises::get_exercises_by_course_id(&mut tx, course_id)
682 .await?
683 .into_iter()
684 .filter(|e| e.needs_peer_review)
685 .collect::<Vec<_>>();
686 for exercise in exercises {
687 info!("Processing exercise {:?}", exercise.id);
688 let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id(
689 &mut tx, &exercise, course_id,
690 )
691 .await?;
692 let peer_review_queue_entries =
693 crate::peer_review_queue_entries::get_all_that_need_peer_reviews_by_exercise_id(
694 &mut tx,
695 exercise.id,
696 )
697 .await?;
698 info!(
699 "Processing {:?} peer review queue entries",
700 peer_review_queue_entries.len()
701 );
702 for peer_review_queue_entry in peer_review_queue_entries {
703 update_peer_review_receiver_exercise_status(
704 &mut tx,
705 &exercise,
706 &peer_or_self_review_config,
707 peer_review_queue_entry,
708 )
709 .await?;
710 }
711 }
712 info!("Done");
713 tx.commit().await?;
714 Ok(())
715}
716
717#[cfg(test)]
718mod tests {
719 use super::*;
720
721 mod validate_peer_or_self_review_submissions_answers {
722 use chrono::TimeZone;
723
724 use crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType;
725
726 use super::*;
727
728 #[test]
729 fn accepts_valid_answers() {
730 let peer_or_self_review_config_id =
731 Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
732 let question_id = Uuid::parse_str("68d5cda3-6ad8-464b-9af1-bd1692fcbee1").unwrap();
733 let questions = HashMap::from([(
734 question_id,
735 create_peer_review_question(question_id, peer_or_self_review_config_id, true)
736 .unwrap(),
737 )]);
738 let answers = vec![create_peer_review_answer(question_id)];
739 assert_eq!(
740 validate_and_sanitize_peer_review_submission_answers(questions, answers)
741 .unwrap()
742 .len(),
743 1
744 );
745 }
746
747 #[test]
748 fn filters_illegal_answers() {
749 let peer_or_self_review_config_id =
750 Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
751 let questions = HashMap::new();
752 let answers = vec![create_peer_review_answer(peer_or_self_review_config_id)];
753 assert_eq!(
754 validate_and_sanitize_peer_review_submission_answers(questions, answers)
755 .unwrap()
756 .len(),
757 0
758 );
759 }
760
761 #[test]
762 fn errors_on_missing_required_answers() {
763 let peer_or_self_review_config_id =
764 Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
765 let question_id = Uuid::parse_str("68d5cda3-6ad8-464b-9af1-bd1692fcbee1").unwrap();
766 let questions = HashMap::from([(
767 question_id,
768 create_peer_review_question(question_id, peer_or_self_review_config_id, true)
769 .unwrap(),
770 )]);
771 assert!(
772 validate_and_sanitize_peer_review_submission_answers(questions, vec![]).is_err()
773 )
774 }
775
776 fn create_peer_review_question(
777 id: Uuid,
778 peer_or_self_review_config_id: Uuid,
779 answer_required: bool,
780 ) -> ModelResult<PeerOrSelfReviewQuestion> {
781 Ok(PeerOrSelfReviewQuestion {
782 id,
783 created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
784 updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
785 deleted_at: None,
786 peer_or_self_review_config_id,
787 order_number: 0,
788 question: "".to_string(),
789 question_type: PeerOrSelfReviewQuestionType::Essay,
790 answer_required,
791 weight: 0.0,
792 })
793 }
794
795 fn create_peer_review_answer(
796 peer_or_self_review_question_id: Uuid,
797 ) -> CourseMaterialPeerOrSelfReviewQuestionAnswer {
798 CourseMaterialPeerOrSelfReviewQuestionAnswer {
799 peer_or_self_review_question_id,
800 text_data: Some("".to_string()),
801 number_data: None,
802 }
803 }
804 }
805}