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_content = serde_json::to_value(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_content,
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 chapter_frontpage = NewPage {
196        chapter_id: Some(chapter.id),
197        content: chapter_frontpage_content,
198        course_id: Some(chapter.course_id),
199        exam_id: None,
200        front_page_of_chapter_id: Some(chapter.id),
201        title: chapter.name.clone(),
202        url_path: format!("/chapter-{}", chapter.chapter_number),
203        exercises: vec![],
204        exercise_slides: vec![],
205        exercise_tasks: vec![],
206        content_search_language: None,
207    };
208
209    let page = match pkey_policy {
210        PKeyPolicy::Fixed((_, front_page_id)) => {
211            // Create page with fixed ID
212            let page_language_group_id = if let Some(course_id) = chapter_frontpage.course_id {
213                let course = crate::courses::get_course(&mut tx, course_id).await?;
214                let new_language_group_id = crate::page_language_groups::insert(
215                    &mut tx,
216                    crate::PKeyPolicy::Generate,
217                    course.course_language_group_id,
218                )
219                .await?;
220                Some(new_language_group_id)
221            } else {
222                None
223            };
224
225            let content_search_language = chapter_frontpage
226                .content_search_language
227                .unwrap_or_else(|| "simple".to_string());
228
229            let page = sqlx::query_as!(
230                Page,
231                r#"
232INSERT INTO pages(
233    id,
234    course_id,
235    exam_id,
236    content,
237    url_path,
238    title,
239    order_number,
240    chapter_id,
241    content_search_language,
242    page_language_group_id
243  )
244VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
245RETURNING id,
246  created_at,
247  updated_at,
248  course_id,
249  exam_id,
250  chapter_id,
251  url_path,
252  title,
253  deleted_at,
254  content,
255  order_number,
256  copied_from,
257  hidden,
258  page_language_group_id
259"#,
260                front_page_id,
261                chapter_frontpage.course_id,
262                chapter_frontpage.exam_id,
263                chapter_frontpage.content,
264                chapter_frontpage.url_path.trim(),
265                chapter_frontpage.title.trim(),
266                0i32,
267                chapter_frontpage.chapter_id,
268                content_search_language as _,
269                page_language_group_id
270            )
271            .fetch_one(&mut *tx)
272            .await?;
273
274            // Create page history
275            let _history_id = crate::page_history::insert(
276                &mut tx,
277                PKeyPolicy::Generate,
278                page.id,
279                page.title.as_str(),
280                &crate::page_history::PageHistoryContent {
281                    content: page.content.clone(),
282                    exercises: chapter_frontpage.exercises,
283                    exercise_slides: chapter_frontpage.exercise_slides,
284                    exercise_tasks: chapter_frontpage.exercise_tasks,
285                    peer_or_self_review_configs: vec![],
286                    peer_or_self_review_questions: vec![],
287                },
288                crate::page_history::HistoryChangeReason::PageSaved,
289                user,
290                None,
291            )
292            .await?;
293
294            page
295        }
296        PKeyPolicy::Generate => {
297            pages::insert_page(
298                &mut tx,
299                chapter_frontpage,
300                user,
301                spec_fetcher,
302                fetch_service_info,
303            )
304            .await?
305        }
306    };
307
308    sqlx::query!(
309        "UPDATE chapters SET front_page_id = $1 WHERE id = $2",
310        page.id,
311        chapter.id
312    )
313    .execute(&mut *tx)
314    .await?;
315
316    let updated_chapter = chapters::get_chapter(&mut tx, chapter.id).await?;
317
318    tx.commit().await?;
319    Ok((updated_chapter, page))
320}