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