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
24pub 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 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 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 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 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 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
153pub 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
174pub 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 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 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}