headless_lms_server/programs/seed/builder/
page.rs

1use anyhow::Result;
2use headless_lms_utils::document_schema_processor::GutenbergBlock;
3
4use headless_lms_models::pages::{
5    CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask, CmsPageUpdate,
6};
7use sqlx::PgConnection;
8
9use crate::programs::seed::builder::{context::SeedContext, exercise::ExerciseBuilder};
10
11use crate::programs::seed::seed_helpers::create_page;
12
13/// Builder for course pages with Gutenberg blocks and exercises.
14#[derive(Debug, Clone)]
15pub struct PageBuilder {
16    pub url: String,
17    pub title: String,
18    pub blocks: Vec<GutenbergBlock>,
19    pub exercises: Vec<ExerciseBuilder>,
20}
21
22impl PageBuilder {
23    pub fn new(url: impl Into<String>, title: impl Into<String>) -> Self {
24        Self {
25            url: url.into(),
26            title: title.into(),
27            blocks: vec![],
28            exercises: vec![],
29        }
30    }
31    pub fn block(mut self, b: GutenbergBlock) -> Self {
32        self.blocks.push(b);
33        self
34    }
35    pub fn exercise(mut self, e: ExerciseBuilder) -> Self {
36        self.exercises.push(e);
37        self
38    }
39    pub fn blocks<I: IntoIterator<Item = GutenbergBlock>>(mut self, it: I) -> Self {
40        self.blocks.extend(it);
41        self
42    }
43    pub fn exercises<I: IntoIterator<Item = ExerciseBuilder>>(mut self, it: I) -> Self {
44        self.exercises.extend(it);
45        self
46    }
47
48    pub(crate) async fn seed(
49        self,
50        conn: &mut PgConnection,
51        cx: &SeedContext,
52        course_id: uuid::Uuid,
53        chapter_id: uuid::Uuid,
54    ) -> Result<uuid::Uuid> {
55        let mut cms_exercises: Vec<CmsPageExercise> = vec![];
56        let mut cms_slides: Vec<CmsPageExerciseSlide> = vec![];
57        let mut cms_tasks: Vec<CmsPageExerciseTask> = vec![];
58        let mut blocks = self.blocks;
59
60        for e in self.exercises {
61            let (b, e_, s, t) = e.to_cms(cx)?;
62            blocks.push(b);
63            cms_exercises.push(e_);
64            cms_slides.push(s);
65            cms_tasks.push(t);
66        }
67
68        create_page(
69            conn,
70            course_id,
71            cx.teacher,
72            Some(chapter_id),
73            CmsPageUpdate {
74                url_path: self.url,
75                title: self.title,
76                chapter_id: Some(chapter_id),
77                content: blocks,
78                exercises: cms_exercises,
79                exercise_slides: cms_slides,
80                exercise_tasks: cms_tasks,
81            },
82        )
83        .await
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use crate::programs::seed::{
90        builder::{
91            exercise::{ExerciseBuilder, ExerciseIds},
92            json_source::JsonSource,
93            page::PageBuilder,
94        },
95        seed_helpers::paragraph,
96    };
97
98    use headless_lms_utils::document_schema_processor::GutenbergBlock;
99    use serde_json::{Map, json};
100    use uuid::Uuid;
101
102    fn create_test_gutenberg_block() -> GutenbergBlock {
103        let mut attributes = Map::new();
104        attributes.insert("content".to_string(), json!("Test content"));
105
106        GutenbergBlock {
107            client_id: Uuid::new_v4(),
108            name: "core/paragraph".to_string(),
109            is_valid: true,
110            attributes,
111            inner_blocks: vec![],
112        }
113    }
114
115    fn create_test_exercise_builder() -> ExerciseBuilder {
116        let ids = ExerciseIds {
117            exercise_id: Uuid::new_v4(),
118            slide_id: Uuid::new_v4(),
119            task_id: Uuid::new_v4(),
120            block_id: Uuid::new_v4(),
121        };
122        let assignment_blocks = vec![paragraph("Answer this question.", Uuid::new_v4())];
123        let spec = JsonSource::Inline(json!({"type": "multiple_choice"}));
124
125        ExerciseBuilder::quizzes("Test Exercise", ids, false, None, spec, assignment_blocks)
126    }
127
128    #[test]
129    fn test_page_builder_creation_with_string() {
130        let page = PageBuilder::new("test-url".to_string(), "Test Title".to_string());
131
132        assert_eq!(page.url, "test-url");
133        assert_eq!(page.title, "Test Title");
134        assert!(page.blocks.is_empty());
135        assert!(page.exercises.is_empty());
136    }
137
138    #[test]
139    fn test_page_builder_creation_with_str() {
140        let page = PageBuilder::new("test-url", "Test Title");
141
142        assert_eq!(page.url, "test-url");
143        assert_eq!(page.title, "Test Title");
144        assert!(page.blocks.is_empty());
145        assert!(page.exercises.is_empty());
146    }
147
148    #[test]
149    fn test_page_builder_creation_mixed_types() {
150        let page = PageBuilder::new("test-url".to_string(), "Test Title");
151
152        assert_eq!(page.url, "test-url");
153        assert_eq!(page.title, "Test Title");
154    }
155
156    #[test]
157    fn test_page_builder_add_single_block() {
158        let block = create_test_gutenberg_block();
159        let page = PageBuilder::new("test-url", "Test Title").block(block.clone());
160
161        assert_eq!(page.blocks.len(), 1);
162        assert_eq!(page.blocks[0].name, block.name);
163        assert_eq!(page.blocks[0].client_id, block.client_id);
164    }
165
166    #[test]
167    fn test_page_builder_add_multiple_blocks() {
168        let block1 = create_test_gutenberg_block();
169        let block2 = create_test_gutenberg_block();
170
171        let page = PageBuilder::new("test-url", "Test Title")
172            .block(block1.clone())
173            .block(block2.clone());
174
175        assert_eq!(page.blocks.len(), 2);
176        assert_eq!(page.blocks[0].client_id, block1.client_id);
177        assert_eq!(page.blocks[1].client_id, block2.client_id);
178    }
179
180    #[test]
181    fn test_page_builder_add_single_exercise() {
182        let exercise = create_test_exercise_builder();
183        let page = PageBuilder::new("test-url", "Test Title").exercise(exercise);
184
185        assert_eq!(page.exercises.len(), 1);
186    }
187
188    #[test]
189    fn test_page_builder_add_multiple_exercises() {
190        let exercise1 = create_test_exercise_builder();
191        let exercise2 = create_test_exercise_builder();
192
193        let page = PageBuilder::new("test-url", "Test Title")
194            .exercise(exercise1)
195            .exercise(exercise2);
196
197        assert_eq!(page.exercises.len(), 2);
198    }
199
200    #[test]
201    fn test_page_builder_fluent_api_chaining() {
202        let block1 = create_test_gutenberg_block();
203        let block2 = create_test_gutenberg_block();
204        let exercise1 = create_test_exercise_builder();
205        let exercise2 = create_test_exercise_builder();
206
207        let page = PageBuilder::new("test-url", "Test Title")
208            .block(block1.clone())
209            .exercise(exercise1)
210            .block(block2.clone())
211            .exercise(exercise2);
212
213        assert_eq!(page.url, "test-url");
214        assert_eq!(page.title, "Test Title");
215        assert_eq!(page.blocks.len(), 2);
216        assert_eq!(page.exercises.len(), 2);
217        assert_eq!(page.blocks[0].client_id, block1.client_id);
218        assert_eq!(page.blocks[1].client_id, block2.client_id);
219    }
220
221    #[test]
222    fn test_page_builder_immutability() {
223        let block = create_test_gutenberg_block();
224        let exercise1 = create_test_exercise_builder();
225        let exercise2 = create_test_exercise_builder();
226
227        let original_page = PageBuilder::new("test-url", "Test Title");
228        let page_with_block = original_page.block(block.clone());
229        let page_with_both = page_with_block.exercise(exercise1);
230
231        let fresh_original = PageBuilder::new("test-url", "Test Title");
232        assert!(fresh_original.blocks.is_empty());
233        assert!(fresh_original.exercises.is_empty());
234
235        let test_page_with_block = PageBuilder::new("test-url", "Test Title").block(block.clone());
236        assert_eq!(test_page_with_block.blocks.len(), 1);
237        assert!(test_page_with_block.exercises.is_empty());
238
239        assert_eq!(page_with_both.blocks.len(), 1);
240        assert_eq!(page_with_both.exercises.len(), 1);
241
242        let another_page = PageBuilder::new("another-url", "Another Title").exercise(exercise2);
243        assert_eq!(another_page.exercises.len(), 1);
244        assert!(another_page.blocks.is_empty());
245    }
246
247    #[test]
248    fn test_page_builder_empty_initialization() {
249        let page = PageBuilder::new("", "");
250
251        assert_eq!(page.url, "");
252        assert_eq!(page.title, "");
253        assert!(page.blocks.is_empty());
254        assert!(page.exercises.is_empty());
255    }
256
257    #[test]
258    fn test_page_builder_with_special_characters() {
259        let page = PageBuilder::new("/path/with-special_chars", "Title with émojis 🚀");
260
261        assert_eq!(page.url, "/path/with-special_chars");
262        assert_eq!(page.title, "Title with émojis 🚀");
263    }
264
265    #[test]
266    fn test_page_builder_blocks_preserve_order() {
267        let block1 = create_test_gutenberg_block();
268        let block2 = create_test_gutenberg_block();
269        let block3 = create_test_gutenberg_block();
270
271        let page = PageBuilder::new("test-url", "Test Title")
272            .block(block1.clone())
273            .block(block2.clone())
274            .block(block3.clone());
275
276        assert_eq!(page.blocks.len(), 3);
277        assert_eq!(page.blocks[0].client_id, block1.client_id);
278        assert_eq!(page.blocks[1].client_id, block2.client_id);
279        assert_eq!(page.blocks[2].client_id, block3.client_id);
280    }
281
282    #[test]
283    fn test_page_builder_exercises_preserve_order() {
284        let exercise1 = create_test_exercise_builder();
285        let exercise2 = create_test_exercise_builder();
286        let exercise3 = create_test_exercise_builder();
287
288        let page = PageBuilder::new("test-url", "Test Title")
289            .exercise(exercise1)
290            .exercise(exercise2)
291            .exercise(exercise3);
292
293        assert_eq!(page.exercises.len(), 3);
294    }
295}