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).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 };
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 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 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 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 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
152pub 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
173pub 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 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 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}