headless_lms_models/library/
content_management.rs

1use futures::future::BoxFuture;
2use headless_lms_utils::document_schema_processor::GutenbergBlock;
3use url::Url;
4
5use crate::{
6    SpecFetcher,
7    chapters::{self, DatabaseChapter, NewChapter},
8    course_instances::{CourseInstance, NewCourseInstance},
9    course_language_groups,
10    course_modules::{CourseModule, NewCourseModule},
11    courses::{self, Course, NewCourse},
12    exercise_service_info::ExerciseServiceInfoApi,
13    pages::{self, NewPage, Page},
14    peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion,
15    prelude::*,
16};
17
18#[derive(Debug, Clone)]
19pub struct CreateNewCourseFixedIds {
20    pub course_id: Uuid,
21    pub default_course_instance_id: Uuid,
22}
23
24/// Creates a new course with a front page and default instances.
25pub async fn create_new_course(
26    conn: &mut PgConnection,
27    pkey_policy: PKeyPolicy<CreateNewCourseFixedIds>,
28    new_course: NewCourse,
29    user: Uuid,
30    spec_fetcher: impl SpecFetcher,
31    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
32) -> ModelResult<(Course, Page, CourseInstance, CourseModule)> {
33    let mut tx = conn.begin().await?;
34
35    let course_language_group_id =
36        course_language_groups::insert(&mut tx, PKeyPolicy::Generate).await?;
37
38    let course_id = courses::insert(
39        &mut tx,
40        pkey_policy.map_ref(|x| x.course_id),
41        course_language_group_id,
42        &new_course,
43    )
44    .await?;
45    let course = courses::get_course(&mut tx, course_id).await?;
46
47    // Create front page for course
48    let course_front_page_blocks = vec![
49        GutenbergBlock::landing_page_hero_section("Welcome to...", "Subheading"),
50        GutenbergBlock::landing_page_copy_text("About this course", "This course teaches you xxx."),
51        GutenbergBlock::course_objective_section(),
52        GutenbergBlock::empty_block_from_name("moocfi/course-chapter-grid".to_string()),
53        GutenbergBlock::empty_block_from_name("moocfi/top-level-pages".to_string()),
54        GutenbergBlock::empty_block_from_name("moocfi/congratulations".to_string()),
55        GutenbergBlock::empty_block_from_name("moocfi/course-progress".to_string()),
56    ];
57
58    let course_front_page = NewPage {
59        chapter_id: None,
60        content: course_front_page_blocks,
61        course_id: Some(course.id),
62        exam_id: None,
63        front_page_of_chapter_id: None,
64        title: course.name.clone(),
65        url_path: String::from("/"),
66        exercises: vec![],
67        exercise_slides: vec![],
68        exercise_tasks: vec![],
69        content_search_language: None,
70    };
71    let page = crate::pages::insert_page(
72        &mut tx,
73        course_front_page,
74        user,
75        spec_fetcher,
76        fetch_service_info,
77    )
78    .await?;
79
80    // Create default course instance
81    let default_course_instance = crate::course_instances::insert(
82        &mut tx,
83        pkey_policy.map_ref(|x| x.default_course_instance_id),
84        NewCourseInstance {
85            course_id: course.id,
86            name: None,
87            description: None,
88            support_email: None,
89            teacher_in_charge_name: &new_course.teacher_in_charge_name,
90            teacher_in_charge_email: &new_course.teacher_in_charge_email,
91            opening_time: None,
92            closing_time: None,
93        },
94    )
95    .await?;
96
97    // Create default course module
98    let default_module = crate::course_modules::insert(
99        &mut tx,
100        PKeyPolicy::Generate,
101        &NewCourseModule::new_course_default(course.id).set_ects_credits(Some(5.0)),
102    )
103    .await?;
104
105    // Create course default peer review config
106    let peer_or_self_review_config_id =
107        crate::peer_or_self_review_configs::insert(&mut tx, PKeyPolicy::Generate, course.id, None)
108            .await?;
109
110    // Create peer review questions for default peer review config
111    crate::peer_or_self_review_questions::upsert_multiple_peer_or_self_review_questions(
112        &mut tx,
113        &[
114            CmsPeerOrSelfReviewQuestion {
115                id: Uuid::new_v4(),
116                peer_or_self_review_config_id,
117                order_number: 0,
118                question: "General comments".to_string(),
119                question_type:
120                    crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay,
121                answer_required: false,
122                weight: 0.0,
123            },
124            CmsPeerOrSelfReviewQuestion {
125                id: Uuid::new_v4(),
126                peer_or_self_review_config_id,
127                order_number: 1,
128                question: "The answer was correct".to_string(),
129                question_type:
130                    crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
131                answer_required: true,
132                weight: 0.0,
133            },
134            CmsPeerOrSelfReviewQuestion {
135                id: Uuid::new_v4(),
136                peer_or_self_review_config_id,
137                order_number: 2,
138                question: "The answer was easy to read".to_string(),
139                question_type:
140                    crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
141                answer_required: true,
142                weight: 0.0,
143            },
144        ],
145    )
146    .await?;
147
148    tx.commit().await?;
149    Ok((course, page, default_course_instance, default_module))
150}
151
152/// Creates a new chapter with a front page.
153pub async fn create_new_chapter(
154    conn: &mut PgConnection,
155    pkey_policy: PKeyPolicy<(Uuid, Uuid)>,
156    new_chapter: &NewChapter,
157    user: Uuid,
158    spec_fetcher: impl SpecFetcher,
159    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
160) -> ModelResult<(DatabaseChapter, Page)> {
161    create_new_chapter_with_content(
162        conn,
163        pkey_policy,
164        new_chapter,
165        user,
166        spec_fetcher,
167        fetch_service_info,
168        None,
169    )
170    .await
171}
172
173/// Creates a new chapter with a front page and optional custom content.
174pub async fn create_new_chapter_with_content(
175    conn: &mut PgConnection,
176    pkey_policy: PKeyPolicy<(Uuid, Uuid)>,
177    new_chapter: &NewChapter,
178    user: Uuid,
179    spec_fetcher: impl SpecFetcher,
180    fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
181    custom_front_page_content: Option<Vec<GutenbergBlock>>,
182) -> ModelResult<(DatabaseChapter, Page)> {
183    let mut tx = conn.begin().await?;
184    let chapter_id = chapters::insert(&mut tx, pkey_policy.map_ref(|x| x.0), new_chapter).await?;
185    let chapter = chapters::get_chapter(&mut tx, chapter_id).await?;
186
187    let default_front_page_content = vec![
188        GutenbergBlock::hero_section(&chapter.name, ""),
189        GutenbergBlock::empty_block_from_name("moocfi/pages-in-chapter".to_string()),
190        GutenbergBlock::empty_block_from_name("moocfi/exercises-in-chapter".to_string()),
191    ];
192
193    let front_page_blocks = custom_front_page_content.unwrap_or(default_front_page_content);
194    let chapter_frontpage_content = serde_json::to_value(front_page_blocks)?;
195    let frontpage_blocks: Vec<GutenbergBlock> = serde_json::from_value(chapter_frontpage_content)?;
196    let chapter_frontpage = NewPage {
197        chapter_id: Some(chapter.id),
198        content: frontpage_blocks,
199        course_id: Some(chapter.course_id),
200        exam_id: None,
201        front_page_of_chapter_id: Some(chapter.id),
202        title: chapter.name.clone(),
203        url_path: format!("/chapter-{}", chapter.chapter_number),
204        exercises: vec![],
205        exercise_slides: vec![],
206        exercise_tasks: vec![],
207        content_search_language: None,
208    };
209
210    let page = match pkey_policy {
211        PKeyPolicy::Fixed((_, front_page_id)) => {
212            // Create page with fixed ID
213            let page_language_group_id = if let Some(course_id) = chapter_frontpage.course_id {
214                let course = crate::courses::get_course(&mut tx, course_id).await?;
215                let new_language_group_id = crate::page_language_groups::insert(
216                    &mut tx,
217                    crate::PKeyPolicy::Generate,
218                    course.course_language_group_id,
219                )
220                .await?;
221                Some(new_language_group_id)
222            } else {
223                None
224            };
225
226            let content_search_language = chapter_frontpage
227                .content_search_language
228                .unwrap_or_else(|| "simple".to_string());
229
230            let page = sqlx::query_as!(
231                Page,
232                r#"
233INSERT INTO pages(
234    id,
235    course_id,
236    exam_id,
237    content,
238    url_path,
239    title,
240    order_number,
241    chapter_id,
242    content_search_language,
243    page_language_group_id
244  )
245VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
246RETURNING id,
247  created_at,
248  updated_at,
249  course_id,
250  exam_id,
251  chapter_id,
252  url_path,
253  title,
254  deleted_at,
255  content,
256  order_number,
257  copied_from,
258  hidden,
259  page_language_group_id
260"#,
261                front_page_id,
262                chapter_frontpage.course_id,
263                chapter_frontpage.exam_id,
264                serde_json::to_value(chapter_frontpage.content)?,
265                chapter_frontpage.url_path.trim(),
266                chapter_frontpage.title.trim(),
267                0i32,
268                chapter_frontpage.chapter_id,
269                content_search_language as _,
270                page_language_group_id
271            )
272            .fetch_one(&mut *tx)
273            .await?;
274
275            // Create page history
276            let _history_id = crate::page_history::insert(
277                &mut tx,
278                PKeyPolicy::Generate,
279                page.id,
280                page.title.as_str(),
281                &crate::page_history::PageHistoryContent {
282                    content: page.content.clone(),
283                    exercises: chapter_frontpage.exercises,
284                    exercise_slides: chapter_frontpage.exercise_slides,
285                    exercise_tasks: chapter_frontpage.exercise_tasks,
286                    peer_or_self_review_configs: vec![],
287                    peer_or_self_review_questions: vec![],
288                },
289                crate::page_history::HistoryChangeReason::PageSaved,
290                user,
291                None,
292            )
293            .await?;
294
295            page
296        }
297        PKeyPolicy::Generate => {
298            pages::insert_page(
299                &mut tx,
300                chapter_frontpage,
301                user,
302                spec_fetcher,
303                fetch_service_info,
304            )
305            .await?
306        }
307    };
308
309    sqlx::query!(
310        "UPDATE chapters SET front_page_id = $1 WHERE id = $2",
311        page.id,
312        chapter.id
313    )
314    .execute(&mut *tx)
315    .await?;
316
317    let updated_chapter = chapters::get_chapter(&mut tx, chapter.id).await?;
318
319    tx.commit().await?;
320    Ok((updated_chapter, page))
321}