1use anyhow::{Context, Result};
2use headless_lms_base::config::ApplicationConfiguration;
3use sqlx::{Connection, PgConnection};
4use tracing::log::warn;
5use uuid::Uuid;
6
7use headless_lms_models::{
8 PKeyPolicy, certificate_configuration_to_requirements,
9 certificate_configurations::{self, DatabaseCertificateConfiguration},
10 chatbot_configurations::{self, NewChatbotConf},
11 course_instances::{self, CourseInstance, NewCourseInstance},
12 course_language_groups,
13 course_modules::CourseModule,
14 courses::{self, Course, NewCourse},
15 file_uploads, glossary,
16 pages::{self, NewCoursePage, NewPage},
17 roles::{self, RoleDomain, UserRole},
18};
19use headless_lms_utils::document_schema_processor::GutenbergBlock;
20
21use crate::programs::seed::{
22 builder::{context::SeedContext, module::ModuleBuilder},
23 seed_helpers::get_seed_spec_fetcher,
24};
25
26#[derive(Debug, Clone)]
28pub struct CourseInstanceConfig {
29 pub name: Option<String>,
30 pub description: Option<String>,
31 pub support_email: Option<String>,
32 pub teacher_in_charge_name: String,
33 pub teacher_in_charge_email: String,
34 pub opening_time: Option<chrono::DateTime<chrono::Utc>>,
35 pub closing_time: Option<chrono::DateTime<chrono::Utc>>,
36 pub instance_id: Option<Uuid>,
37}
38
39#[derive(Debug, Clone)]
41pub struct PageConfig {
42 pub url: String,
43 pub title: String,
44 pub page_number: i32,
45 pub is_hidden: bool,
46 pub content: Option<Vec<headless_lms_utils::document_schema_processor::GutenbergBlock>>,
47}
48
49#[derive(Debug, Clone)]
51pub struct GlossaryEntry {
52 pub acronym: String,
53 pub definition: String,
54}
55
56#[derive(Debug, Clone)]
58pub struct CertificateConfig {
59 pub background_svg_path: String,
60 pub certificate_id: Option<Uuid>,
61}
62
63#[derive(Debug, Clone)]
65pub struct CourseBuilder {
66 pub name: String,
67 pub slug: String,
68 pub language_code: String,
69 pub can_add_chatbot: bool,
70 pub description: String,
71 pub default_instance_name: &'static [u8],
72 pub modules: Vec<ModuleBuilder>,
73 pub extra_roles: Vec<(Uuid, UserRole)>,
74 pub course_id: Option<Uuid>,
75 pub instances: Vec<CourseInstanceConfig>,
76 pub chatbot_configs: Vec<NewChatbotConf>,
77 pub pages: Vec<PageConfig>,
78 pub glossary_entries: Vec<GlossaryEntry>,
79 pub certificate_config: Option<CertificateConfig>,
80 pub front_page_content: Option<Vec<GutenbergBlock>>,
81 pub chapter_locking_enabled: bool,
82 pub flagged_answers_skip_manual_review_and_allow_retry: bool,
83 pub flagged_answers_threshold: Option<i32>,
84}
85
86impl CourseBuilder {
87 pub fn new(name: impl Into<String>, slug: impl Into<String>) -> Self {
88 Self {
89 name: name.into(),
90 slug: slug.into(),
91 language_code: "en".into(),
92 can_add_chatbot: false,
93 description: "Sample course.".into(),
94 default_instance_name: b"default-instance",
95 modules: vec![],
96 extra_roles: vec![],
97 course_id: None,
98 instances: vec![],
99 chatbot_configs: vec![],
100 pages: vec![],
101 glossary_entries: vec![],
102 certificate_config: None,
103 front_page_content: None,
104 chapter_locking_enabled: false,
105 flagged_answers_skip_manual_review_and_allow_retry: false,
106 flagged_answers_threshold: None,
107 }
108 }
109
110 pub fn flagged_answers_skip_manual_review_and_allow_retry(mut self, v: bool) -> Self {
112 self.flagged_answers_skip_manual_review_and_allow_retry = v;
113 self
114 }
115
116 pub fn flagged_answers_threshold(mut self, v: Option<i32>) -> Self {
118 self.flagged_answers_threshold = v;
119 self
120 }
121
122 pub fn chatbot(mut self, v: bool) -> Self {
123 self.can_add_chatbot = v;
124 self
125 }
126 pub fn chapter_locking_enabled(mut self, v: bool) -> Self {
127 self.chapter_locking_enabled = v;
128 self
129 }
130 pub fn desc(mut self, d: impl Into<String>) -> Self {
131 self.description = d.into();
132 self
133 }
134 pub fn language(mut self, lc: impl Into<String>) -> Self {
135 self.language_code = lc.into();
136 self
137 }
138 pub fn module(mut self, m: ModuleBuilder) -> Self {
139 self.modules.push(m);
140 self
141 }
142 pub fn modules<I: IntoIterator<Item = ModuleBuilder>>(mut self, it: I) -> Self {
143 self.modules.extend(it);
144 self
145 }
146 pub fn role(mut self, user_id: Uuid, role: UserRole) -> Self {
147 self.extra_roles.push((user_id, role));
148 self
149 }
150 pub fn course_id(mut self, id: Uuid) -> Self {
151 self.course_id = Some(id);
152 self
153 }
154
155 pub fn instance(mut self, config: CourseInstanceConfig) -> Self {
156 self.instances.push(config);
157 self
158 }
159
160 pub fn chatbot_config(mut self, config: NewChatbotConf) -> Self {
161 if !self.can_add_chatbot {
162 warn!("Can't add chatbot to this course!!!!");
163 return self;
164 }
165 self.chatbot_configs.push(config);
166 self
167 }
168
169 pub fn top_level_page(
171 mut self,
172 url: impl Into<String>,
173 title: impl Into<String>,
174 page_number: i32,
175 is_hidden: bool,
176 content: Option<Vec<headless_lms_utils::document_schema_processor::GutenbergBlock>>,
177 ) -> Self {
178 self.pages.push(PageConfig {
179 url: url.into(),
180 title: title.into(),
181 page_number,
182 is_hidden,
183 content,
184 });
185 self
186 }
187
188 pub fn glossary_entry(
189 mut self,
190 acronym: impl Into<String>,
191 definition: impl Into<String>,
192 ) -> Self {
193 self.glossary_entries.push(GlossaryEntry {
194 acronym: acronym.into(),
195 definition: definition.into(),
196 });
197 self
198 }
199
200 pub fn certificate_config(
201 mut self,
202 background_svg_path: impl Into<String>,
203 certificate_id: Option<Uuid>,
204 ) -> Self {
205 self.certificate_config = Some(CertificateConfig {
206 background_svg_path: background_svg_path.into(),
207 certificate_id,
208 });
209 self
210 }
211
212 pub fn front_page_content(mut self, content: Vec<GutenbergBlock>) -> Self {
214 self.front_page_content = Some(content);
215 self
216 }
217
218 pub async fn seed(
220 self,
221 conn: &mut PgConnection,
222 app_config: &ApplicationConfiguration,
223 cx: &SeedContext,
224 ) -> Result<(Course, CourseInstance, CourseModule)> {
225 let course_id = self.course_id.unwrap_or(cx.base_course_ns);
226
227 let new_course = NewCourse {
228 name: self.name.clone(),
229 organization_id: cx.org,
230 slug: self.slug.clone(),
231 language_code: self.language_code.clone(),
232 teacher_in_charge_name: "admin".into(),
233 teacher_in_charge_email: "admin@example.com".into(),
234 description: self.description.clone(),
235 is_draft: false,
236 is_test_mode: false,
237 is_unlisted: false,
238 copy_user_permissions: false,
239 is_joinable_by_code_only: false,
240 join_code: None,
241 ask_marketing_consent: false,
242 flagged_answers_threshold: Some(self.flagged_answers_threshold.unwrap_or(3)),
243 can_add_chatbot: self.can_add_chatbot,
244 };
245
246 let mut tx = conn.begin().await.context("starting transaction")?;
248
249 let course_language_group_id = course_language_groups::insert(
250 &mut tx,
251 headless_lms_models::PKeyPolicy::Generate,
252 &new_course.slug,
253 )
254 .await?;
255
256 let course_id = courses::insert(
257 &mut tx,
258 app_config,
259 headless_lms_models::PKeyPolicy::Fixed(course_id),
260 course_language_group_id,
261 &new_course,
262 )
263 .await
264 .context("inserting course")?;
265
266 let course = courses::get_course(&mut tx, course_id)
267 .await
268 .context("getting course")?;
269
270 for chatbot_conf in self.chatbot_configs {
271 let chatbotconf_id = chatbot_conf.chatbotconf_id.unwrap_or_else(Uuid::new_v4);
272 chatbot_configurations::insert(
273 &mut tx,
274 PKeyPolicy::Fixed(chatbotconf_id),
275 NewChatbotConf {
276 course_id: Some(course_id),
277 ..chatbot_conf
278 },
279 )
280 .await
281 .context("inserting chatbot configuration for course")?;
282 }
283
284 let mut last_module = None;
285
286 for (i, m) in self.modules.into_iter().enumerate() {
287 last_module = Some(m.seed(&mut tx, cx, course.id, i as i32).await?);
288 }
289
290 for (user_id, role) in self.extra_roles {
291 roles::insert(&mut tx, user_id, role, RoleDomain::Course(course.id))
292 .await
293 .context("inserting course role")?;
294 }
295
296 let mut default_instance = None;
298 for instance_config in self.instances {
299 let instance_id = instance_config.instance_id.unwrap_or_else(Uuid::new_v4);
300 let instance = course_instances::insert(
301 &mut tx,
302 PKeyPolicy::Fixed(instance_id),
303 NewCourseInstance {
304 course_id: course.id,
305 name: instance_config.name.as_deref(),
306 description: instance_config.description.as_deref(),
307 support_email: instance_config.support_email.as_deref(),
308 teacher_in_charge_name: &instance_config.teacher_in_charge_name,
309 teacher_in_charge_email: &instance_config.teacher_in_charge_email,
310 opening_time: instance_config.opening_time,
311 closing_time: instance_config.closing_time,
312 },
313 )
314 .await
315 .context("inserting course instance")?;
316
317 if default_instance.is_none() {
319 default_instance = Some(instance);
320 }
321 }
322
323 let default_instance =
324 default_instance.expect("At least one course instance must be provided");
325
326 let default_front_page_content = vec![
328 GutenbergBlock::landing_page_hero_section("Welcome to...", "Subheading"),
329 GutenbergBlock::landing_page_copy_text(
330 "About this course",
331 "This course teaches you xxx.",
332 ),
333 GutenbergBlock::course_objective_section(),
334 GutenbergBlock::empty_block_from_name("moocfi/course-chapter-grid".to_string()),
335 GutenbergBlock::empty_block_from_name("moocfi/top-level-pages".to_string()),
336 GutenbergBlock::empty_block_from_name("moocfi/congratulations".to_string()),
337 GutenbergBlock::empty_block_from_name("moocfi/course-progress".to_string()),
338 ];
339
340 let front_page_blocks = self
341 .front_page_content
342 .unwrap_or(default_front_page_content);
343
344 let course_front_page = NewPage {
345 chapter_id: None,
346 content: front_page_blocks,
347 course_id: Some(course.id),
348 exam_id: None,
349 front_page_of_chapter_id: None,
350 title: course.name.clone(),
351 url_path: String::from("/"),
352 exercises: vec![],
353 exercise_slides: vec![],
354 exercise_tasks: vec![],
355 content_search_language: None,
356 hidden: false,
357 };
358
359 pages::insert_page(
360 &mut tx,
361 course_front_page,
362 cx.teacher,
363 get_seed_spec_fetcher(),
364 crate::domain::models_requests::fetch_service_info,
365 )
366 .await
367 .context("inserting course front page")?;
368
369 for page_config in self.pages {
371 let mut course_page = NewCoursePage::new(
372 course.id,
373 page_config.page_number,
374 &page_config.url,
375 &page_config.title,
376 );
377
378 if page_config.is_hidden {
379 course_page = course_page.set_hidden(true);
380 }
381
382 if let Some(content) = page_config.content {
383 course_page = course_page.set_content(content);
384 }
385
386 pages::insert_course_page(&mut tx, &course_page, cx.teacher)
387 .await
388 .context("inserting course page")?;
389 }
390
391 for glossary_entry in self.glossary_entries {
393 glossary::insert(
394 &mut tx,
395 &glossary_entry.acronym,
396 &glossary_entry.definition,
397 course.id,
398 )
399 .await
400 .context("inserting glossary entry")?;
401 }
402
403 if let Some(cert_config) = self.certificate_config {
405 let background_svg_file_upload_id = file_uploads::insert(
406 &mut tx,
407 &format!(
408 "background-{}.svg",
409 last_module
410 .as_ref()
411 .expect("At least one module must be provided")
412 .id
413 ),
414 &cert_config.background_svg_path,
415 "image/svg+xml",
416 None,
417 None,
418 )
419 .await
420 .context("inserting certificate background file")?;
421
422 let certificate_id = cert_config
423 .certificate_id
424 .unwrap_or_else(|| cx.v5(b"886d3e22-5007-4371-94d7-e0ad93a2391c"));
425 let configuration = DatabaseCertificateConfiguration {
426 id: certificate_id,
427 certificate_owner_name_y_pos: None,
428 certificate_owner_name_x_pos: None,
429 certificate_owner_name_font_size: None,
430 certificate_owner_name_text_color: None,
431 certificate_owner_name_text_anchor: None,
432 certificate_validate_url_y_pos: None,
433 certificate_validate_url_x_pos: None,
434 certificate_validate_url_font_size: None,
435 certificate_validate_url_text_color: None,
436 certificate_validate_url_text_anchor: None,
437 certificate_date_y_pos: None,
438 certificate_date_x_pos: None,
439 certificate_date_font_size: None,
440 certificate_date_text_color: None,
441 certificate_date_text_anchor: None,
442 certificate_locale: None,
443 paper_size: None,
444 background_svg_path: cert_config.background_svg_path,
445 background_svg_file_upload_id,
446 overlay_svg_path: None,
447 overlay_svg_file_upload_id: None,
448 render_certificate_grade: false,
449 certificate_grade_y_pos: None,
450 certificate_grade_x_pos: None,
451 certificate_grade_font_size: None,
452 certificate_grade_text_color: None,
453 certificate_grade_text_anchor: None,
454 };
455 let database_configuration =
456 certificate_configurations::insert(&mut tx, &configuration)
457 .await
458 .context("inserting certificate configuration")?;
459 certificate_configuration_to_requirements::insert(
460 &mut tx,
461 database_configuration.id,
462 Some(
463 last_module
464 .as_ref()
465 .expect("At least one module must be provided")
466 .id,
467 ),
468 )
469 .await
470 .context("linking certificate configuration to requirements")?;
471 }
472 tx.commit().await.context("committing transaction")?;
473
474 courses::set_cheater_detection_enabled(conn, course.id, false)
477 .await
478 .context("disabling cheater detection")?;
479
480 if self.chapter_locking_enabled || self.flagged_answers_skip_manual_review_and_allow_retry {
481 use headless_lms_models::courses::CourseUpdate;
482 let course_update = CourseUpdate {
483 name: course.name.clone(),
484 description: course.description.clone(),
485 is_draft: course.is_draft,
486 is_test_mode: course.is_test_mode,
487 can_add_chatbot: course.can_add_chatbot,
488 is_unlisted: course.is_unlisted,
489 is_joinable_by_code_only: course.is_joinable_by_code_only,
490 ask_marketing_consent: course.ask_marketing_consent,
491 flagged_answers_threshold: self.flagged_answers_threshold.unwrap_or(3),
492 flagged_answers_skip_manual_review_and_allow_retry: self
493 .flagged_answers_skip_manual_review_and_allow_retry,
494 closed_at: course.closed_at,
495 closed_additional_message: course.closed_additional_message.clone(),
496 closed_course_successor_id: course.closed_course_successor_id,
497 chapter_locking_enabled: self.chapter_locking_enabled,
498 ai_policy: course.ai_policy,
499 course_material_ai_instructions: course.course_material_ai_instructions,
500 };
501 let updated_course = courses::update_course(conn, app_config, course.id, course_update)
502 .await
503 .context("updating course")?;
504 return Ok((
505 updated_course,
506 default_instance,
507 last_module.expect("At least one module must be provided"),
508 ));
509 }
510
511 Ok((
512 course,
513 default_instance,
514 last_module.expect("At least one module must be provided"),
515 ))
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use crate::programs::seed::builder::module::ModuleBuilder;
522
523 use super::*;
524 use headless_lms_models::roles::UserRole;
525
526 #[test]
527 fn course_builder_new() {
528 let course = CourseBuilder::new("Test Course", "test-course");
529
530 assert_eq!(course.name, "Test Course");
531 assert_eq!(course.slug, "test-course");
532 assert_eq!(course.language_code, "en");
533 assert!(!course.can_add_chatbot);
534 assert_eq!(course.description, "Sample course.");
535 assert_eq!(course.default_instance_name, b"default-instance");
536 assert!(course.modules.is_empty());
537 assert!(course.extra_roles.is_empty());
538 assert!(course.course_id.is_none());
539 }
540
541 #[test]
542 fn course_builder_string_conversion() {
543 let course1 = CourseBuilder::new("String literal", "string-slug");
544 let course2 = CourseBuilder::new(String::from("Owned string"), String::from("owned-slug"));
545
546 assert_eq!(course1.name, "String literal");
547 assert_eq!(course1.slug, "string-slug");
548 assert_eq!(course2.name, "Owned string");
549 assert_eq!(course2.slug, "owned-slug");
550 }
551
552 #[test]
553 fn course_builder_chatbot() {
554 let course = CourseBuilder::new("Test Course", "test-course").chatbot(true);
555 assert!(course.can_add_chatbot);
556
557 let course_disabled = CourseBuilder::new("Test Course", "test-course").chatbot(false);
558 assert!(!course_disabled.can_add_chatbot);
559 }
560
561 #[test]
562 fn course_builder_desc() {
563 let course =
564 CourseBuilder::new("Test Course", "test-course").desc("A comprehensive test course");
565
566 assert_eq!(course.description, "A comprehensive test course");
567
568 let desc_string = String::from("Another description");
569 let course2 = CourseBuilder::new("Test Course", "test-course").desc(desc_string);
570 assert_eq!(course2.description, "Another description");
571 }
572
573 #[test]
574 fn course_builder_language() {
575 let course = CourseBuilder::new("Test Course", "test-course").language("fi-FI");
576
577 assert_eq!(course.language_code, "fi-FI");
578
579 let lang_string = String::from("sv-SE");
580 let course2 = CourseBuilder::new("Test Course", "test-course").language(lang_string);
581 assert_eq!(course2.language_code, "sv-SE");
582 }
583
584 #[test]
585 fn course_builder_module() {
586 let module = ModuleBuilder::new().order(0);
587 let course = CourseBuilder::new("Test Course", "test-course").module(module);
588
589 assert_eq!(course.modules.len(), 1);
590 assert_eq!(course.modules[0].order, Some(0));
591 }
592
593 #[test]
594 fn course_builder_multiple_modules() {
595 let module1 = ModuleBuilder::new().order(0);
596 let module2 = ModuleBuilder::new().order(1);
597 let course = CourseBuilder::new("Test Course", "test-course")
598 .module(module1)
599 .module(module2);
600
601 assert_eq!(course.modules.len(), 2);
602 assert_eq!(course.modules[0].order, Some(0));
603 assert_eq!(course.modules[1].order, Some(1));
604 }
605
606 #[test]
607 fn course_builder_role() {
608 let user_id = Uuid::new_v4();
609 let course =
610 CourseBuilder::new("Test Course", "test-course").role(user_id, UserRole::Teacher);
611
612 assert_eq!(course.extra_roles.len(), 1);
613 assert_eq!(course.extra_roles[0].0, user_id);
614 assert_eq!(course.extra_roles[0].1, UserRole::Teacher);
615 }
616
617 #[test]
618 fn course_builder_multiple_roles() {
619 let user1_id = Uuid::new_v4();
620 let user2_id = Uuid::new_v4();
621 let course = CourseBuilder::new("Test Course", "test-course")
622 .role(user1_id, UserRole::Teacher)
623 .role(user2_id, UserRole::MaterialViewer);
624
625 assert_eq!(course.extra_roles.len(), 2);
626 assert_eq!(course.extra_roles[0].0, user1_id);
627 assert_eq!(course.extra_roles[0].1, UserRole::Teacher);
628 assert_eq!(course.extra_roles[1].0, user2_id);
629 assert_eq!(course.extra_roles[1].1, UserRole::MaterialViewer);
630 }
631
632 #[test]
633 fn course_builder_course_id() {
634 let course_id = Uuid::new_v4();
635 let course = CourseBuilder::new("Test Course", "test-course").course_id(course_id);
636
637 assert_eq!(course.course_id, Some(course_id));
638 }
639
640 #[test]
641 fn course_builder_fluent_interface() {
642 let user_id = Uuid::new_v4();
643 let course_id = Uuid::new_v4();
644 let module = ModuleBuilder::new().order(0);
645
646 let course = CourseBuilder::new("Advanced Course", "advanced-course")
647 .desc("A comprehensive advanced course")
648 .language("fi-FI")
649 .chatbot(true)
650 .module(module)
651 .role(user_id, UserRole::Teacher)
652 .course_id(course_id);
653
654 assert_eq!(course.name, "Advanced Course");
655 assert_eq!(course.slug, "advanced-course");
656 assert_eq!(course.description, "A comprehensive advanced course");
657 assert_eq!(course.language_code, "fi-FI");
658 assert!(course.can_add_chatbot);
659 assert_eq!(course.modules.len(), 1);
660 assert_eq!(course.extra_roles.len(), 1);
661 assert_eq!(course.course_id, Some(course_id));
662 }
663
664 #[test]
665 fn course_builder_default_values() {
666 let course = CourseBuilder::new("Minimal Course", "minimal");
667
668 assert_eq!(course.language_code, "en");
669 assert!(!course.can_add_chatbot);
670 assert_eq!(course.description, "Sample course.");
671 assert_eq!(course.default_instance_name, b"default-instance");
672 assert!(course.modules.is_empty());
673 assert!(course.extra_roles.is_empty());
674 assert!(course.course_id.is_none());
675 }
676
677 #[test]
678 fn course_builder_empty_strings() {
679 let course = CourseBuilder::new("", "");
680
681 assert_eq!(course.name, "");
682 assert_eq!(course.slug, "");
683 assert_eq!(course.language_code, "en");
684 assert_eq!(course.description, "Sample course.");
685 }
686
687 #[test]
688 fn course_builder_method_chaining_order() {
689 let course1 = CourseBuilder::new("Course 1", "course-1")
690 .chatbot(true)
691 .desc("Description 1")
692 .language("fi-FI");
693
694 let course2 = CourseBuilder::new("Course 2", "course-2")
695 .language("fi-FI")
696 .desc("Description 2")
697 .chatbot(true);
698
699 assert_eq!(course1.can_add_chatbot, course2.can_add_chatbot);
700 assert_eq!(course1.language_code, course2.language_code);
701 assert_eq!(course1.description, "Description 1");
702 assert_eq!(course2.description, "Description 2");
703 }
704
705 #[test]
706 fn course_builder_front_page_content() {
707 let custom_content = vec![
708 GutenbergBlock::landing_page_hero_section("Custom Welcome", "Custom Subheading"),
709 GutenbergBlock::course_objective_section(),
710 ];
711
712 let course = CourseBuilder::new("Test Course", "test-course")
713 .front_page_content(custom_content.clone());
714
715 assert_eq!(course.front_page_content, Some(custom_content));
716 }
717
718 #[test]
719 fn course_builder_front_page_content_default() {
720 let course = CourseBuilder::new("Test Course", "test-course");
721
722 assert_eq!(course.front_page_content, None);
723 }
724
725 #[test]
726 fn course_builder_top_level_page() {
727 let course = CourseBuilder::new("Test Course", "test-course")
728 .top_level_page("/welcome", "Welcome Page", 1, false, None)
729 .top_level_page("/hidden", "Hidden Page", 2, true, None);
730
731 assert_eq!(course.pages.len(), 2);
732 assert_eq!(course.pages[0].url, "/welcome");
733 assert_eq!(course.pages[0].title, "Welcome Page");
734 assert_eq!(course.pages[0].page_number, 1);
735 assert!(!course.pages[0].is_hidden);
736 assert_eq!(course.pages[0].content, None);
737
738 assert_eq!(course.pages[1].url, "/hidden");
739 assert_eq!(course.pages[1].title, "Hidden Page");
740 assert_eq!(course.pages[1].page_number, 2);
741 assert!(course.pages[1].is_hidden);
742 assert_eq!(course.pages[1].content, None);
743 }
744
745 #[test]
746 fn course_builder_top_level_page_with_content() {
747 let content = vec![];
748 let course = CourseBuilder::new("Test Course", "test-course").top_level_page(
749 "/content",
750 "Content Page",
751 1,
752 false,
753 Some(content.clone()),
754 );
755
756 assert_eq!(course.pages.len(), 1);
757 assert_eq!(course.pages[0].url, "/content");
758 assert_eq!(course.pages[0].title, "Content Page");
759 assert_eq!(course.pages[0].page_number, 1);
760 assert!(!course.pages[0].is_hidden);
761 assert_eq!(course.pages[0].content, Some(content));
762 }
763
764 #[test]
765 fn course_builder_multiple_top_level_pages() {
766 let course = CourseBuilder::new("Test Course", "test-course")
767 .top_level_page("/welcome", "Welcome", 1, false, None)
768 .top_level_page("/about", "About", 2, false, None)
769 .top_level_page("/secret", "Secret", 3, true, None);
770
771 assert_eq!(course.pages.len(), 3);
772 assert_eq!(course.pages[0].url, "/welcome");
773 assert_eq!(course.pages[1].url, "/about");
774 assert_eq!(course.pages[2].url, "/secret");
775 assert!(course.pages[2].is_hidden);
776 }
777}