Skip to main content

headless_lms_server/programs/seed/
seed_helpers.rs

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