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}