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(
126            "Test Exercise",
127            ids,
128            false,
129            None,
130            spec,
131            assignment_blocks,
132            true,
133        )
134    }
135
136    #[test]
137    fn test_page_builder_creation_with_string() {
138        let page = PageBuilder::new("test-url".to_string(), "Test Title".to_string());
139
140        assert_eq!(page.url, "test-url");
141        assert_eq!(page.title, "Test Title");
142        assert!(page.blocks.is_empty());
143        assert!(page.exercises.is_empty());
144    }
145
146    #[test]
147    fn test_page_builder_creation_with_str() {
148        let page = PageBuilder::new("test-url", "Test Title");
149
150        assert_eq!(page.url, "test-url");
151        assert_eq!(page.title, "Test Title");
152        assert!(page.blocks.is_empty());
153        assert!(page.exercises.is_empty());
154    }
155
156    #[test]
157    fn test_page_builder_creation_mixed_types() {
158        let page = PageBuilder::new("test-url".to_string(), "Test Title");
159
160        assert_eq!(page.url, "test-url");
161        assert_eq!(page.title, "Test Title");
162    }
163
164    #[test]
165    fn test_page_builder_add_single_block() {
166        let block = create_test_gutenberg_block();
167        let page = PageBuilder::new("test-url", "Test Title").block(block.clone());
168
169        assert_eq!(page.blocks.len(), 1);
170        assert_eq!(page.blocks[0].name, block.name);
171        assert_eq!(page.blocks[0].client_id, block.client_id);
172    }
173
174    #[test]
175    fn test_page_builder_add_multiple_blocks() {
176        let block1 = create_test_gutenberg_block();
177        let block2 = create_test_gutenberg_block();
178
179        let page = PageBuilder::new("test-url", "Test Title")
180            .block(block1.clone())
181            .block(block2.clone());
182
183        assert_eq!(page.blocks.len(), 2);
184        assert_eq!(page.blocks[0].client_id, block1.client_id);
185        assert_eq!(page.blocks[1].client_id, block2.client_id);
186    }
187
188    #[test]
189    fn test_page_builder_add_single_exercise() {
190        let exercise = create_test_exercise_builder();
191        let page = PageBuilder::new("test-url", "Test Title").exercise(exercise);
192
193        assert_eq!(page.exercises.len(), 1);
194    }
195
196    #[test]
197    fn test_page_builder_add_multiple_exercises() {
198        let exercise1 = create_test_exercise_builder();
199        let exercise2 = create_test_exercise_builder();
200
201        let page = PageBuilder::new("test-url", "Test Title")
202            .exercise(exercise1)
203            .exercise(exercise2);
204
205        assert_eq!(page.exercises.len(), 2);
206    }
207
208    #[test]
209    fn test_page_builder_fluent_api_chaining() {
210        let block1 = create_test_gutenberg_block();
211        let block2 = create_test_gutenberg_block();
212        let exercise1 = create_test_exercise_builder();
213        let exercise2 = create_test_exercise_builder();
214
215        let page = PageBuilder::new("test-url", "Test Title")
216            .block(block1.clone())
217            .exercise(exercise1)
218            .block(block2.clone())
219            .exercise(exercise2);
220
221        assert_eq!(page.url, "test-url");
222        assert_eq!(page.title, "Test Title");
223        assert_eq!(page.blocks.len(), 2);
224        assert_eq!(page.exercises.len(), 2);
225        assert_eq!(page.blocks[0].client_id, block1.client_id);
226        assert_eq!(page.blocks[1].client_id, block2.client_id);
227    }
228
229    #[test]
230    fn test_page_builder_immutability() {
231        let block = create_test_gutenberg_block();
232        let exercise1 = create_test_exercise_builder();
233        let exercise2 = create_test_exercise_builder();
234
235        let original_page = PageBuilder::new("test-url", "Test Title");
236        let page_with_block = original_page.block(block.clone());
237        let page_with_both = page_with_block.exercise(exercise1);
238
239        let fresh_original = PageBuilder::new("test-url", "Test Title");
240        assert!(fresh_original.blocks.is_empty());
241        assert!(fresh_original.exercises.is_empty());
242
243        let test_page_with_block = PageBuilder::new("test-url", "Test Title").block(block.clone());
244        assert_eq!(test_page_with_block.blocks.len(), 1);
245        assert!(test_page_with_block.exercises.is_empty());
246
247        assert_eq!(page_with_both.blocks.len(), 1);
248        assert_eq!(page_with_both.exercises.len(), 1);
249
250        let another_page = PageBuilder::new("another-url", "Another Title").exercise(exercise2);
251        assert_eq!(another_page.exercises.len(), 1);
252        assert!(another_page.blocks.is_empty());
253    }
254
255    #[test]
256    fn test_page_builder_empty_initialization() {
257        let page = PageBuilder::new("", "");
258
259        assert_eq!(page.url, "");
260        assert_eq!(page.title, "");
261        assert!(page.blocks.is_empty());
262        assert!(page.exercises.is_empty());
263    }
264
265    #[test]
266    fn test_page_builder_with_special_characters() {
267        let page = PageBuilder::new("/path/with-special_chars", "Title with émojis 🚀");
268
269        assert_eq!(page.url, "/path/with-special_chars");
270        assert_eq!(page.title, "Title with émojis 🚀");
271    }
272
273    #[test]
274    fn test_page_builder_blocks_preserve_order() {
275        let block1 = create_test_gutenberg_block();
276        let block2 = create_test_gutenberg_block();
277        let block3 = create_test_gutenberg_block();
278
279        let page = PageBuilder::new("test-url", "Test Title")
280            .block(block1.clone())
281            .block(block2.clone())
282            .block(block3.clone());
283
284        assert_eq!(page.blocks.len(), 3);
285        assert_eq!(page.blocks[0].client_id, block1.client_id);
286        assert_eq!(page.blocks[1].client_id, block2.client_id);
287        assert_eq!(page.blocks[2].client_id, block3.client_id);
288    }
289
290    #[test]
291    fn test_page_builder_exercises_preserve_order() {
292        let exercise1 = create_test_exercise_builder();
293        let exercise2 = create_test_exercise_builder();
294        let exercise3 = create_test_exercise_builder();
295
296        let page = PageBuilder::new("test-url", "Test Title")
297            .exercise(exercise1)
298            .exercise(exercise2)
299            .exercise(exercise3);
300
301        assert_eq!(page.exercises.len(), 3);
302    }
303}