Skip to main content

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