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 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 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 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}