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