1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use crate::course_instances;
6use crate::course_instances::NewCourseInstance;
7use crate::course_language_groups;
8use crate::courses::Course;
9use crate::courses::NewCourse;
10use crate::courses::get_course;
11use crate::exams;
12use crate::exams::Exam;
13use crate::exams::NewExam;
14use crate::pages;
15use crate::prelude::*;
16
17use crate::ModelResult;
18
19pub async fn copy_course(
20 conn: &mut PgConnection,
21 course_id: Uuid,
22 new_course: &NewCourse,
23 same_language_group: bool,
24 user_id: Uuid,
25) -> ModelResult<Course> {
26 let mut tx = conn.begin().await?;
27 let parent_course = get_course(&mut tx, course_id).await?;
28 let course_language_group_id = if same_language_group {
29 parent_course.course_language_group_id
30 } else {
31 course_language_groups::insert(&mut tx, PKeyPolicy::Generate).await?
32 };
33
34 let copied_course = copy_course_with_language_group(
35 &mut tx,
36 course_id,
37 course_language_group_id,
38 new_course,
39 user_id,
40 )
41 .await?;
42
43 tx.commit().await?;
44
45 Ok(copied_course)
46}
47
48pub async fn copy_course_with_language_group(
49 conn: &mut PgConnection,
50 src_course_id: Uuid,
51 target_clg_id: Uuid,
52 new_course: &NewCourse,
53 user_id: Uuid,
54) -> ModelResult<Course> {
55 let parent_course = get_course(conn, src_course_id).await?;
56 let same_clg = target_clg_id == parent_course.course_language_group_id;
57
58 let mut tx = conn.begin().await?;
59
60 let copied_course = sqlx::query_as!(
61 Course,
62 r#"
63INSERT INTO courses (
64 name,
65 organization_id,
66 slug,
67 content_search_language,
68 language_code,
69 copied_from,
70 course_language_group_id,
71 is_draft,
72 base_module_completion_requires_n_submodule_completions,
73 can_add_chatbot,
74 is_unlisted,
75 is_joinable_by_code_only,
76 join_code,
77 ask_marketing_consent,
78 description,
79 flagged_answers_threshold
80 )
81VALUES (
82 $1,
83 $2,
84 $3,
85 $4,
86 $5,
87 $6,
88 $7,
89 $8,
90 $9,
91 $10,
92 $11,
93 $12,
94 $13,
95 $14,
96 $15,
97 $16
98 )
99RETURNING id,
100 name,
101 created_at,
102 updated_at,
103 organization_id,
104 deleted_at,
105 slug,
106 content_search_language::text,
107 language_code,
108 copied_from,
109 course_language_group_id,
110 description,
111 is_draft,
112 is_test_mode,
113 base_module_completion_requires_n_submodule_completions,
114 can_add_chatbot,
115 is_unlisted,
116 is_joinable_by_code_only,
117 join_code,
118 ask_marketing_consent,
119 flagged_answers_threshold,
120 closed_at,
121 closed_additional_message,
122 closed_course_successor_id
123 "#,
124 new_course.name,
125 new_course.organization_id,
126 new_course.slug,
127 parent_course.content_search_language as _,
128 new_course.language_code,
129 parent_course.id,
130 target_clg_id,
131 new_course.is_draft,
132 parent_course.base_module_completion_requires_n_submodule_completions,
133 parent_course.can_add_chatbot,
134 new_course.is_unlisted,
135 new_course.is_joinable_by_code_only,
136 new_course.join_code,
137 new_course.ask_marketing_consent,
138 parent_course.description,
139 parent_course.flagged_answers_threshold
140 )
141 .fetch_one(&mut *tx)
142 .await?;
143
144 copy_course_modules(&mut tx, copied_course.id, src_course_id).await?;
145 copy_course_chapters(&mut tx, copied_course.id, src_course_id).await?;
146
147 if new_course.copy_user_permissions {
148 copy_user_permissions(&mut tx, copied_course.id, src_course_id, user_id).await?;
149 }
150
151 let contents_iter =
152 copy_course_pages_and_return_contents(&mut tx, copied_course.id, src_course_id).await?;
153
154 set_chapter_front_pages(&mut tx, copied_course.id).await?;
155
156 let old_to_new_exercise_ids = map_old_exr_ids_to_new_exr_ids_for_courses(
157 &mut tx,
158 copied_course.id,
159 src_course_id,
160 target_clg_id,
161 same_clg,
162 )
163 .await?;
164
165 for (page_id, content) in contents_iter {
167 if let Value::Array(mut blocks) = content {
168 for block in blocks.iter_mut() {
169 if block["name"] != Value::String("moocfi/exercise".to_string()) {
170 continue;
171 }
172 if let Value::String(old_id) = &block["attributes"]["id"] {
173 let new_id = old_to_new_exercise_ids
174 .get(old_id)
175 .ok_or_else(|| {
176 ModelError::new(
177 ModelErrorType::Generic,
178 "Invalid exercise id in content.".to_string(),
179 None,
180 )
181 })?
182 .to_string();
183 block["attributes"]["id"] = Value::String(new_id);
184 }
185 }
186 sqlx::query!(
187 r#"
188UPDATE pages
189SET content = $1
190WHERE id = $2;
191"#,
192 Value::Array(blocks),
193 page_id
194 )
195 .execute(&mut *tx)
196 .await?;
197 }
198 }
199
200 let pages_contents = pages::get_all_by_course_id_and_visibility(
201 tx.as_mut(),
202 copied_course.id,
203 pages::PageVisibility::Any,
204 )
205 .await?
206 .into_iter()
207 .map(|page| (page.id, page.content))
208 .collect::<HashMap<_, _>>();
209
210 for (page_id, content) in pages_contents {
211 if let Value::Array(mut blocks) = content {
212 for block in blocks.iter_mut() {
213 if let Some(content) = block["attributes"]["content"].as_str() {
214 if content.contains("<a href=") {
215 block["attributes"]["content"] =
216 Value::String(content.replace(&parent_course.slug, &new_course.slug));
217 }
218 }
219 }
220 sqlx::query!(
221 r#"
222UPDATE pages
223SET content = $1
224WHERE id = $2;
225"#,
226 Value::Array(blocks),
227 page_id
228 )
229 .execute(&mut *tx)
230 .await?;
231 }
232 }
233
234 copy_exercise_slides(&mut tx, copied_course.id, src_course_id).await?;
235 copy_exercise_tasks(&mut tx, copied_course.id, src_course_id).await?;
236
237 course_instances::insert(
239 &mut tx,
240 PKeyPolicy::Generate,
241 NewCourseInstance {
242 course_id: copied_course.id,
243 name: None,
244 description: None,
245 support_email: None,
246 teacher_in_charge_name: &new_course.teacher_in_charge_name,
247 teacher_in_charge_email: &new_course.teacher_in_charge_email,
248 opening_time: None,
249 closing_time: None,
250 },
251 )
252 .await?;
253
254 copy_peer_or_self_review_configs(&mut tx, copied_course.id, src_course_id).await?;
255 copy_peer_or_self_review_questions(&mut tx, copied_course.id, src_course_id).await?;
256 copy_material_references(&mut tx, copied_course.id, src_course_id).await?;
257 copy_glossary_entries(&mut tx, copied_course.id, src_course_id).await?;
258
259 copy_certificate_configurations_and_requirements(&mut tx, copied_course.id, src_course_id)
261 .await?;
262 copy_chatbot_configurations(&mut tx, copied_course.id, src_course_id).await?;
263 copy_cheater_thresholds(&mut tx, copied_course.id, src_course_id).await?;
264 copy_course_custom_privacy_policy_checkbox_texts(&mut tx, copied_course.id, src_course_id)
265 .await?;
266 copy_exercise_repositories(&mut tx, copied_course.id, src_course_id).await?;
267 copy_partners_blocks(&mut tx, copied_course.id, src_course_id).await?;
268 copy_privacy_links(&mut tx, copied_course.id, src_course_id).await?;
269 copy_research_consent_forms_and_questions(&mut tx, copied_course.id, src_course_id).await?;
270
271 tx.commit().await?;
272
273 Ok(copied_course)
274}
275
276pub async fn copy_exam(
277 conn: &mut PgConnection,
278 parent_exam_id: &Uuid,
279 new_exam: &NewExam,
280) -> ModelResult<Exam> {
281 let mut tx = conn.begin().await?;
282 let copied_exam = copy_exam_content(&mut tx, parent_exam_id, new_exam, None).await?;
283 tx.commit().await?;
284 Ok(copied_exam)
285}
286
287async fn copy_exam_content(
288 tx: &mut PgConnection,
289 parent_exam_id: &Uuid,
290 new_exam: &NewExam,
291 new_exam_id: Option<Uuid>,
292) -> ModelResult<Exam> {
293 let parent_exam = exams::get(tx, *parent_exam_id).await?;
294
295 let parent_exam_fields = sqlx::query!(
296 "
297SELECT language,
298 organization_id,
299 minimum_points_treshold
300FROM exams
301WHERE id = $1
302 ",
303 parent_exam.id
304 )
305 .fetch_one(&mut *tx)
306 .await?;
307
308 let final_exam_id = new_exam_id.unwrap_or_else(Uuid::new_v4);
309
310 let copied_exam = sqlx::query!(
312 "
313INSERT INTO exams(
314 id,
315 name,
316 organization_id,
317 instructions,
318 starts_at,
319 ends_at,
320 language,
321 time_minutes,
322 minimum_points_treshold,
323 grade_manually
324 )
325VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
326RETURNING *
327 ",
328 final_exam_id,
329 new_exam.name,
330 parent_exam_fields.organization_id,
331 parent_exam.instructions,
332 new_exam.starts_at,
333 new_exam.ends_at,
334 parent_exam_fields.language,
335 new_exam.time_minutes,
336 parent_exam_fields.minimum_points_treshold,
337 new_exam.grade_manually,
338 )
339 .fetch_one(&mut *tx)
340 .await?;
341
342 let contents_iter =
343 copy_exam_pages_and_return_contents(&mut *tx, copied_exam.id, parent_exam.id).await?;
344
345 let old_to_new_exercise_ids =
347 map_old_exr_ids_to_new_exr_ids_for_exams(&mut *tx, copied_exam.id, parent_exam.id).await?;
348
349 for (page_id, content) in contents_iter {
351 if let Value::Array(mut blocks) = content {
352 for block in blocks.iter_mut() {
353 if block["name"] != Value::String("moocfi/exercise".to_string()) {
354 continue;
355 }
356 if let Value::String(old_id) = &block["attributes"]["id"] {
357 let new_id = old_to_new_exercise_ids
358 .get(old_id)
359 .ok_or_else(|| {
360 ModelError::new(
361 ModelErrorType::Generic,
362 "Invalid exercise id in content.".to_string(),
363 None,
364 )
365 })?
366 .to_string();
367 block["attributes"]["id"] = Value::String(new_id);
368 }
369 }
370 sqlx::query!(
371 "
372UPDATE pages
373SET content = $1
374WHERE id = $2;
375 ",
376 Value::Array(blocks),
377 page_id,
378 )
379 .execute(&mut *tx)
380 .await?;
381 }
382 }
383
384 copy_exercise_slides(&mut *tx, copied_exam.id, parent_exam.id).await?;
385 copy_exercise_tasks(&mut *tx, copied_exam.id, parent_exam.id).await?;
386
387 let get_page_id = sqlx::query!(
388 "SELECT id AS page_id FROM pages WHERE exam_id = $1;",
389 copied_exam.id
390 )
391 .fetch_one(&mut *tx)
392 .await?;
393
394 Ok(Exam {
395 courses: vec![], ends_at: copied_exam.ends_at,
397 starts_at: copied_exam.starts_at,
398 id: copied_exam.id,
399 instructions: copied_exam.instructions,
400 name: copied_exam.name,
401 time_minutes: copied_exam.time_minutes,
402 page_id: get_page_id.page_id,
403 minimum_points_treshold: copied_exam.minimum_points_treshold,
404 language: copied_exam
405 .language
406 .unwrap_or_else(|| parent_exam_fields.language.unwrap_or("en-US".to_string())),
407 grade_manually: copied_exam.grade_manually,
408 })
409}
410
411async fn copy_course_pages_and_return_contents(
412 tx: &mut PgConnection,
413 namespace_id: Uuid,
414 parent_course_id: Uuid,
415) -> ModelResult<HashMap<Uuid, Value>> {
416 let contents = sqlx::query!(
418 "
419INSERT INTO pages (
420 id,
421 course_id,
422 content,
423 url_path,
424 title,
425 chapter_id,
426 order_number,
427 copied_from,
428 content_search_language,
429 page_language_group_id,
430 hidden
431 )
432SELECT uuid_generate_v5($1, id::text),
433 $1,
434 content,
435 url_path,
436 title,
437 uuid_generate_v5($1, chapter_id::text),
438 order_number,
439 id,
440 content_search_language,
441 page_language_group_id,
442 hidden
443FROM pages
444WHERE (course_id = $2)
445 AND deleted_at IS NULL
446RETURNING id,
447 content;
448 ",
449 namespace_id,
450 parent_course_id
451 )
452 .fetch_all(tx)
453 .await?
454 .into_iter()
455 .map(|record| (record.id, record.content))
456 .collect();
457
458 Ok(contents)
459}
460
461async fn copy_exam_pages_and_return_contents(
462 tx: &mut PgConnection,
463 namespace_id: Uuid,
464 parent_exam_id: Uuid,
465) -> ModelResult<HashMap<Uuid, Value>> {
466 let contents = sqlx::query!(
467 "
468INSERT INTO pages (
469 id,
470 exam_id,
471 content,
472 url_path,
473 title,
474 chapter_id,
475 order_number,
476 copied_from,
477 content_search_language,
478 hidden
479 )
480SELECT uuid_generate_v5($1, id::text),
481 $1,
482 content,
483 url_path,
484 title,
485 NULL,
486 order_number,
487 id,
488 content_search_language,
489 hidden
490FROM pages
491WHERE (exam_id = $2)
492 AND deleted_at IS NULL
493RETURNING id,
494 content;
495 ",
496 namespace_id,
497 parent_exam_id
498 )
499 .fetch_all(tx)
500 .await?
501 .into_iter()
502 .map(|record| (record.id, record.content))
503 .collect();
504
505 Ok(contents)
506}
507
508async fn set_chapter_front_pages(tx: &mut PgConnection, namespace_id: Uuid) -> ModelResult<()> {
509 sqlx::query!(
511 "
512UPDATE chapters
513SET front_page_id = uuid_generate_v5(course_id, front_page_id::text)
514WHERE course_id = $1
515 AND front_page_id IS NOT NULL;
516 ",
517 namespace_id,
518 )
519 .execute(&mut *tx)
520 .await?;
521
522 Ok(())
523}
524
525async fn copy_course_modules(
526 tx: &mut PgConnection,
527 new_course_id: Uuid,
528 old_course_id: Uuid,
529) -> ModelResult<()> {
530 sqlx::query!(
531 "
532INSERT INTO course_modules (
533 id,
534 course_id,
535 name,
536 order_number,
537 copied_from,
538 automatic_completion,
539 automatic_completion_number_of_exercises_attempted_treshold,
540 automatic_completion_number_of_points_treshold,
541 automatic_completion_requires_exam,
542 certification_enabled,
543 completion_registration_link_override,
544 ects_credits,
545 enable_registering_completion_to_uh_open_university,
546 uh_course_code
547 )
548SELECT uuid_generate_v5($1, id::text),
549 $1,
550 name,
551 order_number,
552 id,
553 automatic_completion,
554 automatic_completion_number_of_exercises_attempted_treshold,
555 automatic_completion_number_of_points_treshold,
556 automatic_completion_requires_exam,
557 certification_enabled,
558 completion_registration_link_override,
559 ects_credits,
560 enable_registering_completion_to_uh_open_university,
561 uh_course_code
562FROM course_modules
563WHERE course_id = $2
564 AND deleted_at IS NULL
565 ",
566 new_course_id,
567 old_course_id,
568 )
569 .execute(&mut *tx)
570 .await?;
571 Ok(())
572}
573
574async fn copy_course_chapters(
576 tx: &mut PgConnection,
577 namespace_id: Uuid,
578 parent_course_id: Uuid,
579) -> ModelResult<()> {
580 sqlx::query!(
581 "
582INSERT INTO chapters (
583 id,
584 name,
585 course_id,
586 chapter_number,
587 front_page_id,
588 opens_at,
589 chapter_image_path,
590 copied_from,
591 course_module_id,
592 color,
593 deadline
594 )
595SELECT uuid_generate_v5($1, id::text),
596 name,
597 $1,
598 chapter_number,
599 front_page_id,
600 opens_at,
601 chapter_image_path,
602 id,
603 uuid_generate_v5($1, course_module_id::text),
604 color,
605 deadline
606FROM chapters
607WHERE (course_id = $2)
608 AND deleted_at IS NULL;
609 ",
610 namespace_id,
611 parent_course_id
612 )
613 .execute(&mut *tx)
614 .await?;
615
616 Ok(())
617}
618
619async fn map_old_exr_ids_to_new_exr_ids_for_courses(
620 tx: &mut PgConnection,
621 new_course_id: Uuid,
622 src_course_id: Uuid,
623 target_clg_id: Uuid,
624 same_clg: bool,
625) -> ModelResult<HashMap<String, String>> {
626 let rows = sqlx::query!(
627 r#"
628WITH src AS (
629 SELECT e.*,
630 CASE
631 WHEN $4 THEN e.exercise_language_group_id
632 ELSE uuid_generate_v5($3, e.id::text)
633 END AS tgt_elg_id
634 FROM exercises e
635 WHERE e.course_id = $2
636 AND e.deleted_at IS NULL
637),
638ins_elg AS (
639 INSERT INTO exercise_language_groups (id, course_language_group_id)
640 SELECT DISTINCT tgt_elg_id,
641 $3
642 FROM src
643 WHERE NOT $4 ON CONFLICT (id) DO NOTHING
644),
645ins_exercises AS (
646 INSERT INTO exercises (
647 id,
648 course_id,
649 name,
650 deadline,
651 page_id,
652 score_maximum,
653 order_number,
654 chapter_id,
655 copied_from,
656 exercise_language_group_id,
657 max_tries_per_slide,
658 limit_number_of_tries,
659 needs_peer_review,
660 use_course_default_peer_or_self_review_config,
661 needs_self_review
662 )
663 SELECT uuid_generate_v5($1, src.id::text),
664 $1,
665 src.name,
666 src.deadline,
667 uuid_generate_v5($1, src.page_id::text),
668 src.score_maximum,
669 src.order_number,
670 uuid_generate_v5($1, src.chapter_id::text),
671 src.id,
672 src.tgt_elg_id,
673 src.max_tries_per_slide,
674 src.limit_number_of_tries,
675 src.needs_peer_review,
676 src.use_course_default_peer_or_self_review_config,
677 src.needs_self_review
678 FROM src
679 RETURNING id,
680 copied_from
681)
682SELECT id,
683 copied_from
684FROM ins_exercises;
685 "#,
686 new_course_id,
687 src_course_id,
688 target_clg_id,
689 same_clg,
690 )
691 .fetch_all(tx)
692 .await?;
693
694 Ok(rows
695 .into_iter()
696 .map(|r| (r.copied_from.unwrap().to_string(), r.id.to_string()))
697 .collect())
698}
699
700async fn map_old_exr_ids_to_new_exr_ids_for_exams(
701 tx: &mut PgConnection,
702 namespace_id: Uuid,
703 parent_exam_id: Uuid,
704) -> ModelResult<HashMap<String, String>> {
705 let old_to_new_exercise_ids = sqlx::query!(
706 "
707INSERT INTO exercises (
708 id,
709 exam_id,
710 name,
711 deadline,
712 page_id,
713 score_maximum,
714 order_number,
715 chapter_id,
716 copied_from,
717 max_tries_per_slide,
718 limit_number_of_tries,
719 needs_peer_review,
720 use_course_default_peer_or_self_review_config,
721 needs_self_review
722 )
723SELECT uuid_generate_v5($1, id::text),
724 $1,
725 name,
726 deadline,
727 uuid_generate_v5($1, page_id::text),
728 score_maximum,
729 order_number,
730 NULL,
731 id,
732 max_tries_per_slide,
733 limit_number_of_tries,
734 needs_peer_review,
735 use_course_default_peer_or_self_review_config,
736 needs_self_review
737FROM exercises
738WHERE exam_id = $2
739 AND deleted_at IS NULL
740RETURNING id,
741 copied_from;
742 ",
743 namespace_id,
744 parent_exam_id
745 )
746 .fetch_all(tx)
747 .await?
748 .into_iter()
749 .map(|record| {
750 Ok((
751 record
752 .copied_from
753 .ok_or_else(|| {
754 ModelError::new(
755 ModelErrorType::Generic,
756 "Query failed to return valid data.".to_string(),
757 None,
758 )
759 })?
760 .to_string(),
761 record.id.to_string(),
762 ))
763 })
764 .collect::<ModelResult<HashMap<String, String>>>()?;
765
766 Ok(old_to_new_exercise_ids)
767}
768
769async fn copy_exercise_slides(
770 tx: &mut PgConnection,
771 namespace_id: Uuid,
772 parent_id: Uuid,
773) -> ModelResult<()> {
774 sqlx::query!(
776 "
777 INSERT INTO exercise_slides (
778 id, exercise_id, order_number
779 )
780 SELECT uuid_generate_v5($1, id::text),
781 uuid_generate_v5($1, exercise_id::text),
782 order_number
783 FROM exercise_slides
784 WHERE exercise_id IN (SELECT id FROM exercises WHERE course_id = $2 OR exam_id = $2 AND deleted_at IS NULL)
785 AND deleted_at IS NULL;
786 ",
787 namespace_id,
788 parent_id
789 )
790 .execute(&mut *tx)
791 .await?;
792
793 Ok(())
794}
795
796async fn copy_exercise_tasks(
797 tx: &mut PgConnection,
798 namespace_id: Uuid,
799 parent_id: Uuid,
800) -> ModelResult<()> {
801 sqlx::query!(
803 "
804INSERT INTO exercise_tasks (
805 id,
806 exercise_slide_id,
807 exercise_type,
808 assignment,
809 private_spec,
810 public_spec,
811 model_solution_spec,
812 order_number,
813 copied_from
814 )
815SELECT uuid_generate_v5($1, id::text),
816 uuid_generate_v5($1, exercise_slide_id::text),
817 exercise_type,
818 assignment,
819 private_spec,
820 public_spec,
821 model_solution_spec,
822 order_number,
823 id
824FROM exercise_tasks
825WHERE exercise_slide_id IN (
826 SELECT s.id
827 FROM exercise_slides s
828 JOIN exercises e ON (e.id = s.exercise_id)
829 WHERE e.course_id = $2 OR e.exam_id = $2
830 AND e.deleted_at IS NULL
831 AND s.deleted_at IS NULL
832 )
833AND deleted_at IS NULL;
834 ",
835 namespace_id,
836 parent_id,
837 )
838 .execute(&mut *tx)
839 .await?;
840 Ok(())
841}
842
843pub async fn copy_user_permissions(
844 conn: &mut PgConnection,
845 new_course_id: Uuid,
846 old_course_id: Uuid,
847 user_id: Uuid,
848) -> ModelResult<()> {
849 sqlx::query!(
850 "
851INSERT INTO roles (
852 id,
853 user_id,
854 organization_id,
855 course_id,
856 role
857 )
858SELECT uuid_generate_v5($2, id::text),
859 user_id,
860 organization_id,
861 $2,
862 role
863FROM roles
864WHERE (course_id = $1)
865AND NOT (user_id = $3)
866AND deleted_at IS NULL;
867 ",
868 old_course_id,
869 new_course_id,
870 user_id
871 )
872 .execute(conn)
873 .await?;
874 Ok(())
875}
876
877async fn copy_peer_or_self_review_configs(
878 tx: &mut PgConnection,
879 namespace_id: Uuid,
880 parent_id: Uuid,
881) -> ModelResult<()> {
882 sqlx::query!(
883 "
884INSERT INTO peer_or_self_review_configs (
885 id,
886 course_id,
887 exercise_id,
888 peer_reviews_to_give,
889 peer_reviews_to_receive,
890 processing_strategy,
891 accepting_threshold,
892 manual_review_cutoff_in_days,
893 points_are_all_or_nothing,
894 review_instructions
895 )
896SELECT uuid_generate_v5($1, posrc.id::text),
897 $1,
898 uuid_generate_v5($1, posrc.exercise_id::text),
899 posrc.peer_reviews_to_give,
900 posrc.peer_reviews_to_receive,
901 posrc.processing_strategy,
902 posrc.accepting_threshold,
903 posrc.manual_review_cutoff_in_days,
904 posrc.points_are_all_or_nothing,
905 posrc.review_instructions
906FROM peer_or_self_review_configs posrc
907 LEFT JOIN exercises e ON (e.id = posrc.exercise_id)
908WHERE posrc.course_id = $2
909 AND posrc.deleted_at IS NULL
910 AND e.deleted_at IS NULL;
911 ",
912 namespace_id,
913 parent_id,
914 )
915 .execute(&mut *tx)
916 .await?;
917 Ok(())
918}
919
920async fn copy_peer_or_self_review_questions(
921 tx: &mut PgConnection,
922 namespace_id: Uuid,
923 parent_id: Uuid,
924) -> ModelResult<()> {
925 sqlx::query!(
926 "
927INSERT INTO peer_or_self_review_questions (
928 id,
929 peer_or_self_review_config_id,
930 order_number,
931 question,
932 question_type,
933 answer_required,
934 weight
935 )
936SELECT uuid_generate_v5($1, q.id::text),
937 uuid_generate_v5($1, q.peer_or_self_review_config_id::text),
938 q.order_number,
939 q.question,
940 q.question_type,
941 q.answer_required,
942 q.weight
943FROM peer_or_self_review_questions q
944 JOIN peer_or_self_review_configs posrc ON (posrc.id = q.peer_or_self_review_config_id)
945 JOIN exercises e ON (e.id = posrc.exercise_id)
946WHERE peer_or_self_review_config_id IN (
947 SELECT id
948 FROM peer_or_self_review_configs
949 WHERE course_id = $2
950 AND deleted_at IS NULL
951 )
952 AND q.deleted_at IS NULL
953 AND e.deleted_at IS NULL
954 AND posrc.deleted_at IS NULL;
955 ",
956 namespace_id,
957 parent_id,
958 )
959 .execute(&mut *tx)
960 .await?;
961 Ok(())
962}
963
964async fn copy_material_references(
965 tx: &mut PgConnection,
966 namespace_id: Uuid,
967 parent_id: Uuid,
968) -> ModelResult<()> {
969 sqlx::query!(
971 "
972INSERT INTO material_references (
973 citation_key,
974 course_id,
975 id,
976 reference
977)
978SELECT citation_key,
979 $1,
980 uuid_generate_v5($1, id::text),
981 reference
982FROM material_references
983WHERE course_id = $2
984AND deleted_at IS NULL;
985 ",
986 namespace_id,
987 parent_id,
988 )
989 .execute(&mut *tx)
990 .await?;
991 Ok(())
992}
993
994async fn copy_glossary_entries(
995 tx: &mut PgConnection,
996 new_course_id: Uuid,
997 old_course_id: Uuid,
998) -> ModelResult<()> {
999 sqlx::query!(
1000 "
1001INSERT INTO glossary (
1002 id,
1003 course_id,
1004 term,
1005 definition
1006 )
1007SELECT uuid_generate_v5($1, id::text),
1008 $1,
1009 term,
1010 definition
1011FROM glossary
1012WHERE course_id = $2
1013 AND deleted_at IS NULL;
1014 ",
1015 new_course_id,
1016 old_course_id,
1017 )
1018 .execute(&mut *tx)
1019 .await?;
1020 Ok(())
1021}
1022
1023async fn copy_certificate_configurations_and_requirements(
1024 tx: &mut PgConnection,
1025 new_course_id: Uuid,
1026 old_course_id: Uuid,
1027) -> ModelResult<()> {
1028 sqlx::query!(
1029 "
1030INSERT INTO certificate_configurations (
1031 id,
1032 background_svg_file_upload_id,
1033 background_svg_path,
1034 certificate_date_font_size,
1035 certificate_date_text_anchor,
1036 certificate_date_text_color,
1037 certificate_date_x_pos,
1038 certificate_date_y_pos,
1039 certificate_grade_font_size,
1040 certificate_grade_text_anchor,
1041 certificate_grade_text_color,
1042 certificate_grade_x_pos,
1043 certificate_grade_y_pos,
1044 certificate_locale,
1045 certificate_owner_name_font_size,
1046 certificate_owner_name_text_anchor,
1047 certificate_owner_name_text_color,
1048 certificate_owner_name_x_pos,
1049 certificate_owner_name_y_pos,
1050 certificate_validate_url_font_size,
1051 certificate_validate_url_text_anchor,
1052 certificate_validate_url_text_color,
1053 certificate_validate_url_x_pos,
1054 certificate_validate_url_y_pos,
1055 overlay_svg_file_upload_id,
1056 overlay_svg_path,
1057 paper_size,
1058 render_certificate_grade
1059 )
1060SELECT uuid_generate_v5($1, id::text),
1061 background_svg_file_upload_id,
1062 background_svg_path,
1063 certificate_date_font_size,
1064 certificate_date_text_anchor,
1065 certificate_date_text_color,
1066 certificate_date_x_pos,
1067 certificate_date_y_pos,
1068 certificate_grade_font_size,
1069 certificate_grade_text_anchor,
1070 certificate_grade_text_color,
1071 certificate_grade_x_pos,
1072 certificate_grade_y_pos,
1073 certificate_locale,
1074 certificate_owner_name_font_size,
1075 certificate_owner_name_text_anchor,
1076 certificate_owner_name_text_color,
1077 certificate_owner_name_x_pos,
1078 certificate_owner_name_y_pos,
1079 certificate_validate_url_font_size,
1080 certificate_validate_url_text_anchor,
1081 certificate_validate_url_text_color,
1082 certificate_validate_url_x_pos,
1083 certificate_validate_url_y_pos,
1084 overlay_svg_file_upload_id,
1085 overlay_svg_path,
1086 paper_size,
1087 render_certificate_grade
1088FROM certificate_configurations
1089WHERE id IN (
1090 SELECT certificate_configuration_id
1091 FROM certificate_configuration_to_requirements cctr
1092 JOIN course_modules cm ON cctr.course_module_id = cm.id
1093 WHERE cm.course_id = $2
1094 AND cctr.deleted_at IS NULL
1095 AND cm.deleted_at IS NULL
1096 )
1097 AND deleted_at IS NULL;
1098 ",
1099 new_course_id,
1100 old_course_id
1101 )
1102 .execute(&mut *tx)
1103 .await?;
1104
1105 sqlx::query!(
1106 "
1107INSERT INTO certificate_configuration_to_requirements (
1108 id,
1109 certificate_configuration_id,
1110 course_module_id
1111 )
1112SELECT uuid_generate_v5($1, cctr.id::text),
1113 uuid_generate_v5($1, cctr.certificate_configuration_id::text),
1114 uuid_generate_v5($1, cctr.course_module_id::text)
1115FROM certificate_configuration_to_requirements cctr
1116 JOIN course_modules cm ON cctr.course_module_id = cm.id
1117WHERE cm.course_id = $2
1118 AND cctr.deleted_at IS NULL
1119 AND cm.deleted_at IS NULL;
1120 ",
1121 new_course_id,
1122 old_course_id
1123 )
1124 .execute(&mut *tx)
1125 .await?;
1126
1127 Ok(())
1128}
1129
1130async fn copy_chatbot_configurations(
1131 tx: &mut PgConnection,
1132 new_course_id: Uuid,
1133 old_course_id: Uuid,
1134) -> ModelResult<()> {
1135 sqlx::query!(
1136 "
1137INSERT INTO chatbot_configurations (
1138 id,
1139 course_id,
1140 chatbot_name,
1141 initial_message,
1142 prompt,
1143 use_azure_search,
1144 maintain_azure_search_index,
1145 use_semantic_reranking,
1146 hide_citations,
1147 temperature,
1148 top_p,
1149 presence_penalty,
1150 frequency_penalty,
1151 response_max_tokens,
1152 daily_tokens_per_user,
1153 weekly_tokens_per_user,
1154 default_chatbot,
1155 enabled_to_students
1156 )
1157SELECT
1158 uuid_generate_v5($1, id::text),
1159 $1,
1160 chatbot_name,
1161 initial_message,
1162 prompt,
1163 use_azure_search,
1164 maintain_azure_search_index,
1165 use_semantic_reranking,
1166 hide_citations,
1167 temperature,
1168 top_p,
1169 presence_penalty,
1170 frequency_penalty,
1171 response_max_tokens,
1172 daily_tokens_per_user,
1173 weekly_tokens_per_user,
1174 default_chatbot,
1175 enabled_to_students
1176FROM chatbot_configurations
1177WHERE course_id = $2
1178 AND deleted_at IS NULL;
1179 ",
1180 new_course_id,
1181 old_course_id
1182 )
1183 .execute(&mut *tx)
1184 .await?;
1185 Ok(())
1186}
1187
1188async fn copy_cheater_thresholds(
1189 tx: &mut PgConnection,
1190 new_course_id: Uuid,
1191 old_course_id: Uuid,
1192) -> ModelResult<()> {
1193 sqlx::query!(
1194 "
1195INSERT INTO cheater_thresholds (id, course_id, points, duration_seconds)
1196SELECT
1197 uuid_generate_v5($1, id::text),
1198 $1,
1199 points,
1200 duration_seconds
1201FROM cheater_thresholds
1202WHERE course_id = $2;
1203 ",
1204 new_course_id,
1205 old_course_id
1206 )
1207 .execute(&mut *tx)
1208 .await?;
1209 Ok(())
1210}
1211
1212async fn copy_course_custom_privacy_policy_checkbox_texts(
1213 tx: &mut PgConnection,
1214 new_course_id: Uuid,
1215 old_course_id: Uuid,
1216) -> ModelResult<()> {
1217 sqlx::query!(
1218 "
1219INSERT INTO course_custom_privacy_policy_checkbox_texts (id, course_id, text_slug, text_html)
1220SELECT uuid_generate_v5($1, id::text),
1221 $1,
1222 text_slug,
1223 text_html
1224FROM course_custom_privacy_policy_checkbox_texts
1225WHERE course_id = $2
1226 AND deleted_at IS NULL;
1227 ",
1228 new_course_id,
1229 old_course_id
1230 )
1231 .execute(&mut *tx)
1232 .await?;
1233 Ok(())
1234}
1235
1236async fn copy_exercise_repositories(
1237 tx: &mut PgConnection,
1238 new_course_id: Uuid,
1239 old_course_id: Uuid,
1240) -> ModelResult<()> {
1241 sqlx::query!(
1242 "
1243INSERT INTO exercise_repositories (
1244 id,
1245 course_id,
1246 url,
1247 deploy_key,
1248 public_key,
1249 STATUS,
1250 error_message
1251 )
1252SELECT uuid_generate_v5($1, id::text),
1253 $1,
1254 url,
1255 deploy_key,
1256 public_key,
1257 STATUS,
1258 error_message
1259FROM exercise_repositories
1260WHERE course_id = $2
1261 AND deleted_at IS NULL;
1262 ",
1263 new_course_id,
1264 old_course_id
1265 )
1266 .execute(&mut *tx)
1267 .await?;
1268 Ok(())
1269}
1270
1271async fn copy_partners_blocks(
1272 tx: &mut PgConnection,
1273 new_course_id: Uuid,
1274 old_course_id: Uuid,
1275) -> ModelResult<()> {
1276 sqlx::query!(
1277 "
1278INSERT INTO partners_blocks (id, course_id, content)
1279SELECT uuid_generate_v5($1, id::text),
1280 $1,
1281 content
1282FROM partners_blocks
1283WHERE course_id = $2
1284 AND deleted_at IS NULL;
1285 ",
1286 new_course_id,
1287 old_course_id
1288 )
1289 .execute(&mut *tx)
1290 .await?;
1291 Ok(())
1292}
1293
1294async fn copy_privacy_links(
1295 tx: &mut PgConnection,
1296 new_course_id: Uuid,
1297 old_course_id: Uuid,
1298) -> ModelResult<()> {
1299 sqlx::query!(
1300 "
1301INSERT INTO privacy_links (id, course_id, url, title)
1302SELECT uuid_generate_v5($1, id::text),
1303 $1,
1304 url,
1305 title
1306FROM privacy_links
1307WHERE course_id = $2
1308 AND deleted_at IS NULL;
1309 ",
1310 new_course_id,
1311 old_course_id
1312 )
1313 .execute(&mut *tx)
1314 .await?;
1315 Ok(())
1316}
1317
1318async fn copy_research_consent_forms_and_questions(
1319 tx: &mut PgConnection,
1320 new_course_id: Uuid,
1321 old_course_id: Uuid,
1322) -> ModelResult<()> {
1323 sqlx::query!(
1324 "
1325INSERT INTO course_specific_research_consent_forms (id, course_id, content)
1326SELECT uuid_generate_v5($1, id::text),
1327 $1,
1328 content
1329FROM course_specific_research_consent_forms
1330WHERE course_id = $2
1331 AND deleted_at IS NULL;
1332 ",
1333 new_course_id,
1334 old_course_id
1335 )
1336 .execute(&mut *tx)
1337 .await?;
1338
1339 sqlx::query!(
1340 "
1341INSERT INTO course_specific_consent_form_questions (
1342 id,
1343 course_id,
1344 research_consent_form_id,
1345 question
1346 )
1347SELECT uuid_generate_v5($1, id::text),
1348 $1,
1349 uuid_generate_v5($1, research_consent_form_id::text),
1350 question
1351FROM course_specific_consent_form_questions
1352WHERE course_id = $2
1353 AND deleted_at IS NULL;
1354 ",
1355 new_course_id,
1356 old_course_id
1357 )
1358 .execute(&mut *tx)
1359 .await?;
1360
1361 Ok(())
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366 use super::*;
1367 use crate::{exercise_tasks::ExerciseTask, pages::Page, test_helper::*};
1368 use pretty_assertions::assert_eq;
1369
1370 #[tokio::test]
1371 async fn elg_preserved_when_same_course_language_group() {
1372 insert_data!(:tx, :user, :org, :course, instance: _i, course_module: _m,
1373 :chapter, :page, :exercise);
1374 let original_ex = crate::exercises::get_by_id(tx.as_mut(), exercise)
1375 .await
1376 .unwrap();
1377
1378 let new_meta = create_new_course(org, "fi-FI".into());
1380 let copied_course = copy_course(tx.as_mut(), course, &new_meta, true, user)
1381 .await
1382 .unwrap();
1383
1384 let copied_ex = crate::exercises::get_exercises_by_course_id(tx.as_mut(), copied_course.id)
1385 .await
1386 .unwrap()
1387 .pop()
1388 .unwrap();
1389
1390 assert_eq!(
1391 original_ex.exercise_language_group_id, copied_ex.exercise_language_group_id,
1392 "ELG must stay identical when CLG is unchanged"
1393 );
1394 }
1395
1396 #[tokio::test]
1399 async fn elg_deterministic_when_reusing_target_clg() {
1400 insert_data!(:tx, :user, :org, :course, instance: _i, course_module: _m,
1401 :chapter, :page, exercise: _e);
1402
1403 let reusable_clg = course_language_groups::insert(tx.as_mut(), PKeyPolicy::Generate)
1405 .await
1406 .unwrap();
1407
1408 let meta1 = create_new_course(org, "en-US".into());
1409 let copy1 =
1410 copy_course_with_language_group(tx.as_mut(), course, reusable_clg, &meta1, user)
1411 .await
1412 .unwrap();
1413
1414 let meta2 = {
1415 let mut nc = create_new_course(org, "pt-BR".into());
1416 nc.slug = "copied-course-2".into(); nc
1418 };
1419 let copy2 =
1420 copy_course_with_language_group(tx.as_mut(), course, reusable_clg, &meta2, user)
1421 .await
1422 .unwrap();
1423
1424 let ex1 = crate::exercises::get_exercises_by_course_id(tx.as_mut(), copy1.id)
1425 .await
1426 .unwrap()
1427 .pop()
1428 .unwrap();
1429 let ex2 = crate::exercises::get_exercises_by_course_id(tx.as_mut(), copy2.id)
1430 .await
1431 .unwrap()
1432 .pop()
1433 .unwrap();
1434
1435 assert_ne!(ex1.course_id, ex2.course_id); assert_eq!(
1437 ex1.exercise_language_group_id, ex2.exercise_language_group_id,
1438 "ELG must be deterministic for the same (target CLG, src exercise)"
1439 );
1440 }
1441
1442 #[tokio::test]
1443 async fn copies_course_as_different_course_language_group() {
1444 insert_data!(:tx, :user, :org, :course);
1445 let course = crate::courses::get_course(tx.as_mut(), course)
1446 .await
1447 .unwrap();
1448 let new_course = create_new_course(org, "en-US".into());
1449 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, false, user)
1450 .await
1451 .unwrap();
1452 assert_ne!(
1453 course.course_language_group_id,
1454 copied_course.course_language_group_id
1455 );
1456 }
1457
1458 #[tokio::test]
1459 async fn copies_course_as_same_course_language_group() {
1460 insert_data!(:tx, :user, :org, :course);
1461 let course = crate::courses::get_course(tx.as_mut(), course)
1462 .await
1463 .unwrap();
1464 let new_course = create_new_course(org, "fi-FI".into());
1465 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1466 .await
1467 .unwrap();
1468 assert_eq!(
1469 course.course_language_group_id,
1470 copied_course.course_language_group_id
1471 );
1472 }
1473
1474 #[tokio::test]
1475 async fn copies_course_instances() {
1476 insert_data!(:tx, :user, :org, :course, instance: _instance);
1477 let course = crate::courses::get_course(tx.as_mut(), course)
1478 .await
1479 .unwrap();
1480 let new_course = create_new_course(org, "en-GB".into());
1481 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1482 .await
1483 .unwrap();
1484 let copied_instances =
1485 crate::course_instances::get_course_instances_for_course(tx.as_mut(), copied_course.id)
1486 .await
1487 .unwrap();
1488 assert_eq!(copied_instances.len(), 1);
1489 }
1490
1491 #[tokio::test]
1492 async fn copies_course_modules() {
1493 insert_data!(:tx, :user, :org, :course);
1494 let course = crate::courses::get_course(tx.as_mut(), course)
1495 .await
1496 .unwrap();
1497 let new_course = create_new_course(org, "pt-BR".into());
1498 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1499 .await
1500 .unwrap();
1501
1502 let original_modules = crate::course_modules::get_by_course_id(tx.as_mut(), course.id)
1503 .await
1504 .unwrap();
1505 let copied_modules = crate::course_modules::get_by_course_id(tx.as_mut(), copied_course.id)
1506 .await
1507 .unwrap();
1508 assert_eq!(
1509 original_modules.first().unwrap().id,
1510 copied_modules.first().unwrap().copied_from.unwrap(),
1511 )
1512 }
1513
1514 #[tokio::test]
1515 async fn copies_course_chapters() {
1516 insert_data!(:tx, :user, :org, :course, instance: _instance, course_module: _course_module, :chapter);
1517 let course = crate::courses::get_course(tx.as_mut(), course)
1518 .await
1519 .unwrap();
1520 let new_course = create_new_course(org, "sv-SV".into());
1521 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1522 .await
1523 .unwrap();
1524 let copied_chapters = crate::chapters::course_chapters(tx.as_mut(), copied_course.id)
1525 .await
1526 .unwrap();
1527 assert_eq!(copied_chapters.len(), 1);
1528 assert_eq!(copied_chapters.first().unwrap().copied_from, Some(chapter));
1529 }
1530
1531 #[tokio::test]
1532 async fn updates_chapter_front_pages() {
1533 insert_data!(:tx, :user, :org, :course, instance: _instance, course_module: _course_module, chapter: _chapter);
1534 let course = crate::courses::get_course(tx.as_mut(), course)
1535 .await
1536 .unwrap();
1537 let new_course = create_new_course(org, "fr-CA".into());
1538 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1539 .await
1540 .unwrap();
1541 let copied_chapters = crate::chapters::course_chapters(tx.as_mut(), copied_course.id)
1542 .await
1543 .unwrap();
1544 let copied_chapter = copied_chapters.first().unwrap();
1545 let copied_chapter_front_page =
1546 crate::pages::get_page(tx.as_mut(), copied_chapter.front_page_id.unwrap())
1547 .await
1548 .unwrap();
1549 assert_eq!(copied_chapter_front_page.course_id, Some(copied_course.id));
1550 }
1551
1552 #[tokio::test]
1553 async fn copies_course_pages() {
1554 insert_data!(:tx, :user, :org, :course, instance: _instance, course_module: _course_module, :chapter, page: _page);
1555 let course = crate::courses::get_course(tx.as_mut(), course)
1556 .await
1557 .unwrap();
1558 let new_course = create_new_course(org, "es-US".into());
1559 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1560 .await
1561 .unwrap();
1562 let mut original_pages_by_id: HashMap<Uuid, Page> =
1563 crate::pages::get_all_by_course_id_and_visibility(
1564 tx.as_mut(),
1565 course.id,
1566 crate::pages::PageVisibility::Any,
1567 )
1568 .await
1569 .unwrap()
1570 .into_iter()
1571 .map(|page| (page.id, page))
1572 .collect();
1573 assert_eq!(original_pages_by_id.len(), 3);
1574 let copied_pages = crate::pages::get_all_by_course_id_and_visibility(
1575 tx.as_mut(),
1576 copied_course.id,
1577 crate::pages::PageVisibility::Any,
1578 )
1579 .await
1580 .unwrap();
1581 assert_eq!(copied_pages.len(), 3);
1582 copied_pages.into_iter().for_each(|copied_page| {
1583 assert!(
1584 original_pages_by_id
1585 .remove(&copied_page.copied_from.unwrap())
1586 .is_some()
1587 );
1588 });
1589 assert!(original_pages_by_id.is_empty());
1590 }
1591
1592 #[tokio::test]
1593 async fn updates_course_slugs_in_internal_links_in_pages_contents() {
1594 insert_data!(:tx, :user, :org, :course, instance: _instance, course_module: _course_module, :chapter, :page);
1595 let course = crate::courses::get_course(tx.as_mut(), course)
1596 .await
1597 .unwrap();
1598 crate::pages::update_page_content(
1599 tx.as_mut(),
1600 page,
1601 &serde_json::json!([{
1602 "name": "core/paragraph",
1603 "isValid": true,
1604 "clientId": "b2ecb473-38cc-4df1-84f7-45709cc63e95",
1605 "attributes": {
1606 "content": format!("Internal link <a href=\"http://project-331.local/org/uh-cs/courses/{slug2}\">http://project-331.local/org/uh-cs/courses/{slug1}</a>", slug2 = course.slug, slug1 = course.slug),
1607 "dropCap":false
1608 },
1609 "innerBlocks": []
1610 }]),
1611 )
1612 .await.unwrap();
1613
1614 let new_course = create_new_course(org, "fi-FI".into());
1615 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1616 .await
1617 .unwrap();
1618
1619 let copied_pages = crate::pages::get_all_by_course_id_and_visibility(
1620 tx.as_mut(),
1621 copied_course.id,
1622 crate::pages::PageVisibility::Any,
1623 )
1624 .await
1625 .unwrap();
1626 let copied_page = copied_pages
1627 .into_iter()
1628 .find(|copied_page| copied_page.copied_from == Some(page))
1629 .unwrap();
1630 let copied_content_in_page = copied_page.content[0]["attributes"]["content"]
1631 .as_str()
1632 .unwrap();
1633 let content_with_updated_course_slug = "Internal link <a href=\"http://project-331.local/org/uh-cs/courses/copied-course\">http://project-331.local/org/uh-cs/courses/copied-course</a>";
1634 assert_eq!(copied_content_in_page, content_with_updated_course_slug);
1635 }
1636
1637 #[tokio::test]
1638 async fn updates_exercise_id_in_content() {
1639 insert_data!(:tx, :user, :org, :course, instance: _instance, course_module: _course_module, :chapter, :page, :exercise);
1640 let course = crate::courses::get_course(tx.as_mut(), course)
1641 .await
1642 .unwrap();
1643 crate::pages::update_page_content(
1644 tx.as_mut(),
1645 page,
1646 &serde_json::json!([{
1647 "name": "moocfi/exercise",
1648 "isValid": true,
1649 "clientId": "b2ecb473-38cc-4df1-84f7-06709cc63e95",
1650 "attributes": {
1651 "id": exercise,
1652 "name": "Exercise"
1653 },
1654 "innerBlocks": []
1655 }]),
1656 )
1657 .await
1658 .unwrap();
1659 let new_course = create_new_course(org, "es-MX".into());
1660 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1661 .await
1662 .unwrap();
1663 let copied_pages = crate::pages::get_all_by_course_id_and_visibility(
1664 tx.as_mut(),
1665 copied_course.id,
1666 crate::pages::PageVisibility::Any,
1667 )
1668 .await
1669 .unwrap();
1670 let copied_page = copied_pages
1671 .into_iter()
1672 .find(|copied_page| copied_page.copied_from == Some(page))
1673 .unwrap();
1674 let copied_exercise_id_in_content =
1675 Uuid::parse_str(copied_page.content[0]["attributes"]["id"].as_str().unwrap()).unwrap();
1676 let copied_exercise =
1677 crate::exercises::get_by_id(tx.as_mut(), copied_exercise_id_in_content)
1678 .await
1679 .unwrap();
1680 assert_eq!(copied_exercise.course_id.unwrap(), copied_course.id);
1681 }
1682
1683 #[tokio::test]
1684 async fn copies_exercises_tasks_and_slides() {
1685 insert_data!(:tx, :user, :org, :course, instance: _instance, course_module: _course_module, :chapter, :page, :exercise, :slide, :task);
1686 let course = crate::courses::get_course(tx.as_mut(), course)
1687 .await
1688 .unwrap();
1689 let new_course = create_new_course(org, "fi-SV".into());
1690 let copied_course = copy_course(tx.as_mut(), course.id, &new_course, true, user)
1691 .await
1692 .unwrap();
1693 let copied_exercises =
1694 crate::exercises::get_exercises_by_course_id(tx.as_mut(), copied_course.id)
1695 .await
1696 .unwrap();
1697 assert_eq!(copied_exercises.len(), 1);
1698 let copied_exercise = copied_exercises.first().unwrap();
1699 assert_eq!(copied_exercise.copied_from, Some(exercise));
1700 let original_exercise = crate::exercises::get_by_id(tx.as_mut(), exercise)
1701 .await
1702 .unwrap();
1703 assert_eq!(
1704 copied_exercise.max_tries_per_slide,
1705 original_exercise.max_tries_per_slide
1706 );
1707 assert_eq!(
1708 copied_exercise.limit_number_of_tries,
1709 original_exercise.limit_number_of_tries
1710 );
1711 assert_eq!(
1712 copied_exercise.needs_peer_review,
1713 original_exercise.needs_peer_review
1714 );
1715 assert_eq!(
1716 copied_exercise.use_course_default_peer_or_self_review_config,
1717 original_exercise.use_course_default_peer_or_self_review_config
1718 );
1719 let copied_slides = crate::exercise_slides::get_exercise_slides_by_exercise_id(
1720 tx.as_mut(),
1721 copied_exercise.id,
1722 )
1723 .await
1724 .unwrap();
1725 assert_eq!(copied_slides.len(), 1);
1726 let copied_slide = copied_slides.first().unwrap();
1727 let copied_tasks: Vec<ExerciseTask> =
1728 crate::exercise_tasks::get_exercise_tasks_by_exercise_slide_id(
1729 tx.as_mut(),
1730 &copied_slide.id,
1731 )
1732 .await
1733 .unwrap();
1734 assert_eq!(copied_tasks.len(), 1);
1735 let copied_task = copied_tasks.first().unwrap();
1736 assert_eq!(copied_task.copied_from, Some(task));
1737
1738 let original_course_chapters = crate::chapters::course_chapters(tx.as_mut(), course.id)
1739 .await
1740 .unwrap();
1741 for original_chapter in original_course_chapters {
1742 for copied_exercise in &copied_exercises {
1743 assert_ne!(original_chapter.id, copied_exercise.id);
1744 }
1745 }
1746 }
1747
1748 fn create_new_course(organization_id: Uuid, language_code: String) -> NewCourse {
1749 NewCourse {
1750 name: "Copied course".to_string(),
1751 slug: "copied-course".to_string(),
1752 organization_id,
1753 language_code,
1754 teacher_in_charge_name: "Teacher".to_string(),
1755 teacher_in_charge_email: "teacher@example.com".to_string(),
1756 description: "".to_string(),
1757 is_draft: true,
1758 is_test_mode: false,
1759 is_unlisted: false,
1760 copy_user_permissions: false,
1761 is_joinable_by_code_only: false,
1762 join_code: None,
1763 ask_marketing_consent: false,
1764 flagged_answers_threshold: Some(3),
1765 can_add_chatbot: false,
1766 }
1767 }
1768}