1use futures::future::BoxFuture;
2use headless_lms_utils::document_schema_processor::{
3 COURSE_OBJECTIVE_PLACEHOLDERS, GutenbergBlock,
4};
5use url::Url;
6
7use crate::{
8 SpecFetcher,
9 chapters::{self, DatabaseChapter, NewChapter},
10 course_instances::{CourseInstance, NewCourseInstance},
11 course_language_groups,
12 course_modules::{CourseModule, NewCourseModule},
13 courses::{self, Course, NewCourse},
14 entity_history::{self, HistoryAuthor},
15 exercise_service_info::ExerciseServiceInfoApi,
16 library::entity_snapshots::ContentEntityType,
17 pages::{self, NewPage, Page},
18 peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion,
19 prelude::*,
20};
21
22#[derive(Debug, Clone)]
23pub struct CreateNewCourseFixedIds {
24 pub course_id: Uuid,
25 pub default_course_instance_id: Uuid,
26}
27
28const FRONT_PAGE_TITLE_PLACEHOLDER: &str = "Welcome to...";
29const FRONT_PAGE_SUBTITLE_PLACEHOLDER: &str = "Subheading";
30const FRONT_PAGE_ABOUT_PLACEHOLDER: &str = "This course teaches you xxx.";
31
32pub const TEMPLATE_PLACEHOLDER_TEXTS: [&str; 9] = [
35 FRONT_PAGE_TITLE_PLACEHOLDER,
36 FRONT_PAGE_SUBTITLE_PLACEHOLDER,
37 FRONT_PAGE_ABOUT_PLACEHOLDER,
38 COURSE_OBJECTIVE_PLACEHOLDERS[0].0,
39 COURSE_OBJECTIVE_PLACEHOLDERS[0].1,
40 COURSE_OBJECTIVE_PLACEHOLDERS[1].0,
41 COURSE_OBJECTIVE_PLACEHOLDERS[1].1,
42 COURSE_OBJECTIVE_PLACEHOLDERS[2].0,
43 COURSE_OBJECTIVE_PLACEHOLDERS[2].1,
44];
45
46pub fn default_course_front_page_content() -> Vec<GutenbergBlock> {
48 vec![
49 GutenbergBlock::landing_page_hero_section(
50 FRONT_PAGE_TITLE_PLACEHOLDER,
51 FRONT_PAGE_SUBTITLE_PLACEHOLDER,
52 ),
53 GutenbergBlock::landing_page_copy_text("About this course", FRONT_PAGE_ABOUT_PLACEHOLDER),
54 GutenbergBlock::course_objective_section(),
55 GutenbergBlock::empty_block_from_name("moocfi/course-chapter-grid".to_string()),
56 GutenbergBlock::empty_block_from_name("moocfi/top-level-pages".to_string()),
57 GutenbergBlock::empty_block_from_name("moocfi/congratulations".to_string()),
58 GutenbergBlock::empty_block_from_name("moocfi/course-progress".to_string()),
59 ]
60}
61
62pub async fn create_new_course(
64 conn: &mut PgConnection,
65 app_config: &ApplicationConfiguration,
66 pkey_policy: PKeyPolicy<CreateNewCourseFixedIds>,
67 new_course: NewCourse,
68 user: Uuid,
69 spec_fetcher: impl SpecFetcher,
70 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
71) -> ModelResult<(Course, Page, CourseInstance, CourseModule)> {
72 let mut tx = conn.begin().await?;
73
74 let course_language_group_id =
75 course_language_groups::insert(&mut tx, PKeyPolicy::Generate, &new_course.slug).await?;
76
77 let course_id = courses::insert(
78 &mut tx,
79 app_config,
80 pkey_policy.map_ref(|x| x.course_id),
81 course_language_group_id,
82 &new_course,
83 )
84 .await?;
85 let course = courses::get_course(&mut tx, course_id).await?;
86
87 let course_front_page = NewPage {
88 chapter_id: None,
89 content: default_course_front_page_content(),
90 course_id: Some(course.id),
91 exam_id: None,
92 front_page_of_chapter_id: None,
93 title: course.name.clone(),
94 url_path: String::from("/"),
95 exercises: vec![],
96 exercise_slides: vec![],
97 exercise_tasks: vec![],
98 content_search_language: None,
99 hidden: false,
100 };
101 let page = crate::pages::insert_page(
102 &mut tx,
103 course_front_page,
104 user,
105 spec_fetcher,
106 fetch_service_info,
107 )
108 .await?;
109
110 let default_course_instance = crate::course_instances::insert(
112 &mut tx,
113 pkey_policy.map_ref(|x| x.default_course_instance_id),
114 NewCourseInstance {
115 course_id: course.id,
116 name: None,
117 description: None,
118 support_email: None,
119 teacher_in_charge_name: &new_course.teacher_in_charge_name,
120 teacher_in_charge_email: &new_course.teacher_in_charge_email,
121 opening_time: None,
122 closing_time: None,
123 },
124 )
125 .await?;
126
127 let default_module = crate::course_modules::insert(
129 &mut tx,
130 PKeyPolicy::Generate,
131 &NewCourseModule::new_course_default(course.id).set_ects_credits(Some(5.0)),
132 )
133 .await?;
134
135 let peer_or_self_review_config_id =
137 crate::peer_or_self_review_configs::insert(&mut tx, PKeyPolicy::Generate, course.id, None)
138 .await?;
139
140 crate::peer_or_self_review_questions::upsert_multiple_peer_or_self_review_questions(
142 &mut tx,
143 &[
144 CmsPeerOrSelfReviewQuestion {
145 id: Uuid::new_v4(),
146 peer_or_self_review_config_id,
147 order_number: 0,
148 question: "General comments".to_string(),
149 question_type:
150 crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay,
151 answer_required: false,
152 weight: 0.0,
153 },
154 CmsPeerOrSelfReviewQuestion {
155 id: Uuid::new_v4(),
156 peer_or_self_review_config_id,
157 order_number: 1,
158 question: "The answer was correct".to_string(),
159 question_type:
160 crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
161 answer_required: true,
162 weight: 0.0,
163 },
164 CmsPeerOrSelfReviewQuestion {
165 id: Uuid::new_v4(),
166 peer_or_self_review_config_id,
167 order_number: 2,
168 question: "The answer was easy to read".to_string(),
169 question_type:
170 crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale,
171 answer_required: true,
172 weight: 0.0,
173 },
174 ],
175 )
176 .await?;
177
178 tx.commit().await?;
179 Ok((course, page, default_course_instance, default_module))
180}
181
182pub struct ChapterFrontPage {
184 pub title: String,
185 pub url_path: String,
186 pub content: Vec<GutenbergBlock>,
187}
188
189impl ChapterFrontPage {
190 pub fn new(chapter_name: &str, chapter_number: i32) -> Self {
192 Self {
193 title: chapter_name.to_string(),
194 url_path: chapters::front_page_path(chapter_number),
195 content: vec![
196 GutenbergBlock::hero_section(chapter_name, ""),
197 GutenbergBlock::empty_block_from_name("moocfi/pages-in-chapter".to_string()),
198 GutenbergBlock::empty_block_from_name("moocfi/exercises-in-chapter".to_string()),
199 ],
200 }
201 }
202}
203
204pub async fn create_new_chapter(
206 conn: &mut PgConnection,
207 pkey_policy: PKeyPolicy<(Uuid, Uuid)>,
208 new_chapter: &NewChapter,
209 user: Uuid,
210 spec_fetcher: impl SpecFetcher,
211 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
212) -> ModelResult<(DatabaseChapter, Page)> {
213 create_new_chapter_with_content(
214 conn,
215 pkey_policy,
216 new_chapter,
217 user,
218 spec_fetcher,
219 fetch_service_info,
220 None,
221 )
222 .await
223}
224
225pub async fn create_new_chapter_with_content(
227 conn: &mut PgConnection,
228 pkey_policy: PKeyPolicy<(Uuid, Uuid)>,
229 new_chapter: &NewChapter,
230 user: Uuid,
231 spec_fetcher: impl SpecFetcher,
232 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
233 custom_front_page_content: Option<Vec<GutenbergBlock>>,
234) -> ModelResult<(DatabaseChapter, Page)> {
235 let mut tx = conn.begin().await?;
236 let chapter_id = chapters::insert(&mut tx, pkey_policy.map_ref(|x| x.0), new_chapter).await?;
237 let chapter = chapters::get_chapter(&mut tx, chapter_id).await?;
238
239 let front_page = ChapterFrontPage::new(&chapter.name, chapter.chapter_number);
240 let chapter_frontpage = NewPage {
241 chapter_id: Some(chapter.id),
242 content: custom_front_page_content.unwrap_or(front_page.content),
243 course_id: Some(chapter.course_id),
244 exam_id: None,
245 front_page_of_chapter_id: Some(chapter.id),
246 title: front_page.title,
247 url_path: front_page.url_path,
248 exercises: vec![],
249 exercise_slides: vec![],
250 exercise_tasks: vec![],
251 content_search_language: None,
252 hidden: false,
253 };
254
255 let page = match pkey_policy {
256 PKeyPolicy::Fixed((_, front_page_id)) => {
257 let course = crate::courses::get_course(&mut tx, chapter.course_id).await?;
258 let page = pages::insert_page_row(
259 &mut tx,
260 pages::PageRow {
261 id: PKeyPolicy::Fixed(front_page_id),
262 course: Some(&course),
263 exam_id: None,
264 chapter_id: Some(chapter.id),
265 url_path: &chapter_frontpage.url_path,
266 title: &chapter_frontpage.title,
267 order_number: 0,
268 hidden: chapter_frontpage.hidden,
269 content_search_language: None,
270 page_language_group_id: None,
271 },
272 &serde_json::to_value(&chapter_frontpage.content)?,
273 )
274 .await?;
275 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 HistoryAuthor::user(user),
290 None,
291 )
292 .await?;
293 page
294 }
295 PKeyPolicy::Generate => {
296 pages::insert_page(
297 &mut tx,
298 chapter_frontpage,
299 user,
300 spec_fetcher,
301 fetch_service_info,
302 )
303 .await?
304 }
305 };
306
307 sqlx::query!(
308 "UPDATE chapters SET front_page_id = $1 WHERE id = $2",
309 page.id,
310 chapter.id
311 )
312 .execute(&mut *tx)
313 .await?;
314
315 let updated_chapter = chapters::get_chapter(&mut tx, chapter.id).await?;
316 entity_history::record_current(
317 &mut tx,
318 ContentEntityType::Chapter,
319 chapter.id,
320 HistoryAuthor::user(user),
321 )
322 .await?;
323
324 tx.commit().await?;
325 Ok((updated_chapter, page))
326}