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 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 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 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 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 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 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
155pub 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
176pub 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 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 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}