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