1use futures::future::BoxFuture;
2use url::Url;
3use utoipa::ToSchema;
4
5use crate::error::{ensure_version, missing_model_error};
6use crate::{
7 exercise_service_info::ExerciseServiceInfoApi,
8 exercises::{self, Exercise},
9 library::{self, peer_or_self_reviewing::CourseMaterialPeerOrSelfReviewData},
10 peer_or_self_review_questions::{
11 CmsPeerOrSelfReviewQuestion,
12 delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids,
13 upsert_multiple_peer_or_self_review_questions,
14 },
15 prelude::*,
16 user_exercise_states::{self, ReviewingStage},
17};
18
19#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
20
21pub struct PeerOrSelfReviewConfig {
22 pub id: Uuid,
23 pub created_at: DateTime<Utc>,
24 pub updated_at: DateTime<Utc>,
25 pub deleted_at: Option<DateTime<Utc>>,
26 pub course_id: Uuid,
27 pub exercise_id: Option<Uuid>,
28 pub peer_reviews_to_give: i32,
29 pub peer_reviews_to_receive: i32,
30 pub accepting_threshold: f32,
31 pub processing_strategy: PeerReviewProcessingStrategy,
32 pub manual_review_cutoff_in_days: i32,
33 pub points_are_all_or_nothing: bool,
34 pub reset_answer_if_zero_points_from_review: bool,
35 pub review_instructions: Option<serde_json::Value>,
36}
37
38#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
40
41pub struct CourseMaterialPeerOrSelfReviewConfig {
42 pub id: Uuid,
43 pub course_id: Uuid,
44 pub exercise_id: Option<Uuid>,
45 pub peer_reviews_to_give: i32,
46 pub peer_reviews_to_receive: i32,
47}
48
49#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
50
51pub struct CmsPeerOrSelfReviewConfig {
52 pub id: Uuid,
53 pub course_id: Uuid,
54 pub exercise_id: Option<Uuid>,
55 pub peer_reviews_to_give: i32,
56 pub peer_reviews_to_receive: i32,
57 pub accepting_threshold: f32,
58 pub processing_strategy: PeerReviewProcessingStrategy,
59 pub points_are_all_or_nothing: bool,
60 pub reset_answer_if_zero_points_from_review: bool,
61 pub review_instructions: Option<serde_json::Value>,
62}
63
64impl CmsPeerOrSelfReviewConfig {
65 pub fn settings_differing_from(&self, other: &CmsPeerOrSelfReviewConfig) -> Vec<&'static str> {
68 let CmsPeerOrSelfReviewConfig {
69 id: _,
70 course_id: _,
71 exercise_id: _,
72 peer_reviews_to_give,
73 peer_reviews_to_receive,
74 accepting_threshold,
75 processing_strategy,
76 points_are_all_or_nothing,
77 reset_answer_if_zero_points_from_review,
78 review_instructions: _,
79 } = self;
80 [
81 (
82 "peer_reviews_to_give",
83 *peer_reviews_to_give != other.peer_reviews_to_give,
84 ),
85 (
86 "peer_reviews_to_receive",
87 *peer_reviews_to_receive != other.peer_reviews_to_receive,
88 ),
89 (
90 "accepting_threshold",
91 *accepting_threshold != other.accepting_threshold,
92 ),
93 (
94 "processing_strategy",
95 *processing_strategy != other.processing_strategy,
96 ),
97 (
98 "points_are_all_or_nothing",
99 *points_are_all_or_nothing != other.points_are_all_or_nothing,
100 ),
101 (
102 "reset_answer_if_zero_points_from_review",
103 *reset_answer_if_zero_points_from_review
104 != other.reset_answer_if_zero_points_from_review,
105 ),
106 ]
107 .into_iter()
108 .filter(|(_, differs)| *differs)
109 .map(|(name, _)| name)
110 .collect()
111 }
112}
113
114#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
115
116pub struct CmsPeerOrSelfReviewConfiguration {
117 pub peer_or_self_review_config: CmsPeerOrSelfReviewConfig,
118 pub peer_or_self_review_questions: Vec<CmsPeerOrSelfReviewQuestion>,
119}
120
121#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
124pub struct VersionedCmsPeerOrSelfReviewConfiguration {
125 #[serde(flatten)]
126 pub configuration: CmsPeerOrSelfReviewConfiguration,
127 pub updated_at: DateTime<Utc>,
128}
129
130#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
133pub struct CmsPeerOrSelfReviewConfigurationSave {
134 #[serde(flatten)]
135 pub configuration: CmsPeerOrSelfReviewConfiguration,
136 pub expected_updated_at: Option<DateTime<Utc>>,
137}
138
139#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, sqlx::Type, ToSchema)]
145#[sqlx(
146 type_name = "peer_review_processing_strategy",
147 rename_all = "snake_case"
148)]
149pub enum PeerReviewProcessingStrategy {
150 AutomaticallyGradeByAverage,
152 AutomaticallyGradeOrManualReviewByAverage,
154 ManualReviewEverything,
156}
157
158pub async fn insert(
159 conn: &mut PgConnection,
160 pkey_policy: PKeyPolicy<Uuid>,
161 course_id: Uuid,
162 exercise_id: Option<Uuid>,
163) -> ModelResult<Uuid> {
164 let res = sqlx::query!(
165 "
166INSERT INTO peer_or_self_review_configs (id, course_id, exercise_id)
167VALUES ($1, $2, $3)
168RETURNING *
169 ",
170 pkey_policy.into_uuid(),
171 course_id,
172 exercise_id,
173 )
174 .fetch_one(conn)
175 .await?;
176 Ok(res.id)
177}
178
179pub async fn upsert_with_id(
180 conn: &mut PgConnection,
181 pkey_policy: PKeyPolicy<Uuid>,
182 cms_peer_review: &CmsPeerOrSelfReviewConfig,
183) -> ModelResult<CmsPeerOrSelfReviewConfig> {
184 let res = sqlx::query_as!(
185 CmsPeerOrSelfReviewConfig,
186 r#"
187 INSERT INTO peer_or_self_review_configs (
188 id,
189 course_id,
190 exercise_id,
191 peer_reviews_to_give,
192 peer_reviews_to_receive,
193 accepting_threshold,
194 processing_strategy,
195 points_are_all_or_nothing,
196 review_instructions,
197 reset_answer_if_zero_points_from_review
198 )
199VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (id) DO
200UPDATE
201SET course_id = excluded.course_id,
202 exercise_id = excluded.exercise_id,
203 peer_reviews_to_give = excluded.peer_reviews_to_give,
204 peer_reviews_to_receive = excluded.peer_reviews_to_receive,
205 accepting_threshold = excluded.accepting_threshold,
206 processing_strategy = excluded.processing_strategy,
207 points_are_all_or_nothing = excluded.points_are_all_or_nothing,
208 reset_answer_if_zero_points_from_review = excluded.reset_answer_if_zero_points_from_review,
209 review_instructions = excluded.review_instructions
210RETURNING id,
211 course_id,
212 exercise_id,
213 peer_reviews_to_give,
214 peer_reviews_to_receive,
215 accepting_threshold,
216 processing_strategy,
217 points_are_all_or_nothing,
218 review_instructions,
219 reset_answer_if_zero_points_from_review
220"#,
221 pkey_policy.into_uuid(),
222 cms_peer_review.course_id,
223 cms_peer_review.exercise_id,
224 cms_peer_review.peer_reviews_to_give,
225 cms_peer_review.peer_reviews_to_receive,
226 cms_peer_review.accepting_threshold,
227 cms_peer_review.processing_strategy as _,
228 cms_peer_review.points_are_all_or_nothing,
229 cms_peer_review.review_instructions,
230 cms_peer_review.reset_answer_if_zero_points_from_review,
231 )
232 .fetch_one(conn)
233 .await?;
234 Ok(res)
235}
236
237pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<PeerOrSelfReviewConfig> {
238 let res = sqlx::query_as!(
239 PeerOrSelfReviewConfig,
240 r#"
241SELECT *
242FROM peer_or_self_review_configs
243WHERE id = $1
244 AND deleted_at IS NULL
245 "#,
246 id
247 )
248 .fetch_one(conn)
249 .await?;
250 Ok(res)
251}
252
253pub async fn get_by_ids(
255 conn: &mut PgConnection,
256 ids: &[Uuid],
257) -> ModelResult<Vec<PeerOrSelfReviewConfig>> {
258 let res = sqlx::query_as!(
259 PeerOrSelfReviewConfig,
260 r#"
261SELECT *
262FROM peer_or_self_review_configs
263WHERE id = ANY($1)
264 AND deleted_at IS NULL
265 "#,
266 ids
267 )
268 .fetch_all(conn)
269 .await?;
270 Ok(res)
271}
272
273pub async fn get_by_exercise_id(
275 conn: &mut PgConnection,
276 exercise_id: Uuid,
277) -> ModelResult<PeerOrSelfReviewConfig> {
278 let res = sqlx::query_as!(
279 PeerOrSelfReviewConfig,
280 r#"
281SELECT *
282FROM peer_or_self_review_configs
283WHERE exercise_id = $1
284 AND deleted_at IS NULL
285 "#,
286 exercise_id
287 )
288 .fetch_one(conn)
289 .await?;
290 Ok(res)
291}
292
293pub async fn get_by_exercise_or_course_id(
295 conn: &mut PgConnection,
296 exercise: &Exercise,
297 course_id: Uuid,
298) -> ModelResult<PeerOrSelfReviewConfig> {
299 if exercise.use_course_default_peer_or_self_review_config {
300 get_default_for_course_by_course_id(conn, course_id).await
301 } else {
302 get_by_exercise_id(conn, exercise.id).await
303 }
304}
305
306pub async fn get_default_for_course_by_course_id(
307 conn: &mut PgConnection,
308 course_id: Uuid,
309) -> ModelResult<PeerOrSelfReviewConfig> {
310 let res = sqlx::query_as!(
311 PeerOrSelfReviewConfig,
312 r#"
313SELECT *
314FROM peer_or_self_review_configs
315WHERE course_id = $1
316 AND exercise_id IS NULL
317 AND deleted_at IS NULL;
318 "#,
319 course_id
320 )
321 .fetch_one(conn)
322 .await?;
323 Ok(res)
324}
325
326pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<Uuid> {
327 let res = sqlx::query!(
328 "
329UPDATE peer_or_self_review_configs
330SET deleted_at = now()
331WHERE id = $1
332AND deleted_at IS NULL
333RETURNING *
334 ",
335 id
336 )
337 .fetch_one(conn)
338 .await?;
339 Ok(res.id)
340}
341
342pub async fn get_course_material_peer_or_self_review_data(
343 conn: &mut PgConnection,
344 user_id: Uuid,
345 exercise_id: Uuid,
346 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
347 file_store: &dyn FileStore,
348 app_conf: &ApplicationConfiguration,
349) -> ModelResult<CourseMaterialPeerOrSelfReviewData> {
350 let exercise = exercises::get_by_id(conn, exercise_id).await?;
351 let (_current_exercise_slide, instance_or_exam_id) = exercises::get_or_select_exercise_slide(
352 &mut *conn,
353 Some(user_id),
354 &exercise,
355 &fetch_service_info,
356 file_store,
357 app_conf,
358 )
359 .await?;
360
361 let user_exercise_state = match instance_or_exam_id {
362 Some(course_or_exam_id) => {
363 user_exercise_states::get_user_exercise_state_if_exists(
364 conn,
365 user_id,
366 exercise.id,
367 course_or_exam_id,
368 )
369 .await?
370 }
371 _ => None,
372 };
373
374 match user_exercise_state {
375 Some(ref user_exercise_state) => {
376 if matches!(
377 user_exercise_state.reviewing_stage,
378 ReviewingStage::PeerReview | ReviewingStage::WaitingForPeerReviews
379 ) {
380 let res = library::peer_or_self_reviewing::try_to_select_exercise_slide_submission_for_peer_review(
383 conn,
384 &exercise,
385 user_exercise_state,
386 &fetch_service_info,
387 file_store,
388 app_conf,
389 )
390 .await?;
391 Ok(res)
392 } else if user_exercise_state.reviewing_stage == ReviewingStage::SelfReview {
393 let res = library::peer_or_self_reviewing::select_own_submission_for_self_review(
394 conn,
395 &exercise,
396 user_exercise_state,
397 &fetch_service_info,
398 file_store,
399 app_conf,
400 )
401 .await?;
402 Ok(res)
403 } else {
404 Err(ModelError::new(
405 ModelErrorType::PreconditionFailed,
406 "You cannot peer review yet".to_string(),
407 None,
408 ))
409 }
410 }
411 None => Err(ModelError::new(
412 ModelErrorType::InvalidRequest,
413 "You haven't answered this exercise".to_string(),
414 None,
415 )),
416 }
417}
418
419pub async fn get_peer_reviews_by_page_ids(
421 conn: &mut PgConnection,
422 page_ids: &[Uuid],
423) -> ModelResult<Vec<CmsPeerOrSelfReviewConfig>> {
424 let res = sqlx::query_as!(
425 CmsPeerOrSelfReviewConfig,
426 r#"
427SELECT pr.id as id,
428 pr.course_id as course_id,
429 pr.exercise_id as exercise_id,
430 pr.peer_reviews_to_give as peer_reviews_to_give,
431 pr.peer_reviews_to_receive as peer_reviews_to_receive,
432 pr.accepting_threshold as accepting_threshold,
433 pr.processing_strategy,
434 points_are_all_or_nothing,
435 pr.reset_answer_if_zero_points_from_review,
436 pr.review_instructions
437from pages p
438 join exercises e on p.id = e.page_id
439 join peer_or_self_review_configs pr on e.id = pr.exercise_id
440where p.id = ANY($1)
441 AND p.deleted_at IS NULL
442 AND e.deleted_at IS NULL
443 AND pr.deleted_at IS NULL;
444 "#,
445 page_ids,
446 )
447 .fetch_all(conn)
448 .await?;
449
450 Ok(res)
451}
452
453pub async fn delete_peer_reviews_by_exrcise_ids(
454 conn: &mut PgConnection,
455 exercise_ids: &[Uuid],
456) -> ModelResult<Vec<Uuid>> {
457 let res = sqlx::query!(
458 "
459UPDATE peer_or_self_review_configs
460SET deleted_at = now()
461WHERE exercise_id = ANY ($1)
462AND deleted_at IS NULL
463RETURNING *;
464 ",
465 exercise_ids
466 )
467 .fetch_all(conn)
468 .await?
469 .into_iter()
470 .map(|x| x.id)
471 .collect();
472 Ok(res)
473}
474
475pub async fn get_course_default_cms_peer_review(
476 conn: &mut PgConnection,
477 course_id: Uuid,
478) -> ModelResult<CmsPeerOrSelfReviewConfig> {
479 let res = sqlx::query_as!(
480 CmsPeerOrSelfReviewConfig,
481 r#"
482SELECT id,
483 course_id,
484 exercise_id,
485 peer_reviews_to_give,
486 peer_reviews_to_receive,
487 accepting_threshold,
488 processing_strategy,
489 points_are_all_or_nothing,
490 reset_answer_if_zero_points_from_review,
491 review_instructions
492FROM peer_or_self_review_configs
493WHERE course_id = $1
494 AND exercise_id IS NULL
495 AND deleted_at IS NULL;
496"#,
497 course_id
498 )
499 .fetch_one(conn)
500 .await?;
501 Ok(res)
502}
503
504pub async fn set_course_default_review_instructions(
507 conn: &mut PgConnection,
508 config_id: Uuid,
509 course_id: Uuid,
510 review_instructions: Option<&serde_json::Value>,
511) -> ModelResult<()> {
512 let updated = sqlx::query!(
513 r#"
514UPDATE peer_or_self_review_configs
515SET review_instructions = $3
516WHERE id = $1
517 AND course_id = $2
518 AND exercise_id IS NULL
519 AND deleted_at IS NULL
520 "#,
521 config_id,
522 course_id,
523 review_instructions,
524 )
525 .execute(conn)
526 .await?;
527 if updated.rows_affected() == 0 {
528 return Err(model_err!(
529 PreconditionFailed,
530 "The course default review config doesn't exist".to_string()
531 ));
532 }
533 Ok(())
534}
535
536pub async fn get_cms_peer_review_by_id(
537 conn: &mut PgConnection,
538 peer_or_self_review_config_id: Uuid,
539) -> ModelResult<CmsPeerOrSelfReviewConfig> {
540 let res = sqlx::query_as!(
541 CmsPeerOrSelfReviewConfig,
542 r#"
543SELECT id,
544 course_id,
545 exercise_id,
546 peer_reviews_to_give,
547 peer_reviews_to_receive,
548 accepting_threshold,
549 processing_strategy,
550 points_are_all_or_nothing,
551 reset_answer_if_zero_points_from_review,
552 review_instructions
553FROM peer_or_self_review_configs
554WHERE id = $1;
555 "#,
556 peer_or_self_review_config_id
557 )
558 .fetch_one(conn)
559 .await?;
560 Ok(res)
561}
562
563pub async fn upsert_course_default_cms_peer_review_and_questions(
564 conn: &mut PgConnection,
565 peer_or_self_review_configuration: &CmsPeerOrSelfReviewConfiguration,
566) -> ModelResult<CmsPeerOrSelfReviewConfiguration> {
567 let peer_or_self_review_config = upsert_with_id(
569 conn,
570 PKeyPolicy::Fixed(
571 peer_or_self_review_configuration
572 .peer_or_self_review_config
573 .id,
574 ),
575 &peer_or_self_review_configuration.peer_or_self_review_config,
576 )
577 .await?;
578
579 let previous_peer_or_self_review_question_ids =
581 delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids(
582 conn,
583 &[peer_or_self_review_config.id],
584 )
585 .await?;
586 let peer_or_self_review_questions = upsert_multiple_peer_or_self_review_questions(
587 conn,
588 &peer_or_self_review_configuration
589 .peer_or_self_review_questions
590 .iter()
591 .map(|prq| {
592 let id = if previous_peer_or_self_review_question_ids.contains(&prq.id) {
593 prq.id
594 } else {
595 Uuid::new_v4()
596 };
597 CmsPeerOrSelfReviewQuestion { id, ..prq.clone() }
598 })
599 .collect::<Vec<_>>(),
600 )
601 .await?;
602
603 Ok(CmsPeerOrSelfReviewConfiguration {
604 peer_or_self_review_config,
605 peer_or_self_review_questions,
606 })
607}
608
609pub async fn upsert_for_course_id(
612 conn: &mut PgConnection,
613 course_id: Uuid,
614 peer_or_self_review_configuration: &CmsPeerOrSelfReviewConfiguration,
615 expected_updated_at: Option<DateTime<Utc>>,
616) -> ModelResult<VersionedCmsPeerOrSelfReviewConfiguration> {
617 let input = &peer_or_self_review_configuration.peer_or_self_review_config;
618 if input.course_id != course_id {
619 return Err(model_err!(
620 PreconditionFailed,
621 "Peer review config course does not match expected course".to_string()
622 ));
623 }
624 if peer_or_self_review_configuration
625 .peer_or_self_review_questions
626 .iter()
627 .any(|q| q.peer_or_self_review_config_id != input.id)
628 {
629 return Err(model_err!(
630 PreconditionFailed,
631 "Peer review questions do not belong to the peer review config".to_string()
632 ));
633 }
634
635 let mut tx = conn.begin().await?;
636 if expected_updated_at.is_some() {
637 let current_updated_at = sqlx::query_scalar!(
638 "
639SELECT updated_at
640FROM peer_or_self_review_configs
641WHERE id = $1
642 AND course_id = $2
643 AND deleted_at IS NULL
644FOR UPDATE
645",
646 input.id,
647 course_id
648 )
649 .fetch_optional(&mut *tx)
650 .await?;
651 ensure_version(
652 current_updated_at,
653 expected_updated_at,
654 "The default peer review settings were changed after they were opened.",
655 )?;
656 }
657 let peer_or_self_review_config = sqlx::query_as!(
658 CmsPeerOrSelfReviewConfig,
659 r#"
660INSERT INTO peer_or_self_review_configs (
661 id,
662 course_id,
663 exercise_id,
664 peer_reviews_to_give,
665 peer_reviews_to_receive,
666 accepting_threshold,
667 processing_strategy,
668 points_are_all_or_nothing,
669 review_instructions,
670 reset_answer_if_zero_points_from_review
671)
672SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10
673WHERE (
674 $3::uuid IS NULL
675 OR EXISTS (
676 SELECT 1
677 FROM exercises
678 WHERE id = $3
679 AND course_id = $2
680 AND deleted_at IS NULL
681 )
682)
683ON CONFLICT (id) DO UPDATE
684SET course_id = excluded.course_id,
685 exercise_id = excluded.exercise_id,
686 peer_reviews_to_give = excluded.peer_reviews_to_give,
687 peer_reviews_to_receive = excluded.peer_reviews_to_receive,
688 accepting_threshold = excluded.accepting_threshold,
689 processing_strategy = excluded.processing_strategy,
690 points_are_all_or_nothing = excluded.points_are_all_or_nothing,
691 reset_answer_if_zero_points_from_review = excluded.reset_answer_if_zero_points_from_review,
692 review_instructions = excluded.review_instructions,
693 deleted_at = NULL
694WHERE peer_or_self_review_configs.course_id = $2
695RETURNING id,
696 course_id,
697 exercise_id,
698 peer_reviews_to_give,
699 peer_reviews_to_receive,
700 accepting_threshold,
701 processing_strategy,
702 points_are_all_or_nothing,
703 review_instructions,
704 reset_answer_if_zero_points_from_review
705 "#,
706 input.id,
707 course_id,
708 input.exercise_id,
709 input.peer_reviews_to_give,
710 input.peer_reviews_to_receive,
711 input.accepting_threshold,
712 input.processing_strategy as _,
713 input.points_are_all_or_nothing,
714 input.review_instructions,
715 input.reset_answer_if_zero_points_from_review,
716 )
717 .fetch_optional(&mut *tx)
718 .await?;
719 let Some(peer_or_self_review_config) = peer_or_self_review_config else {
720 return Err(model_err!(
721 PreconditionFailed,
722 "Peer review config exercise does not belong to the expected course".to_string()
723 ));
724 };
725
726 let previous_peer_or_self_review_question_ids =
727 delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids(
728 &mut tx,
729 &[peer_or_self_review_config.id],
730 )
731 .await?;
732 let peer_or_self_review_questions = upsert_multiple_peer_or_self_review_questions(
733 &mut tx,
734 &peer_or_self_review_configuration
735 .peer_or_self_review_questions
736 .iter()
737 .map(|prq| {
738 let id = if previous_peer_or_self_review_question_ids.contains(&prq.id) {
739 prq.id
740 } else {
741 Uuid::new_v4()
742 };
743 CmsPeerOrSelfReviewQuestion { id, ..prq.clone() }
744 })
745 .collect::<Vec<_>>(),
746 )
747 .await?;
748
749 let updated_at = get_by_id(&mut tx, peer_or_self_review_config.id)
750 .await?
751 .updated_at;
752 tx.commit().await?;
753
754 Ok(VersionedCmsPeerOrSelfReviewConfiguration {
755 configuration: CmsPeerOrSelfReviewConfiguration {
756 peer_or_self_review_config,
757 peer_or_self_review_questions,
758 },
759 updated_at,
760 })
761}
762
763pub async fn get_course_default_cms_peer_review_configuration(
765 conn: &mut PgConnection,
766 course_id: Uuid,
767) -> ModelResult<Option<CmsPeerOrSelfReviewConfiguration>> {
768 let mut tx = conn.begin().await?;
769 let Some(peer_or_self_review_config) = get_course_default_cms_peer_review(&mut tx, course_id)
770 .await
771 .optional()?
772 else {
773 tx.rollback().await?;
774 return Ok(None);
775 };
776 let peer_or_self_review_questions =
777 crate::peer_or_self_review_questions::get_course_default_cms_peer_or_self_review_questions(
778 &mut tx,
779 peer_or_self_review_config.id,
780 )
781 .await?;
782 tx.commit().await?;
783 Ok(Some(CmsPeerOrSelfReviewConfiguration {
784 peer_or_self_review_config,
785 peer_or_self_review_questions,
786 }))
787}
788
789pub async fn get_course_default_versioned_cms_peer_review(
791 conn: &mut PgConnection,
792 course_id: Uuid,
793) -> ModelResult<VersionedCmsPeerOrSelfReviewConfiguration> {
794 let mut tx = conn.begin().await?;
795 let configuration = get_course_default_cms_peer_review_configuration(&mut tx, course_id)
796 .await?
797 .ok_or_else(missing_model_error(
798 ModelErrorType::RecordNotFound,
799 "The course has no default peer review config",
800 ))?;
801 let updated_at = get_by_id(&mut tx, configuration.peer_or_self_review_config.id)
802 .await?
803 .updated_at;
804 tx.commit().await?;
805 Ok(VersionedCmsPeerOrSelfReviewConfiguration {
806 configuration,
807 updated_at,
808 })
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use crate::test_helper::*;
815
816 #[tokio::test]
817 async fn only_one_default_peer_review_per_course() {
818 insert_data!(:tx, :user, :org, :course);
819
820 let peer_review_1 = insert(tx.as_mut(), PKeyPolicy::Generate, course, None).await;
821 assert!(peer_review_1.is_err());
822 }
823}