headless_lms_server/programs/seed/builder/
page.rs1use 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#[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}