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_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 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 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 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 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}