Skip to main content

headless_lms_server/programs/seed/
seed_helpers.rs

1use anyhow::{Result, anyhow};
2use chrono::{DateTime, Utc};
3use headless_lms_base::config::ApplicationConfiguration;
4use headless_lms_models::{
5    PKeyPolicy, SpecFetcher, course_exams,
6    exams::{self, NewExam},
7    exercise_slide_submissions,
8    exercise_task_gradings::{self, ExerciseTaskGradingResult, UserPointsUpdateStrategy},
9    exercise_task_submissions,
10    exercises::{self, GradingProgress},
11    library::grading::SubmittedAnswer,
12    page_history::HistoryChangeReason,
13    pages::{
14        self, CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask, CmsPageUpdate, NewPage,
15        PageUpdateArgs,
16    },
17    peer_or_self_review_configs::{self, CmsPeerOrSelfReviewConfig},
18    peer_or_self_review_questions::{self, CmsPeerOrSelfReviewQuestion},
19    user_exercise_slide_states, user_exercise_states,
20};
21use headless_lms_utils::{
22    attributes,
23    document_schema_processor::{GutenbergBlock, validate_unique_client_ids},
24    file_store::FileStore,
25};
26use once_cell::sync::OnceCell;
27use serde_json::Value;
28use sqlx::PgConnection;
29use std::sync::Arc;
30use std::{collections::HashMap, vec};
31use uuid::Uuid;
32
33use crate::domain::models_requests::{self, JwtKey};
34
35// Static holder for our cached spec fetcher
36static SEED_SPEC_FETCHER: OnceCell<Box<dyn SpecFetcher + Send + Sync>> = OnceCell::new();
37
38/// Initialize the global spec fetcher for seeding. Must be called once before any seeding operations.
39pub fn init_seed_spec_fetcher(base_url: String, jwt_key: Arc<JwtKey>) -> Result<()> {
40    let fetcher = Box::new(models_requests::make_seed_spec_fetcher_with_cache(
41        base_url,
42        Uuid::new_v4(),
43        jwt_key,
44    ));
45
46    SEED_SPEC_FETCHER
47        .set(fetcher)
48        .map_err(|_| anyhow!("Seed spec fetcher already initialized"))?;
49    Ok(())
50}
51
52/// Get the global spec fetcher for seeding. Panics if not initialized.
53pub fn get_seed_spec_fetcher() -> &'static (dyn SpecFetcher + Send + Sync) {
54    // We can use unwrap here since this is a fatal error during seeding
55    SEED_SPEC_FETCHER
56        .get()
57        .unwrap_or_else(|| panic!("Seed spec fetcher not initialized"))
58}
59
60pub async fn create_page(
61    conn: &mut PgConnection,
62    course_id: Uuid,
63    author: Uuid,
64    chapter_id: Option<Uuid>,
65    page_data: CmsPageUpdate,
66) -> Result<Uuid> {
67    validate_unique_client_ids(page_data.content.clone())?;
68    let new_page = NewPage {
69        content: vec![],
70        url_path: page_data.url_path.to_string(),
71        title: format!("{} WIP", page_data.title),
72        course_id: Some(course_id),
73        exam_id: None,
74        chapter_id,
75        front_page_of_chapter_id: None,
76        exercises: vec![],
77        exercise_slides: vec![],
78        exercise_tasks: vec![],
79        content_search_language: None,
80        hidden: page_data.hidden,
81    };
82    let page = pages::insert_page(
83        conn,
84        new_page,
85        author,
86        get_seed_spec_fetcher(),
87        models_requests::fetch_service_info,
88    )
89    .await?;
90    pages::update_page(
91        conn,
92        PageUpdateArgs {
93            page_id: page.id,
94            author,
95            cms_page_update: CmsPageUpdate {
96                content: page_data.content,
97                exercises: page_data.exercises,
98                exercise_slides: page_data.exercise_slides,
99                exercise_tasks: page_data.exercise_tasks,
100                url_path: page_data.url_path,
101                title: page_data.title,
102                chapter_id,
103                hidden: page_data.hidden,
104            },
105            retain_ids: true,
106            history_change_reason: HistoryChangeReason::PageSaved,
107            is_exam_page: false,
108        },
109        get_seed_spec_fetcher(),
110        models_requests::fetch_service_info,
111    )
112    .await?;
113    Ok(page.id)
114}
115
116pub fn paragraph(content: &str, block: Uuid) -> GutenbergBlock {
117    GutenbergBlock {
118        name: "core/paragraph".to_string(),
119        is_valid: true,
120        client_id: block,
121        attributes: attributes! {
122            "content": content,
123            "dropCap": false,
124        },
125        inner_blocks: vec![],
126    }
127}
128
129pub fn chatbot_block(block: Uuid, chatbot_conf_id: Uuid, course_id: Uuid) -> GutenbergBlock {
130    GutenbergBlock {
131        client_id: block,
132        name: "moocfi/chatbot".to_string(),
133        is_valid: true,
134        attributes: attributes! {
135            "chatbotConfigurationId": chatbot_conf_id,
136            "courseId": course_id,
137        },
138        inner_blocks: vec![],
139    }
140}
141
142pub fn heading(content: &str, client_id: Uuid, level: i32) -> GutenbergBlock {
143    GutenbergBlock {
144        name: "core/heading".to_string(),
145        is_valid: true,
146        client_id,
147        attributes: attributes! {
148            "content": content,
149            "level": level,
150        },
151        inner_blocks: vec![],
152    }
153}
154
155pub fn list(block: Uuid, ordered: bool, inner_blocks: Vec<GutenbergBlock>) -> GutenbergBlock {
156    GutenbergBlock {
157        name: "core/list".to_string(),
158        client_id: block,
159        is_valid: true,
160        attributes: attributes! {
161            "ordered": ordered,
162        },
163        inner_blocks,
164    }
165}
166
167pub fn list_item(block: Uuid, content: &str) -> GutenbergBlock {
168    GutenbergBlock {
169        name: "core/list-item".to_string(),
170        client_id: block,
171        is_valid: true,
172        attributes: attributes! {
173            "content": content,
174        },
175        inner_blocks: vec![],
176    }
177}
178
179#[derive(Clone, Copy)]
180pub struct CommonExerciseData {
181    pub exercise_id: Uuid,
182    pub exercise_slide_id: Uuid,
183    pub exercise_task_id: Uuid,
184    pub block_id: Uuid,
185}
186
187pub fn create_best_exercise(
188    paragraph_id: Uuid,
189    spec_1: Uuid,
190    spec_2: Uuid,
191    spec_3: Uuid,
192    exercise_name: Option<String>,
193    exercise_data: CommonExerciseData,
194) -> (
195    GutenbergBlock,
196    CmsPageExercise,
197    CmsPageExerciseSlide,
198    CmsPageExerciseTask,
199) {
200    let CommonExerciseData {
201        exercise_id,
202        exercise_slide_id,
203        exercise_task_id,
204        block_id,
205    } = exercise_data;
206    let (exercise_block, exercise, mut slides, mut tasks) =
207        example_exercise_flexible(ExampleExerciseFlexibleParams {
208            exercise_id,
209            exercise_name: exercise_name.unwrap_or_else(|| "Best exercise".to_string()),
210            exercise_slides: vec![(
211                exercise_slide_id,
212                vec![(
213                    exercise_task_id,
214                    "example-exercise".to_string(),
215                    serde_json::json!([paragraph("Answer this question.", paragraph_id)]),
216                    serde_json::json!([
217                        {
218                            "name": "a",
219                            "correct": false,
220                            "id": spec_1,
221                        },
222                        {
223                            "name": "b",
224                            "correct": true,
225                            "id": spec_2,
226                        },
227                        {
228                            "name": "c",
229                            "correct": true,
230                            "id": spec_3,
231                        },
232                    ]),
233                )],
234            )],
235            client_id: block_id,
236            needs_peer_review: Some(false),
237            peer_or_self_review_config: None,
238            peer_or_self_review_questions: None,
239            teacher_reviews_answer_after_locking: true,
240        });
241    (
242        exercise_block,
243        exercise,
244        slides.swap_remove(0),
245        tasks.swap_remove(0),
246    )
247}
248
249#[allow(clippy::type_complexity)]
250pub struct ExampleExerciseFlexibleParams {
251    pub exercise_id: Uuid,
252    pub exercise_name: String,
253    pub exercise_slides: Vec<(Uuid, Vec<(Uuid, String, Value, Value)>)>,
254    pub client_id: Uuid,
255    pub needs_peer_review: Option<bool>,
256    pub peer_or_self_review_config: Option<CmsPeerOrSelfReviewConfig>,
257    pub peer_or_self_review_questions: Option<Vec<CmsPeerOrSelfReviewQuestion>>,
258    pub teacher_reviews_answer_after_locking: bool,
259}
260
261#[allow(clippy::type_complexity)]
262pub fn example_exercise_flexible(
263    params: ExampleExerciseFlexibleParams,
264) -> (
265    GutenbergBlock,
266    CmsPageExercise,
267    Vec<CmsPageExerciseSlide>,
268    Vec<CmsPageExerciseTask>,
269) {
270    let ExampleExerciseFlexibleParams {
271        exercise_id,
272        exercise_name,
273        exercise_slides,
274        client_id,
275        needs_peer_review,
276        peer_or_self_review_config,
277        peer_or_self_review_questions,
278        teacher_reviews_answer_after_locking,
279    } = params;
280    let block = GutenbergBlock {
281        client_id,
282        name: "moocfi/exercise".to_string(),
283        is_valid: true,
284        attributes: attributes! {
285            "id": exercise_id,
286            "name": exercise_name,
287            "dropCap": false,
288        },
289        inner_blocks: vec![],
290    };
291    let slides: Vec<CmsPageExerciseSlide> = exercise_slides
292        .iter()
293        .map(|(slide_id, _)| CmsPageExerciseSlide {
294            id: *slide_id,
295            exercise_id,
296            order_number: 1,
297        })
298        .collect();
299    let tasks: Vec<CmsPageExerciseTask> = exercise_slides
300        .into_iter()
301        .flat_map(|(slide_id, tasks)| {
302            tasks.into_iter().enumerate().map(
303                move |(order_number, (task_id, task_type, assignment, spec))| {
304                    (
305                        slide_id,
306                        task_id,
307                        task_type,
308                        assignment,
309                        spec,
310                        order_number as i32,
311                    )
312                },
313            )
314        })
315        .map(
316            |(slide_id, task_id, exercise_type, assignment, spec, order_number)| {
317                CmsPageExerciseTask {
318                    id: task_id,
319                    exercise_slide_id: slide_id,
320                    assignment,
321                    exercise_type,
322                    private_spec: Some(spec),
323                    order_number,
324                    private_spec_files: vec![],
325                }
326            },
327        )
328        .collect();
329
330    let exercise = CmsPageExercise {
331        id: exercise_id,
332        name: exercise_name,
333        order_number: 0,
334        score_maximum: tasks.len() as i32,
335        max_tries_per_slide: None,
336        limit_number_of_tries: false,
337        deadline: None,
338        needs_peer_review: needs_peer_review.unwrap_or(false),
339        needs_self_review: false,
340        use_course_default_peer_or_self_review_config: false,
341        teacher_reviews_answer_after_locking,
342        peer_or_self_review_config,
343        peer_or_self_review_questions,
344    };
345    (block, exercise, slides, tasks)
346}
347
348pub fn quizzes_exercise(
349    name: String,
350    paragraph_id: Uuid,
351    needs_peer_review: bool,
352    private_spec: serde_json::Value,
353    deadline: Option<DateTime<Utc>>,
354    exercise_data: CommonExerciseData,
355) -> (
356    GutenbergBlock,
357    CmsPageExercise,
358    CmsPageExerciseSlide,
359    CmsPageExerciseTask,
360) {
361    let CommonExerciseData {
362        exercise_id,
363        exercise_slide_id,
364        exercise_task_id,
365        block_id,
366    } = exercise_data;
367    let block = GutenbergBlock {
368        client_id: block_id,
369        name: "moocfi/exercise".to_string(),
370        is_valid: true,
371        attributes: attributes! {
372            "id": exercise_id,
373            "name": name,
374            "dropCap": false,
375        },
376        inner_blocks: vec![],
377    };
378    let exercise = CmsPageExercise {
379        id: exercise_id,
380        name,
381        order_number: 1,
382        score_maximum: 1,
383        max_tries_per_slide: None,
384        limit_number_of_tries: false,
385        deadline,
386        needs_peer_review,
387        needs_self_review: false,
388        use_course_default_peer_or_self_review_config: true,
389        teacher_reviews_answer_after_locking: true,
390        peer_or_self_review_config: None,
391        peer_or_self_review_questions: None,
392    };
393    let exercise_slide = CmsPageExerciseSlide {
394        id: exercise_slide_id,
395        exercise_id,
396        order_number: 1,
397    };
398    let exercise_task = CmsPageExerciseTask {
399        id: exercise_task_id,
400        exercise_slide_id,
401        assignment: serde_json::json!([paragraph("Answer this question.", paragraph_id)]),
402        exercise_type: "quizzes".to_string(),
403        private_spec: Some(serde_json::json!(private_spec)),
404        order_number: 0,
405        private_spec_files: vec![],
406    };
407    (block, exercise, exercise_slide, exercise_task)
408}
409
410#[allow(clippy::too_many_arguments)]
411pub fn tmc_exercise(
412    name: String,
413    exercise_id: Uuid,
414    exercise_slide_id: Uuid,
415    exercise_task_id: Uuid,
416    block_id: Uuid,
417    paragraph_id: Uuid,
418    needs_peer_review: bool,
419    private_spec: serde_json::Value,
420    deadline: Option<DateTime<Utc>>,
421) -> (
422    GutenbergBlock,
423    CmsPageExercise,
424    CmsPageExerciseSlide,
425    CmsPageExerciseTask,
426) {
427    let block = GutenbergBlock {
428        client_id: block_id,
429        name: "moocfi/exercise".to_string(),
430        is_valid: true,
431        attributes: attributes! {
432            "id": exercise_id,
433            "name": name,
434            "dropCap": false,
435        },
436        inner_blocks: vec![],
437    };
438    let exercise = CmsPageExercise {
439        id: exercise_id,
440        name,
441        order_number: 1,
442        score_maximum: 1,
443        max_tries_per_slide: None,
444        limit_number_of_tries: false,
445        deadline,
446        needs_peer_review,
447        needs_self_review: false,
448        use_course_default_peer_or_self_review_config: true,
449        teacher_reviews_answer_after_locking: true,
450        peer_or_self_review_config: None,
451        peer_or_self_review_questions: None,
452    };
453    let exercise_slide = CmsPageExerciseSlide {
454        id: exercise_slide_id,
455        exercise_id,
456        order_number: 1,
457    };
458    let exercise_task = CmsPageExerciseTask {
459        id: exercise_task_id,
460        exercise_slide_id,
461        assignment: serde_json::json!([paragraph("Write an `add` function.", paragraph_id)]),
462        exercise_type: "tmc".to_string(),
463        private_spec: Some(serde_json::json!(private_spec)),
464        order_number: 0,
465        private_spec_files: vec![],
466    };
467    (block, exercise, exercise_slide, exercise_task)
468}
469
470#[allow(clippy::too_many_arguments)]
471pub async fn submit_and_grade(
472    conn: &mut PgConnection,
473    id: &[u8],
474    exercise_id: Uuid,
475    exercise_slide_id: Uuid,
476    course_id: Uuid,
477    exercise_task_id: Uuid,
478    user_id: Uuid,
479    course_instance_id: Uuid,
480    spec: String,
481    out_of_100: f32,
482    file_store: &dyn FileStore,
483    app_conf: &ApplicationConfiguration,
484) -> Result<()> {
485    // combine the id with the user id to ensure it's unique
486    let id: Vec<u8> = [id, &user_id.as_bytes()[..]].concat();
487    let slide_submission = exercise_slide_submissions::insert_exercise_slide_submission_with_id(
488        conn,
489        Uuid::new_v4(),
490        &exercise_slide_submissions::NewExerciseSlideSubmission {
491            exercise_slide_id,
492            course_id: Some(course_id),
493            exam_id: None,
494            exercise_id,
495            user_id,
496            user_points_update_strategy: UserPointsUpdateStrategy::CanAddPointsAndCanRemovePoints,
497        },
498    )
499    .await?;
500    let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
501        conn,
502        user_id,
503        exercise_id,
504        Some(course_id),
505        None,
506    )
507    .await?;
508    // Set selected exercise slide
509    user_exercise_states::upsert_selected_exercise_slide_id(
510        conn,
511        user_id,
512        exercise_id,
513        Some(course_id),
514        None,
515        Some(exercise_slide_id),
516    )
517    .await?;
518    let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
519        conn,
520        user_exercise_state.id,
521        exercise_slide_id,
522    )
523    .await?;
524    let task_submission_id = exercise_task_submissions::insert_with_id(
525        conn,
526        &exercise_task_submissions::SubmissionData {
527            id: Uuid::new_v5(&course_id, &id),
528            exercise_id,
529            course_id,
530            exercise_task_id,
531            exercise_slide_submission_id: slide_submission.id,
532            exercise_slide_id,
533            user_id,
534            course_instance_id,
535            answer: SubmittedAnswer::Json {
536                data: Value::String(spec),
537            },
538        },
539    )
540    .await?;
541
542    let task_submission =
543        exercise_task_submissions::get_by_id(conn, task_submission_id, file_store, app_conf)
544            .await?;
545    let exercise = exercises::get_by_id(conn, exercise_id).await?;
546    let grading = exercise_task_gradings::new_grading(conn, &exercise, &task_submission).await?;
547    let grading_result = ExerciseTaskGradingResult {
548        feedback_json: Some(serde_json::json!([{"SelectedOptioIsCorrect": true}])),
549        feedback_text: Some("Good job!".to_string()),
550        grading_progress: GradingProgress::FullyGraded,
551        score_given: out_of_100,
552        score_maximum: 100,
553        set_user_variables: Some(HashMap::new()),
554    };
555    headless_lms_models::library::grading::propagate_user_exercise_state_update_from_exercise_task_grading_result(
556        conn,
557        &exercise,
558        &grading,
559        &grading_result,
560        user_exercise_slide_state,
561        UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
562    )
563    .await
564    .unwrap();
565    Ok(())
566}
567
568#[allow(clippy::too_many_arguments)]
569pub async fn create_exam(
570    conn: &mut PgConnection,
571    name: String,
572    starts_at: Option<DateTime<Utc>>,
573    ends_at: Option<DateTime<Utc>>,
574    time_minutes: i32,
575    organization_id: Uuid,
576    course_id: Uuid,
577    exam_id: Uuid,
578    teacher: Uuid,
579    minimum_points_treshold: i32,
580    grade_manually: bool,
581) -> Result<Uuid> {
582    let new_exam_id = exams::insert(
583        conn,
584        PKeyPolicy::Fixed(exam_id),
585        &NewExam {
586            name,
587            starts_at,
588            ends_at,
589            time_minutes,
590            organization_id,
591            minimum_points_treshold,
592            grade_manually,
593        },
594    )
595    .await?;
596
597    let (exam_exercise_block_1, exam_exercise_1, exam_exercise_slide_1, exam_exercise_task_1) =
598        quizzes_exercise(
599            "Multiple choice with feedback".to_string(),
600            Uuid::new_v5(&course_id, b"eced4875-ece9-4c3d-ad0a-2443e61b3e78"),
601            false,
602            serde_json::from_str(include_str!(
603                "../../assets/quizzes-multiple-choice-feedback.json"
604            ))?,
605            None,
606            CommonExerciseData {
607                exercise_id: Uuid::new_v5(&course_id, b"b1b16970-60bc-426e-9537-b29bd2185db3"),
608                exercise_slide_id: Uuid::new_v5(
609                    &course_id,
610                    b"ea461a21-e0b4-4e09-a811-231f583b3dcb",
611                ),
612                exercise_task_id: Uuid::new_v5(&course_id, b"9d8ccf47-3e83-4459-8f2f-8e546a75f372"),
613                block_id: Uuid::new_v5(&course_id, b"a4edb4e5-507d-43f1-8058-9d95941dbf09"),
614            },
615        );
616    let (exam_exercise_block_2, exam_exercise_2, exam_exercise_slide_2, exam_exercise_task_2) =
617        create_best_exercise(
618            Uuid::new_v5(&course_id, b"fe5bb5a9-d0ab-4072-abe1-119c9c1e4f4a"),
619            Uuid::new_v5(&course_id, b"22959aad-26fc-4212-8259-c128cdab8b08"),
620            Uuid::new_v5(&course_id, b"d8ba9e92-4530-4a74-9b11-eb708fa54d40"),
621            Uuid::new_v5(&course_id, b"846f4895-f573-41e2-9926-cd700723ac18"),
622            Some("Best exercise".to_string()),
623            CommonExerciseData {
624                exercise_id: Uuid::new_v5(&course_id, b"44f472e5-b726-4c50-89a1-93f4170673f5"),
625                exercise_slide_id: Uuid::new_v5(
626                    &course_id,
627                    b"23182b3d-fbf4-4c0d-93fa-e9ddc199cc52",
628                ),
629                exercise_task_id: Uuid::new_v5(&course_id, b"ca105826-5007-439f-87be-c25f9c79506e"),
630                block_id: Uuid::new_v5(&course_id, b"96a9e586-cf88-4cb2-b7c9-efc2bc47e90b"),
631            },
632        );
633    pages::insert_page(
634        conn,
635        NewPage {
636            exercises: vec![exam_exercise_1, exam_exercise_2],
637            exercise_slides: vec![exam_exercise_slide_1, exam_exercise_slide_2],
638            exercise_tasks: vec![exam_exercise_task_1, exam_exercise_task_2],
639            content: vec![
640                heading(
641                    "The exam",
642                    Uuid::parse_str("d6cf16ce-fe78-4e57-8399-e8b63d7fddac").unwrap(),
643                    1,
644                ),
645                paragraph(
646                    "In this exam you're supposed to answer to two easy questions. Good luck!",
647                    Uuid::parse_str("474d4f21-798b-4ba0-b39f-120b134e7fa0").unwrap(),
648                ),
649                exam_exercise_block_1,
650                exam_exercise_block_2,
651            ],
652            url_path: "".to_string(),
653            title: "".to_string(),
654            course_id: None,
655            exam_id: Some(new_exam_id),
656            chapter_id: None,
657            front_page_of_chapter_id: None,
658            content_search_language: None,
659            hidden: false,
660        },
661        teacher,
662        get_seed_spec_fetcher(),
663        models_requests::fetch_service_info,
664    )
665    .await?;
666    course_exams::upsert(conn, new_exam_id, course_id).await?;
667    Ok(new_exam_id)
668}
669
670#[allow(clippy::too_many_arguments)]
671pub async fn create_best_peer_review(
672    conn: &mut PgConnection,
673    course_id: Uuid,
674    exercise_id: Uuid,
675    processing_strategy: peer_or_self_review_configs::PeerReviewProcessingStrategy,
676    accepting_threshold: f32,
677    points_are_all_or_nothing: bool,
678    peer_reviews_to_give: i32,
679    peer_reviews_to_receive: i32,
680) -> Result<()> {
681    let prc = peer_or_self_review_configs::upsert_with_id(
682        conn,
683        PKeyPolicy::Generate,
684        &CmsPeerOrSelfReviewConfig {
685            id: Uuid::new_v4(),
686            course_id,
687            exercise_id: Some(exercise_id),
688            peer_reviews_to_give,
689            peer_reviews_to_receive,
690            accepting_threshold,
691            processing_strategy,
692            points_are_all_or_nothing,
693            review_instructions: None,
694            reset_answer_if_zero_points_from_review: false,
695        },
696    )
697    .await?;
698
699    peer_or_self_review_questions::insert(
700        conn,
701        PKeyPolicy::Generate,
702        &CmsPeerOrSelfReviewQuestion {
703            id: Uuid::new_v4(),
704            peer_or_self_review_config_id: prc.id,
705            order_number: 0,
706            question: "What are your thoughts on the answer".to_string(),
707            question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay,
708            answer_required: true,
709            weight: 0.0,
710        },
711    )
712    .await?;
713
714    peer_or_self_review_questions::insert(
715        conn,
716        PKeyPolicy::Generate,
717        &CmsPeerOrSelfReviewQuestion {
718            id: Uuid::new_v4(),
719            peer_or_self_review_config_id: prc.id,
720            order_number: 1,
721            question: "Was the answer correct?".to_string(),
722            question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
723            answer_required: true,
724            weight: 0.0,
725        },
726    )
727    .await?;
728
729    peer_or_self_review_questions::insert(
730        conn,
731        PKeyPolicy::Generate,
732        &CmsPeerOrSelfReviewQuestion {
733            id: Uuid::new_v4(),
734            peer_or_self_review_config_id: prc.id,
735            order_number: 2,
736            question: "Was the answer good?".to_string(),
737            question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
738            answer_required: true,
739            weight: 0.0,
740        },
741    )
742    .await?;
743
744    exercises::set_exercise_to_use_exercise_specific_peer_or_self_review_config(
745        conn,
746        exercise_id,
747        true,
748        false,
749        false,
750    )
751    .await?;
752    Ok(())
753}