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, CourseInstanceOrExamId, 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_instance_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_instance_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_instance_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_instance_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 CourseInstanceOrExamId::Instance(peer_review_queue_entry.course_instance_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 let course_instance_id = reviewer_user_exercise_state.get_course_instance_id()?;
399
400 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_instance_id).await? {
402 let data = get_course_material_peer_or_self_review_data(
403 conn,
404 &peer_or_self_review_config,
405 &Some(saved_exercise_slide_submission_to_review),
406 reviewer_user_exercise_state.user_id,
407 course_instance_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_instance_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_instance_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 course_instance_id,
469 exercise.id,
470 fetch_service_info,
471 )
472 .await?;
473
474 Ok(data)
475}
476
477pub async fn select_own_submission_for_self_review(
479 conn: &mut PgConnection,
480 exercise: &Exercise,
481 reviewer_user_exercise_state: &UserExerciseState,
482 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
483) -> ModelResult<CourseMaterialPeerOrSelfReviewData> {
484 let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id(
485 conn,
486 exercise,
487 exercise.get_course_id()?,
488 )
489 .await?;
490 let course_instance_id = reviewer_user_exercise_state.get_course_instance_id()?;
491 let exercise_slide_submission =
492 exercise_slide_submissions::get_users_latest_exercise_slide_submission(
493 conn,
494 reviewer_user_exercise_state.get_selected_exercise_slide_id()?,
495 reviewer_user_exercise_state.user_id,
496 )
497 .await?;
498 let data = get_course_material_peer_or_self_review_data(
499 conn,
500 &peer_or_self_review_config,
501 &Some(exercise_slide_submission),
502 reviewer_user_exercise_state.user_id,
503 course_instance_id,
504 exercise.id,
505 fetch_service_info,
506 )
507 .await?;
508
509 Ok(data)
510}
511
512async fn try_to_select_peer_review_candidate_from_queue(
513 conn: &mut PgConnection,
514 exercise_id: Uuid,
515 excluded_user_id: Uuid,
516 excluded_exercise_slide_submission_ids: &[Uuid],
517) -> ModelResult<Option<ExerciseSlideSubmission>> {
518 const MAX_ATTEMPTS: u32 = 10;
519 let mut attempts = 0;
520
521 while attempts < MAX_ATTEMPTS {
523 attempts += 1;
524 let maybe_submission = try_to_select_peer_review_candidate_from_queue_impl(
525 conn,
526 exercise_id,
527 excluded_user_id,
528 excluded_exercise_slide_submission_ids,
529 )
530 .await?;
531
532 if let Some((ess_id, selected_submission_needs_peer_review)) = maybe_submission {
533 if excluded_exercise_slide_submission_ids.contains(&ess_id) {
534 warn!(exercise_slide_submission_id = %ess_id, "Selected exercise slide submission that should have been excluded from the selection process. Trying again.");
535 continue;
536 }
537
538 let ess = exercise_slide_submissions::get_by_id(conn, ess_id)
539 .await
540 .optional()?;
541 if let Some(ess) = ess {
542 if ess.course_id.is_none() || ess.course_instance_id.is_none() {
544 warn!(exercise_slide_submission_id = %ess_id, "Selected exercise slide submission that doesn't have a course_id or course_instance_id. Skipping it.");
545 continue;
546 };
547 if ess.deleted_at.is_none() {
548 let peer_review_queue_entry = peer_review_queue_entries::get_by_receiving_peer_reviews_exercise_slide_submission_id(conn, ess_id).await?;
550 if !selected_submission_needs_peer_review {
552 return Ok(Some(ess));
553 }
554 if peer_review_queue_entry.deleted_at.is_none()
555 && !peer_review_queue_entry.removed_from_queue_for_unusual_reason
556 {
557 return Ok(Some(ess));
558 } else {
559 if attempts == MAX_ATTEMPTS {
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, "Max attempts reached, returning submission despite being removed from queue");
561 return Ok(Some(ess));
562 }
563 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.");
564 continue;
565 }
566 }
567 } else {
568 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.");
571 peer_review_queue_entries::delete_by_receiving_peer_reviews_exercise_slide_submission_id(
572 conn, ess_id,
573 ).await?;
574 info!("Deleting done, trying to select a new peer review candidate");
575 }
576 } else {
577 return Ok(None);
579 }
580 }
581
582 warn!("Maximum attempts ({MAX_ATTEMPTS}) reached without finding a valid submission");
583 Ok(None)
584}
585
586async fn try_to_select_peer_review_candidate_from_queue_impl(
588 conn: &mut PgConnection,
589 exercise_id: Uuid,
590 excluded_user_id: Uuid,
591 excluded_exercise_slide_submission_ids: &[Uuid],
592) -> ModelResult<Option<(Uuid, bool)>> {
593 let mut rng = rng();
594 let mut candidates = peer_review_queue_entries::get_many_that_need_peer_reviews_by_exercise_id_and_review_priority(conn,
596 exercise_id,
597 excluded_user_id,
598 excluded_exercise_slide_submission_ids,
599 MAX_PEER_REVIEW_CANDIDATES,
600 ).await?;
601 candidates.shuffle(&mut rng);
602 match candidates.into_iter().next() {
603 Some(candidate) => Ok(Some((
604 candidate.receiving_peer_reviews_exercise_slide_submission_id,
605 true,
606 ))),
607 None => {
608 let mut candidates = peer_review_queue_entries::get_any_including_not_needing_review(
610 conn,
611 exercise_id,
612 excluded_user_id,
613 excluded_exercise_slide_submission_ids,
614 MAX_PEER_REVIEW_CANDIDATES,
615 )
616 .await?;
617 candidates.shuffle(&mut rng);
618 Ok(candidates.into_iter().next().map(|entry| {
619 (
620 entry.receiving_peer_reviews_exercise_slide_submission_id,
621 false,
622 )
623 }))
624 }
625 }
626}
627
628async fn get_course_material_peer_or_self_review_data(
629 conn: &mut PgConnection,
630 peer_or_self_review_config: &PeerOrSelfReviewConfig,
631 exercise_slide_submission: &Option<ExerciseSlideSubmission>,
632 reviewer_user_id: Uuid,
633 reviewer_course_instance_id: Uuid,
634 exercise_id: Uuid,
635 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
636) -> ModelResult<CourseMaterialPeerOrSelfReviewData> {
637 let peer_or_self_review_questions =
638 peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id(
639 conn,
640 peer_or_self_review_config.id,
641 )
642 .await?;
643 let num_peer_reviews_given =
644 peer_or_self_review_submissions::get_num_peer_reviews_given_by_user_and_course_instance_and_exercise(
645 conn,
646 reviewer_user_id,
647 reviewer_course_instance_id,
648 exercise_id,
649 )
650 .await?;
651
652 let answer_to_review = match exercise_slide_submission {
653 Some(exercise_slide_submission) => {
654 let exercise_slide_submission_id = exercise_slide_submission.id;
655 let course_material_exercise_tasks = exercise_task_submissions::get_exercise_task_submission_info_by_exercise_slide_submission_id(
656 conn,
657 exercise_slide_submission_id,
658 reviewer_user_id,
659 fetch_service_info
660 ).await?;
661 Some(CourseMaterialPeerOrSelfReviewDataAnswerToReview {
662 exercise_slide_submission_id,
663 course_material_exercise_tasks,
664 })
665 }
666 None => None,
667 };
668
669 Ok(CourseMaterialPeerOrSelfReviewData {
670 answer_to_review,
671 peer_or_self_review_config: peer_or_self_review_config.clone(),
672 peer_or_self_review_questions,
673 num_peer_reviews_given,
674 })
675}
676
677#[instrument(skip(conn))]
678pub async fn update_peer_review_queue_reviews_received(
679 conn: &mut PgConnection,
680 course_id: Uuid,
681) -> ModelResult<()> {
682 let mut tx = conn.begin().await?;
683 info!("Updating peer review queue reviews received");
684 let exercises = crate::exercises::get_exercises_by_course_id(&mut tx, course_id)
685 .await?
686 .into_iter()
687 .filter(|e| e.needs_peer_review)
688 .collect::<Vec<_>>();
689 for exercise in exercises {
690 info!("Processing exercise {:?}", exercise.id);
691 let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id(
692 &mut tx, &exercise, course_id,
693 )
694 .await?;
695 let peer_review_queue_entries =
696 crate::peer_review_queue_entries::get_all_that_need_peer_reviews_by_exercise_id(
697 &mut tx,
698 exercise.id,
699 )
700 .await?;
701 info!(
702 "Processing {:?} peer review queue entries",
703 peer_review_queue_entries.len()
704 );
705 for peer_review_queue_entry in peer_review_queue_entries {
706 update_peer_review_receiver_exercise_status(
707 &mut tx,
708 &exercise,
709 &peer_or_self_review_config,
710 peer_review_queue_entry,
711 )
712 .await?;
713 }
714 }
715 info!("Done");
716 tx.commit().await?;
717 Ok(())
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723
724 mod validate_peer_or_self_review_submissions_answers {
725 use chrono::TimeZone;
726
727 use crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType;
728
729 use super::*;
730
731 #[test]
732 fn accepts_valid_answers() {
733 let peer_or_self_review_config_id =
734 Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
735 let question_id = Uuid::parse_str("68d5cda3-6ad8-464b-9af1-bd1692fcbee1").unwrap();
736 let questions = HashMap::from([(
737 question_id,
738 create_peer_review_question(question_id, peer_or_self_review_config_id, true)
739 .unwrap(),
740 )]);
741 let answers = vec![create_peer_review_answer(question_id)];
742 assert_eq!(
743 validate_and_sanitize_peer_review_submission_answers(questions, answers)
744 .unwrap()
745 .len(),
746 1
747 );
748 }
749
750 #[test]
751 fn filters_illegal_answers() {
752 let peer_or_self_review_config_id =
753 Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
754 let questions = HashMap::new();
755 let answers = vec![create_peer_review_answer(peer_or_self_review_config_id)];
756 assert_eq!(
757 validate_and_sanitize_peer_review_submission_answers(questions, answers)
758 .unwrap()
759 .len(),
760 0
761 );
762 }
763
764 #[test]
765 fn errors_on_missing_required_answers() {
766 let peer_or_self_review_config_id =
767 Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap();
768 let question_id = Uuid::parse_str("68d5cda3-6ad8-464b-9af1-bd1692fcbee1").unwrap();
769 let questions = HashMap::from([(
770 question_id,
771 create_peer_review_question(question_id, peer_or_self_review_config_id, true)
772 .unwrap(),
773 )]);
774 assert!(
775 validate_and_sanitize_peer_review_submission_answers(questions, vec![]).is_err()
776 )
777 }
778
779 fn create_peer_review_question(
780 id: Uuid,
781 peer_or_self_review_config_id: Uuid,
782 answer_required: bool,
783 ) -> ModelResult<PeerOrSelfReviewQuestion> {
784 Ok(PeerOrSelfReviewQuestion {
785 id,
786 created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
787 updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(),
788 deleted_at: None,
789 peer_or_self_review_config_id,
790 order_number: 0,
791 question: "".to_string(),
792 question_type: PeerOrSelfReviewQuestionType::Essay,
793 answer_required,
794 weight: 0.0,
795 })
796 }
797
798 fn create_peer_review_answer(
799 peer_or_self_review_question_id: Uuid,
800 ) -> CourseMaterialPeerOrSelfReviewQuestionAnswer {
801 CourseMaterialPeerOrSelfReviewQuestionAnswer {
802 peer_or_self_review_question_id,
803 text_data: Some("".to_string()),
804 number_data: None,
805 }
806 }
807 }
808}