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
142/// A `moocfi/chart`. `spec` is the Vega-Lite JSON specification as a string; data is
143/// referenced by URL from within the spec (see the chart block editor).
144/// `data_file_url` is the uploaded file the spec reads from, or `None` for a chart without one.
145pub fn chart_block(
146    block: Uuid,
147    spec: &str,
148    caption: &str,
149    height: i32,
150    height_is_auto: bool,
151    data_file_url: Option<&str>,
152) -> GutenbergBlock {
153    let mut attributes = attributes! {
154        "spec": spec,
155        "caption": caption,
156        "height": height,
157        "heightIsAuto": height_is_auto,
158    };
159    if let Some(url) = data_file_url {
160        attributes.insert("dataFileUrl".into(), serde_json::json!(url));
161    }
162    GutenbergBlock {
163        client_id: block,
164        name: "moocfi/chart".to_string(),
165        is_valid: true,
166        attributes,
167        inner_blocks: vec![],
168    }
169}
170
171pub fn heading(content: &str, client_id: Uuid, level: i32) -> GutenbergBlock {
172    GutenbergBlock {
173        name: "core/heading".to_string(),
174        is_valid: true,
175        client_id,
176        attributes: attributes! {
177            "content": content,
178            "level": level,
179        },
180        inner_blocks: vec![],
181    }
182}
183
184pub fn list(block: Uuid, ordered: bool, inner_blocks: Vec<GutenbergBlock>) -> GutenbergBlock {
185    GutenbergBlock {
186        name: "core/list".to_string(),
187        client_id: block,
188        is_valid: true,
189        attributes: attributes! {
190            "ordered": ordered,
191        },
192        inner_blocks,
193    }
194}
195
196pub fn list_item(block: Uuid, content: &str) -> GutenbergBlock {
197    GutenbergBlock {
198        name: "core/list-item".to_string(),
199        client_id: block,
200        is_valid: true,
201        attributes: attributes! {
202            "content": content,
203        },
204        inner_blocks: vec![],
205    }
206}
207
208#[derive(Clone, Copy)]
209pub struct CommonExerciseData {
210    pub exercise_id: Uuid,
211    pub exercise_slide_id: Uuid,
212    pub exercise_task_id: Uuid,
213    pub block_id: Uuid,
214}
215
216pub fn create_best_exercise(
217    paragraph_id: Uuid,
218    spec_1: Uuid,
219    spec_2: Uuid,
220    spec_3: Uuid,
221    exercise_name: Option<String>,
222    exercise_data: CommonExerciseData,
223) -> (
224    GutenbergBlock,
225    CmsPageExercise,
226    CmsPageExerciseSlide,
227    CmsPageExerciseTask,
228) {
229    let CommonExerciseData {
230        exercise_id,
231        exercise_slide_id,
232        exercise_task_id,
233        block_id,
234    } = exercise_data;
235    let (exercise_block, exercise, mut slides, mut tasks) =
236        example_exercise_flexible(ExampleExerciseFlexibleParams {
237            exercise_id,
238            exercise_name: exercise_name.unwrap_or_else(|| "Best exercise".to_string()),
239            exercise_slides: vec![(
240                exercise_slide_id,
241                vec![(
242                    exercise_task_id,
243                    "example-exercise".to_string(),
244                    serde_json::json!([paragraph("Answer this question.", paragraph_id)]),
245                    serde_json::json!([
246                        {
247                            "name": "a",
248                            "correct": false,
249                            "id": spec_1,
250                        },
251                        {
252                            "name": "b",
253                            "correct": true,
254                            "id": spec_2,
255                        },
256                        {
257                            "name": "c",
258                            "correct": true,
259                            "id": spec_3,
260                        },
261                    ]),
262                )],
263            )],
264            client_id: block_id,
265            needs_peer_review: Some(false),
266            peer_or_self_review_config: None,
267            peer_or_self_review_questions: None,
268            teacher_reviews_answer_after_locking: true,
269        });
270    (
271        exercise_block,
272        exercise,
273        slides.swap_remove(0),
274        tasks.swap_remove(0),
275    )
276}
277
278#[allow(clippy::type_complexity)]
279pub struct ExampleExerciseFlexibleParams {
280    pub exercise_id: Uuid,
281    pub exercise_name: String,
282    pub exercise_slides: Vec<(Uuid, Vec<(Uuid, String, Value, Value)>)>,
283    pub client_id: Uuid,
284    pub needs_peer_review: Option<bool>,
285    pub peer_or_self_review_config: Option<CmsPeerOrSelfReviewConfig>,
286    pub peer_or_self_review_questions: Option<Vec<CmsPeerOrSelfReviewQuestion>>,
287    pub teacher_reviews_answer_after_locking: bool,
288}
289
290#[allow(clippy::type_complexity)]
291pub fn example_exercise_flexible(
292    params: ExampleExerciseFlexibleParams,
293) -> (
294    GutenbergBlock,
295    CmsPageExercise,
296    Vec<CmsPageExerciseSlide>,
297    Vec<CmsPageExerciseTask>,
298) {
299    let ExampleExerciseFlexibleParams {
300        exercise_id,
301        exercise_name,
302        exercise_slides,
303        client_id,
304        needs_peer_review,
305        peer_or_self_review_config,
306        peer_or_self_review_questions,
307        teacher_reviews_answer_after_locking,
308    } = params;
309    let block = GutenbergBlock {
310        client_id,
311        name: "moocfi/exercise".to_string(),
312        is_valid: true,
313        attributes: attributes! {
314            "id": exercise_id,
315            "name": exercise_name,
316            "dropCap": false,
317        },
318        inner_blocks: vec![],
319    };
320    let slides: Vec<CmsPageExerciseSlide> = exercise_slides
321        .iter()
322        .map(|(slide_id, _)| CmsPageExerciseSlide {
323            id: *slide_id,
324            exercise_id,
325            order_number: 1,
326        })
327        .collect();
328    let tasks: Vec<CmsPageExerciseTask> = exercise_slides
329        .into_iter()
330        .flat_map(|(slide_id, tasks)| {
331            tasks.into_iter().enumerate().map(
332                move |(order_number, (task_id, task_type, assignment, spec))| {
333                    (
334                        slide_id,
335                        task_id,
336                        task_type,
337                        assignment,
338                        spec,
339                        order_number as i32,
340                    )
341                },
342            )
343        })
344        .map(
345            |(slide_id, task_id, exercise_type, assignment, spec, order_number)| {
346                CmsPageExerciseTask {
347                    id: task_id,
348                    exercise_slide_id: slide_id,
349                    assignment,
350                    exercise_type,
351                    private_spec: Some(spec),
352                    order_number,
353                    private_spec_files: vec![],
354                }
355            },
356        )
357        .collect();
358
359    let exercise = CmsPageExercise {
360        id: exercise_id,
361        name: exercise_name,
362        order_number: 0,
363        score_maximum: tasks.len() as i32,
364        max_tries_per_slide: None,
365        limit_number_of_tries: false,
366        deadline: None,
367        needs_peer_review: needs_peer_review.unwrap_or(false),
368        needs_self_review: false,
369        use_course_default_peer_or_self_review_config: false,
370        teacher_reviews_answer_after_locking,
371        peer_or_self_review_config,
372        peer_or_self_review_questions,
373    };
374    (block, exercise, slides, tasks)
375}
376
377pub fn quizzes_exercise(
378    name: String,
379    paragraph_id: Uuid,
380    needs_peer_review: bool,
381    private_spec: serde_json::Value,
382    deadline: Option<DateTime<Utc>>,
383    exercise_data: CommonExerciseData,
384) -> (
385    GutenbergBlock,
386    CmsPageExercise,
387    CmsPageExerciseSlide,
388    CmsPageExerciseTask,
389) {
390    let CommonExerciseData {
391        exercise_id,
392        exercise_slide_id,
393        exercise_task_id,
394        block_id,
395    } = exercise_data;
396    let block = GutenbergBlock {
397        client_id: block_id,
398        name: "moocfi/exercise".to_string(),
399        is_valid: true,
400        attributes: attributes! {
401            "id": exercise_id,
402            "name": name,
403            "dropCap": false,
404        },
405        inner_blocks: vec![],
406    };
407    let exercise = CmsPageExercise {
408        id: exercise_id,
409        name,
410        order_number: 1,
411        score_maximum: 1,
412        max_tries_per_slide: None,
413        limit_number_of_tries: false,
414        deadline,
415        needs_peer_review,
416        needs_self_review: false,
417        use_course_default_peer_or_self_review_config: true,
418        teacher_reviews_answer_after_locking: true,
419        peer_or_self_review_config: None,
420        peer_or_self_review_questions: None,
421    };
422    let exercise_slide = CmsPageExerciseSlide {
423        id: exercise_slide_id,
424        exercise_id,
425        order_number: 1,
426    };
427    let exercise_task = CmsPageExerciseTask {
428        id: exercise_task_id,
429        exercise_slide_id,
430        assignment: serde_json::json!([paragraph("Answer this question.", paragraph_id)]),
431        exercise_type: "quizzes".to_string(),
432        private_spec: Some(serde_json::json!(private_spec)),
433        order_number: 0,
434        private_spec_files: vec![],
435    };
436    (block, exercise, exercise_slide, exercise_task)
437}
438
439#[allow(clippy::too_many_arguments)]
440pub fn tmc_exercise(
441    name: String,
442    exercise_id: Uuid,
443    exercise_slide_id: Uuid,
444    exercise_task_id: Uuid,
445    block_id: Uuid,
446    paragraph_id: Uuid,
447    needs_peer_review: bool,
448    private_spec: serde_json::Value,
449    deadline: Option<DateTime<Utc>>,
450) -> (
451    GutenbergBlock,
452    CmsPageExercise,
453    CmsPageExerciseSlide,
454    CmsPageExerciseTask,
455) {
456    let block = GutenbergBlock {
457        client_id: block_id,
458        name: "moocfi/exercise".to_string(),
459        is_valid: true,
460        attributes: attributes! {
461            "id": exercise_id,
462            "name": name,
463            "dropCap": false,
464        },
465        inner_blocks: vec![],
466    };
467    let exercise = CmsPageExercise {
468        id: exercise_id,
469        name,
470        order_number: 1,
471        score_maximum: 1,
472        max_tries_per_slide: None,
473        limit_number_of_tries: false,
474        deadline,
475        needs_peer_review,
476        needs_self_review: false,
477        use_course_default_peer_or_self_review_config: true,
478        teacher_reviews_answer_after_locking: true,
479        peer_or_self_review_config: None,
480        peer_or_self_review_questions: None,
481    };
482    let exercise_slide = CmsPageExerciseSlide {
483        id: exercise_slide_id,
484        exercise_id,
485        order_number: 1,
486    };
487    let exercise_task = CmsPageExerciseTask {
488        id: exercise_task_id,
489        exercise_slide_id,
490        assignment: serde_json::json!([paragraph("Write an `add` function.", paragraph_id)]),
491        exercise_type: "tmc".to_string(),
492        private_spec: Some(serde_json::json!(private_spec)),
493        order_number: 0,
494        private_spec_files: vec![],
495    };
496    (block, exercise, exercise_slide, exercise_task)
497}
498
499#[allow(clippy::too_many_arguments)]
500pub async fn submit_and_grade(
501    conn: &mut PgConnection,
502    id: &[u8],
503    exercise_id: Uuid,
504    exercise_slide_id: Uuid,
505    course_id: Uuid,
506    exercise_task_id: Uuid,
507    user_id: Uuid,
508    course_instance_id: Uuid,
509    spec: String,
510    out_of_100: f32,
511    file_store: &dyn FileStore,
512    app_conf: &ApplicationConfiguration,
513) -> Result<()> {
514    // combine the id with the user id to ensure it's unique
515    let id: Vec<u8> = [id, &user_id.as_bytes()[..]].concat();
516    let slide_submission = exercise_slide_submissions::insert_exercise_slide_submission_with_id(
517        conn,
518        Uuid::new_v4(),
519        &exercise_slide_submissions::NewExerciseSlideSubmission {
520            exercise_slide_id,
521            course_id: Some(course_id),
522            exam_id: None,
523            exercise_id,
524            user_id,
525            user_points_update_strategy: UserPointsUpdateStrategy::CanAddPointsAndCanRemovePoints,
526        },
527    )
528    .await?;
529    let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
530        conn,
531        user_id,
532        exercise_id,
533        Some(course_id),
534        None,
535    )
536    .await?;
537    // Set selected exercise slide
538    user_exercise_states::upsert_selected_exercise_slide_id(
539        conn,
540        user_id,
541        exercise_id,
542        Some(course_id),
543        None,
544        Some(exercise_slide_id),
545    )
546    .await?;
547    let user_exercise_slide_state = user_exercise_slide_states::get_or_insert_by_unique_index(
548        conn,
549        user_exercise_state.id,
550        exercise_slide_id,
551    )
552    .await?;
553    let task_submission_id = exercise_task_submissions::insert_with_id(
554        conn,
555        &exercise_task_submissions::SubmissionData {
556            id: Uuid::new_v5(&course_id, &id),
557            exercise_id,
558            course_id,
559            exercise_task_id,
560            exercise_slide_submission_id: slide_submission.id,
561            exercise_slide_id,
562            user_id,
563            course_instance_id,
564            answer: SubmittedAnswer::Json {
565                data: Value::String(spec),
566            },
567        },
568    )
569    .await?;
570
571    let task_submission =
572        exercise_task_submissions::get_by_id(conn, task_submission_id, file_store, app_conf)
573            .await?;
574    let exercise = exercises::get_by_id(conn, exercise_id).await?;
575    let grading = exercise_task_gradings::new_grading(conn, &exercise, &task_submission).await?;
576    let grading_result = ExerciseTaskGradingResult {
577        feedback_json: Some(serde_json::json!([{"SelectedOptioIsCorrect": true}])),
578        feedback_text: Some("Good job!".to_string()),
579        grading_progress: GradingProgress::FullyGraded,
580        score_given: out_of_100,
581        score_maximum: 100,
582        set_user_variables: Some(HashMap::new()),
583    };
584    headless_lms_models::library::grading::propagate_user_exercise_state_update_from_exercise_task_grading_result(
585        conn,
586        &exercise,
587        &grading,
588        &grading_result,
589        user_exercise_slide_state,
590        UserPointsUpdateStrategy::CanAddPointsButCannotRemovePoints,
591    )
592    .await
593    .unwrap();
594    Ok(())
595}
596
597#[allow(clippy::too_many_arguments)]
598pub async fn create_exam(
599    conn: &mut PgConnection,
600    name: String,
601    starts_at: Option<DateTime<Utc>>,
602    ends_at: Option<DateTime<Utc>>,
603    time_minutes: i32,
604    organization_id: Uuid,
605    course_id: Uuid,
606    exam_id: Uuid,
607    teacher: Uuid,
608    minimum_points_treshold: i32,
609    grade_manually: bool,
610) -> Result<Uuid> {
611    let new_exam_id = exams::insert(
612        conn,
613        PKeyPolicy::Fixed(exam_id),
614        &NewExam {
615            name,
616            starts_at,
617            ends_at,
618            time_minutes,
619            organization_id,
620            minimum_points_treshold,
621            grade_manually,
622        },
623    )
624    .await?;
625
626    let (exam_exercise_block_1, exam_exercise_1, exam_exercise_slide_1, exam_exercise_task_1) =
627        quizzes_exercise(
628            "Multiple choice with feedback".to_string(),
629            Uuid::new_v5(&course_id, b"eced4875-ece9-4c3d-ad0a-2443e61b3e78"),
630            false,
631            serde_json::from_str(include_str!(
632                "../../assets/quizzes-multiple-choice-feedback.json"
633            ))?,
634            None,
635            CommonExerciseData {
636                exercise_id: Uuid::new_v5(&course_id, b"b1b16970-60bc-426e-9537-b29bd2185db3"),
637                exercise_slide_id: Uuid::new_v5(
638                    &course_id,
639                    b"ea461a21-e0b4-4e09-a811-231f583b3dcb",
640                ),
641                exercise_task_id: Uuid::new_v5(&course_id, b"9d8ccf47-3e83-4459-8f2f-8e546a75f372"),
642                block_id: Uuid::new_v5(&course_id, b"a4edb4e5-507d-43f1-8058-9d95941dbf09"),
643            },
644        );
645    let (exam_exercise_block_2, exam_exercise_2, exam_exercise_slide_2, exam_exercise_task_2) =
646        create_best_exercise(
647            Uuid::new_v5(&course_id, b"fe5bb5a9-d0ab-4072-abe1-119c9c1e4f4a"),
648            Uuid::new_v5(&course_id, b"22959aad-26fc-4212-8259-c128cdab8b08"),
649            Uuid::new_v5(&course_id, b"d8ba9e92-4530-4a74-9b11-eb708fa54d40"),
650            Uuid::new_v5(&course_id, b"846f4895-f573-41e2-9926-cd700723ac18"),
651            Some("Best exercise".to_string()),
652            CommonExerciseData {
653                exercise_id: Uuid::new_v5(&course_id, b"44f472e5-b726-4c50-89a1-93f4170673f5"),
654                exercise_slide_id: Uuid::new_v5(
655                    &course_id,
656                    b"23182b3d-fbf4-4c0d-93fa-e9ddc199cc52",
657                ),
658                exercise_task_id: Uuid::new_v5(&course_id, b"ca105826-5007-439f-87be-c25f9c79506e"),
659                block_id: Uuid::new_v5(&course_id, b"96a9e586-cf88-4cb2-b7c9-efc2bc47e90b"),
660            },
661        );
662    pages::insert_page(
663        conn,
664        NewPage {
665            exercises: vec![exam_exercise_1, exam_exercise_2],
666            exercise_slides: vec![exam_exercise_slide_1, exam_exercise_slide_2],
667            exercise_tasks: vec![exam_exercise_task_1, exam_exercise_task_2],
668            content: vec![
669                heading(
670                    "The exam",
671                    Uuid::parse_str("d6cf16ce-fe78-4e57-8399-e8b63d7fddac").unwrap(),
672                    1,
673                ),
674                paragraph(
675                    "In this exam you're supposed to answer to two easy questions. Good luck!",
676                    Uuid::parse_str("474d4f21-798b-4ba0-b39f-120b134e7fa0").unwrap(),
677                ),
678                exam_exercise_block_1,
679                exam_exercise_block_2,
680            ],
681            url_path: "".to_string(),
682            title: "".to_string(),
683            course_id: None,
684            exam_id: Some(new_exam_id),
685            chapter_id: None,
686            front_page_of_chapter_id: None,
687            content_search_language: None,
688            hidden: false,
689        },
690        teacher,
691        get_seed_spec_fetcher(),
692        models_requests::fetch_service_info,
693    )
694    .await?;
695    course_exams::upsert(conn, new_exam_id, course_id).await?;
696    Ok(new_exam_id)
697}
698
699#[allow(clippy::too_many_arguments)]
700pub async fn create_best_peer_review(
701    conn: &mut PgConnection,
702    course_id: Uuid,
703    exercise_id: Uuid,
704    processing_strategy: peer_or_self_review_configs::PeerReviewProcessingStrategy,
705    accepting_threshold: f32,
706    points_are_all_or_nothing: bool,
707    peer_reviews_to_give: i32,
708    peer_reviews_to_receive: i32,
709) -> Result<()> {
710    let prc = peer_or_self_review_configs::upsert_with_id(
711        conn,
712        PKeyPolicy::Generate,
713        &CmsPeerOrSelfReviewConfig {
714            id: Uuid::new_v4(),
715            course_id,
716            exercise_id: Some(exercise_id),
717            peer_reviews_to_give,
718            peer_reviews_to_receive,
719            accepting_threshold,
720            processing_strategy,
721            points_are_all_or_nothing,
722            review_instructions: None,
723            reset_answer_if_zero_points_from_review: false,
724        },
725    )
726    .await?;
727
728    peer_or_self_review_questions::insert(
729        conn,
730        PKeyPolicy::Generate,
731        &CmsPeerOrSelfReviewQuestion {
732            id: Uuid::new_v4(),
733            peer_or_self_review_config_id: prc.id,
734            order_number: 0,
735            question: "What are your thoughts on the answer".to_string(),
736            question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay,
737            answer_required: true,
738            weight: 0.0,
739        },
740    )
741    .await?;
742
743    peer_or_self_review_questions::insert(
744        conn,
745        PKeyPolicy::Generate,
746        &CmsPeerOrSelfReviewQuestion {
747            id: Uuid::new_v4(),
748            peer_or_self_review_config_id: prc.id,
749            order_number: 1,
750            question: "Was the answer correct?".to_string(),
751            question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
752            answer_required: true,
753            weight: 0.0,
754        },
755    )
756    .await?;
757
758    peer_or_self_review_questions::insert(
759        conn,
760        PKeyPolicy::Generate,
761        &CmsPeerOrSelfReviewQuestion {
762            id: Uuid::new_v4(),
763            peer_or_self_review_config_id: prc.id,
764            order_number: 2,
765            question: "Was the answer good?".to_string(),
766            question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
767            answer_required: true,
768            weight: 0.0,
769        },
770    )
771    .await?;
772
773    exercises::set_exercise_to_use_exercise_specific_peer_or_self_review_config(
774        conn,
775        exercise_id,
776        true,
777        false,
778        false,
779    )
780    .await?;
781    Ok(())
782}