Skip to main content

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                hidden: false,
82            },
83        )
84        .await
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use crate::programs::seed::{
91        builder::{
92            exercise::{ExerciseBuilder, ExerciseIds},
93            json_source::JsonSource,
94            page::PageBuilder,
95        },
96        seed_helpers::paragraph,
97    };
98
99    use headless_lms_utils::document_schema_processor::GutenbergBlock;
100    use serde_json::{Map, json};
101    use uuid::Uuid;
102
103    fn create_test_gutenberg_block() -> GutenbergBlock {
104        let mut attributes = Map::new();
105        attributes.insert("content".to_string(), json!("Test content"));
106
107        GutenbergBlock {
108            client_id: Uuid::new_v4(),
109            name: "core/paragraph".to_string(),
110            is_valid: true,
111            attributes,
112            inner_blocks: vec![],
113        }
114    }
115
116    fn create_test_exercise_builder() -> ExerciseBuilder {
117        let ids = ExerciseIds {
118            exercise_id: Uuid::new_v4(),
119            slide_id: Uuid::new_v4(),
120            task_id: Uuid::new_v4(),
121            block_id: Uuid::new_v4(),
122        };
123        let assignment_blocks = vec![paragraph("Answer this question.", Uuid::new_v4())];
124        let spec = JsonSource::Inline(json!({"type": "multiple_choice"}));
125
126        ExerciseBuilder::quizzes(
127            "Test Exercise",
128            ids,
129            false,
130            None,
131            spec,
132            assignment_blocks,
133            true,
134        )
135    }
136
137    #[test]
138    fn test_page_builder_creation_with_string() {
139        let page = PageBuilder::new("test-url".to_string(), "Test Title".to_string());
140
141        assert_eq!(page.url, "test-url");
142        assert_eq!(page.title, "Test Title");
143        assert!(page.blocks.is_empty());
144        assert!(page.exercises.is_empty());
145    }
146
147    #[test]
148    fn test_page_builder_creation_with_str() {
149        let page = PageBuilder::new("test-url", "Test Title");
150
151        assert_eq!(page.url, "test-url");
152        assert_eq!(page.title, "Test Title");
153        assert!(page.blocks.is_empty());
154        assert!(page.exercises.is_empty());
155    }
156
157    #[test]
158    fn test_page_builder_creation_mixed_types() {
159        let page = PageBuilder::new("test-url".to_string(), "Test Title");
160
161        assert_eq!(page.url, "test-url");
162        assert_eq!(page.title, "Test Title");
163    }
164
165    #[test]
166    fn test_page_builder_add_single_block() {
167        let block = create_test_gutenberg_block();
168        let page = PageBuilder::new("test-url", "Test Title").block(block.clone());
169
170        assert_eq!(page.blocks.len(), 1);
171        assert_eq!(page.blocks[0].name, block.name);
172        assert_eq!(page.blocks[0].client_id, block.client_id);
173    }
174
175    #[test]
176    fn test_page_builder_add_multiple_blocks() {
177        let block1 = create_test_gutenberg_block();
178        let block2 = create_test_gutenberg_block();
179
180        let page = PageBuilder::new("test-url", "Test Title")
181            .block(block1.clone())
182            .block(block2.clone());
183
184        assert_eq!(page.blocks.len(), 2);
185        assert_eq!(page.blocks[0].client_id, block1.client_id);
186        assert_eq!(page.blocks[1].client_id, block2.client_id);
187    }
188
189    #[test]
190    fn test_page_builder_add_single_exercise() {
191        let exercise = create_test_exercise_builder();
192        let page = PageBuilder::new("test-url", "Test Title").exercise(exercise);
193
194        assert_eq!(page.exercises.len(), 1);
195    }
196
197    #[test]
198    fn test_page_builder_add_multiple_exercises() {
199        let exercise1 = create_test_exercise_builder();
200        let exercise2 = create_test_exercise_builder();
201
202        let page = PageBuilder::new("test-url", "Test Title")
203            .exercise(exercise1)
204            .exercise(exercise2);
205
206        assert_eq!(page.exercises.len(), 2);
207    }
208
209    #[test]
210    fn test_page_builder_fluent_api_chaining() {
211        let block1 = create_test_gutenberg_block();
212        let block2 = create_test_gutenberg_block();
213        let exercise1 = create_test_exercise_builder();
214        let exercise2 = create_test_exercise_builder();
215
216        let page = PageBuilder::new("test-url", "Test Title")
217            .block(block1.clone())
218            .exercise(exercise1)
219            .block(block2.clone())
220            .exercise(exercise2);
221
222        assert_eq!(page.url, "test-url");
223        assert_eq!(page.title, "Test Title");
224        assert_eq!(page.blocks.len(), 2);
225        assert_eq!(page.exercises.len(), 2);
226        assert_eq!(page.blocks[0].client_id, block1.client_id);
227        assert_eq!(page.blocks[1].client_id, block2.client_id);
228    }
229
230    #[test]
231    fn test_page_builder_immutability() {
232        let block = create_test_gutenberg_block();
233        let exercise1 = create_test_exercise_builder();
234        let exercise2 = create_test_exercise_builder();
235
236        let original_page = PageBuilder::new("test-url", "Test Title");
237        let page_with_block = original_page.block(block.clone());
238        let page_with_both = page_with_block.exercise(exercise1);
239
240        let fresh_original = PageBuilder::new("test-url", "Test Title");
241        assert!(fresh_original.blocks.is_empty());
242        assert!(fresh_original.exercises.is_empty());
243
244        let test_page_with_block = PageBuilder::new("test-url", "Test Title").block(block.clone());
245        assert_eq!(test_page_with_block.blocks.len(), 1);
246        assert!(test_page_with_block.exercises.is_empty());
247
248        assert_eq!(page_with_both.blocks.len(), 1);
249        assert_eq!(page_with_both.exercises.len(), 1);
250
251        let another_page = PageBuilder::new("another-url", "Another Title").exercise(exercise2);
252        assert_eq!(another_page.exercises.len(), 1);
253        assert!(another_page.blocks.is_empty());
254    }
255
256    #[test]
257    fn test_page_builder_empty_initialization() {
258        let page = PageBuilder::new("", "");
259
260        assert_eq!(page.url, "");
261        assert_eq!(page.title, "");
262        assert!(page.blocks.is_empty());
263        assert!(page.exercises.is_empty());
264    }
265
266    #[test]
267    fn test_page_builder_with_special_characters() {
268        let page = PageBuilder::new("/path/with-special_chars", "Title with émojis 🚀");
269
270        assert_eq!(page.url, "/path/with-special_chars");
271        assert_eq!(page.title, "Title with émojis 🚀");
272    }
273
274    #[test]
275    fn test_page_builder_blocks_preserve_order() {
276        let block1 = create_test_gutenberg_block();
277        let block2 = create_test_gutenberg_block();
278        let block3 = create_test_gutenberg_block();
279
280        let page = PageBuilder::new("test-url", "Test Title")
281            .block(block1.clone())
282            .block(block2.clone())
283            .block(block3.clone());
284
285        assert_eq!(page.blocks.len(), 3);
286        assert_eq!(page.blocks[0].client_id, block1.client_id);
287        assert_eq!(page.blocks[1].client_id, block2.client_id);
288        assert_eq!(page.blocks[2].client_id, block3.client_id);
289    }
290
291    #[test]
292    fn test_page_builder_exercises_preserve_order() {
293        let exercise1 = create_test_exercise_builder();
294        let exercise2 = create_test_exercise_builder();
295        let exercise3 = create_test_exercise_builder();
296
297        let page = PageBuilder::new("test-url", "Test Title")
298            .exercise(exercise1)
299            .exercise(exercise2)
300            .exercise(exercise3);
301
302        assert_eq!(page.exercises.len(), 3);
303    }
304}