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
35static SEED_SPEC_FETCHER: OnceCell<Box<dyn SpecFetcher + Send + Sync>> = OnceCell::new();
37
38pub 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
52pub fn get_seed_spec_fetcher() -> &'static (dyn SpecFetcher + Send + Sync) {
54 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 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 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}