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