headless_lms_server/programs/seed/builder/
module.rs

1use anyhow::{Context, Result};
2
3use headless_lms_models::course_modules::{
4    self, AutomaticCompletionRequirements, CompletionPolicy, CourseModule, NewCourseModule,
5};
6use sqlx::PgConnection;
7
8use crate::programs::seed::builder::{chapter::ChapterBuilder, context::SeedContext};
9
10/// Builder for course modules that group chapters with ECTS credits and Open University registration.
11#[derive(Debug, Clone)]
12pub struct ModuleBuilder {
13    pub name: Option<String>,
14    pub order: Option<i32>,
15    pub ects: Option<f32>,
16    pub chapters: Vec<ChapterBuilder>,
17    pub register_to_open_university: bool,
18    pub completion_policy: CompletionPolicy,
19}
20
21impl Default for ModuleBuilder {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl ModuleBuilder {
28    pub fn new() -> Self {
29        Self {
30            name: None,
31            order: None,
32            ects: None,
33            chapters: vec![],
34            register_to_open_university: false,
35            completion_policy: CompletionPolicy::Manual,
36        }
37    }
38    pub fn order(mut self, n: i32) -> Self {
39        self.order = Some(n);
40        self
41    }
42    pub fn name(mut self, n: impl Into<String>) -> Self {
43        self.name = Some(n.into());
44        self
45    }
46    pub fn ects(mut self, e: f32) -> Self {
47        self.ects = Some(e);
48        self
49    }
50    pub fn register_to_open_university(mut self, v: bool) -> Self {
51        self.register_to_open_university = v;
52        self
53    }
54    pub fn chapter(mut self, c: ChapterBuilder) -> Self {
55        self.chapters.push(c);
56        self
57    }
58    pub fn chapters<I: IntoIterator<Item = ChapterBuilder>>(mut self, it: I) -> Self {
59        self.chapters.extend(it);
60        self
61    }
62    pub fn completion_policy(mut self, policy: CompletionPolicy) -> Self {
63        self.completion_policy = policy;
64        self
65    }
66    pub fn manual_completion(mut self) -> Self {
67        self.completion_policy = CompletionPolicy::Manual;
68        self
69    }
70    pub fn automatic_completion(
71        mut self,
72        exercises_threshold: Option<i32>,
73        points_threshold: Option<i32>,
74        requires_exam: bool,
75    ) -> Self {
76        // We'll set the course_module_id when seeding
77        self.completion_policy = CompletionPolicy::Automatic(AutomaticCompletionRequirements {
78            course_module_id: uuid::Uuid::new_v4(), // Temporary, will be updated during seeding
79            number_of_exercises_attempted_treshold: exercises_threshold,
80            number_of_points_treshold: points_threshold,
81            requires_exam,
82        });
83        self
84    }
85
86    pub(crate) async fn seed(
87        self,
88        conn: &mut PgConnection,
89        cx: &SeedContext,
90        course_id: uuid::Uuid,
91        fallback_order: i32,
92    ) -> Result<CourseModule> {
93        let order = self.order.unwrap_or(fallback_order);
94
95        let module = course_modules::insert(
96            conn,
97            headless_lms_models::PKeyPolicy::Generate,
98            &NewCourseModule::new(course_id, self.name, order)
99                .set_ects_credits(self.ects)
100                .set_completion_policy(self.completion_policy.clone()),
101        )
102        .await
103        .with_context(|| format!("inserting module (order {:?})", order))?;
104
105        // Update completion policy if it's automatic to set the correct module ID
106        if let CompletionPolicy::Automatic(mut requirements) = self.completion_policy {
107            requirements.course_module_id = module.id;
108            let updated_policy = CompletionPolicy::Automatic(requirements);
109            course_modules::update_automatic_completion_status(conn, module.id, &updated_policy)
110                .await
111                .context("updating automatic completion policy")?;
112        }
113
114        if self.register_to_open_university {
115            course_modules::update_enable_registering_completion_to_uh_open_university(
116                conn, module.id, true,
117            )
118            .await
119            .context("enabling OU registration for module")?;
120        }
121
122        for ch in self.chapters {
123            ch.seed(conn, cx, course_id, module.id).await?;
124        }
125
126        Ok(module)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use crate::programs::seed::builder::chapter::ChapterBuilder;
133
134    use super::*;
135
136    #[test]
137    fn module_builder_new() {
138        let module = ModuleBuilder::new();
139
140        assert!(module.order.is_none());
141        assert!(module.name.is_none());
142        assert!(module.ects.is_none());
143        assert!(module.chapters.is_empty());
144        assert!(!module.register_to_open_university);
145        assert_eq!(module.completion_policy, CompletionPolicy::Manual);
146    }
147
148    #[test]
149    fn module_builder_order() {
150        let module = ModuleBuilder::new().order(5);
151
152        assert_eq!(module.order, Some(5));
153    }
154
155    #[test]
156    fn module_builder_name() {
157        let module = ModuleBuilder::new().name("Test Module");
158
159        assert_eq!(module.name, Some("Test Module".to_string()));
160    }
161
162    #[test]
163    fn module_builder_name_string_conversion() {
164        let module1 = ModuleBuilder::new().name("String literal");
165        let module2 = ModuleBuilder::new().name(String::from("Owned string"));
166
167        assert_eq!(module1.name, Some("String literal".to_string()));
168        assert_eq!(module2.name, Some("Owned string".to_string()));
169    }
170
171    #[test]
172    fn module_builder_ects() {
173        let module = ModuleBuilder::new().ects(5.0);
174
175        assert_eq!(module.ects, Some(5.0));
176    }
177
178    #[test]
179    fn module_builder_ects_fractional() {
180        let module = ModuleBuilder::new().ects(2.5);
181
182        assert_eq!(module.ects, Some(2.5));
183    }
184
185    #[test]
186    fn module_builder_register_to_open_university_true() {
187        let module = ModuleBuilder::new().register_to_open_university(true);
188
189        assert!(module.register_to_open_university);
190    }
191
192    #[test]
193    fn module_builder_register_to_open_university_false() {
194        let module = ModuleBuilder::new().register_to_open_university(false);
195
196        assert!(!module.register_to_open_university);
197    }
198
199    #[test]
200    fn module_builder_chapter() {
201        let chapter = ChapterBuilder::new(1, "Test Chapter");
202        let module = ModuleBuilder::new().chapter(chapter);
203
204        assert_eq!(module.chapters.len(), 1);
205        assert_eq!(module.chapters[0].number, 1);
206        assert_eq!(module.chapters[0].name, "Test Chapter");
207    }
208
209    #[test]
210    fn module_builder_multiple_chapters() {
211        let chapter1 = ChapterBuilder::new(1, "Chapter 1");
212        let chapter2 = ChapterBuilder::new(2, "Chapter 2");
213        let module = ModuleBuilder::new().chapter(chapter1).chapter(chapter2);
214
215        assert_eq!(module.chapters.len(), 2);
216        assert_eq!(module.chapters[0].number, 1);
217        assert_eq!(module.chapters[0].name, "Chapter 1");
218        assert_eq!(module.chapters[1].number, 2);
219        assert_eq!(module.chapters[1].name, "Chapter 2");
220    }
221
222    #[test]
223    fn module_builder_fluent_interface() {
224        let chapter1 = ChapterBuilder::new(1, "Chapter 1");
225        let chapter2 = ChapterBuilder::new(2, "Chapter 2");
226
227        let module = ModuleBuilder::new()
228            .order(1)
229            .name("Advanced Module")
230            .ects(3.5)
231            .register_to_open_university(true)
232            .chapter(chapter1)
233            .chapter(chapter2);
234
235        assert_eq!(module.order, Some(1));
236        assert_eq!(module.name, Some("Advanced Module".to_string()));
237        assert_eq!(module.ects, Some(3.5));
238        assert!(module.register_to_open_university);
239        assert_eq!(module.chapters.len(), 2);
240    }
241
242    #[test]
243    fn module_builder_method_chaining_order() {
244        let chapter1 = ChapterBuilder::new(1, "Test Chapter");
245        let chapter2 = ChapterBuilder::new(1, "Test Chapter");
246
247        let module1 = ModuleBuilder::new()
248            .order(1)
249            .name("Module 1")
250            .ects(2.0)
251            .register_to_open_university(true)
252            .chapter(chapter1);
253
254        let module2 = ModuleBuilder::new()
255            .register_to_open_university(true)
256            .chapter(chapter2)
257            .ects(2.0)
258            .name("Module 1")
259            .order(1);
260
261        assert_eq!(module1.name, module2.name);
262        assert_eq!(module1.ects, module2.ects);
263        assert_eq!(
264            module1.register_to_open_university,
265            module2.register_to_open_university
266        );
267        assert_eq!(module1.chapters.len(), module2.chapters.len());
268        assert_eq!(module1.order, module2.order);
269    }
270
271    #[test]
272    fn module_builder_default_values() {
273        let module = ModuleBuilder::new();
274
275        assert!(module.order.is_none());
276        assert!(module.name.is_none());
277        assert!(module.ects.is_none());
278        assert!(module.chapters.is_empty());
279        assert!(!module.register_to_open_university);
280    }
281
282    #[test]
283    fn module_builder_order_preservation() {
284        let module = ModuleBuilder::new().order(999);
285
286        assert_eq!(module.order, Some(999));
287    }
288
289    #[test]
290    fn module_builder_manual_completion() {
291        let module = ModuleBuilder::new().manual_completion();
292
293        assert_eq!(module.completion_policy, CompletionPolicy::Manual);
294    }
295
296    #[test]
297    fn module_builder_automatic_completion() {
298        let module = ModuleBuilder::new().automatic_completion(Some(5), Some(100), true);
299
300        match module.completion_policy {
301            CompletionPolicy::Automatic(requirements) => {
302                assert_eq!(requirements.number_of_exercises_attempted_treshold, Some(5));
303                assert_eq!(requirements.number_of_points_treshold, Some(100));
304                assert!(requirements.requires_exam);
305            }
306            CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
307        }
308    }
309
310    #[test]
311    fn module_builder_automatic_completion_no_thresholds() {
312        let module = ModuleBuilder::new().automatic_completion(None, None, false);
313
314        match module.completion_policy {
315            CompletionPolicy::Automatic(requirements) => {
316                assert_eq!(requirements.number_of_exercises_attempted_treshold, None);
317                assert_eq!(requirements.number_of_points_treshold, None);
318                assert!(!requirements.requires_exam);
319            }
320            CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
321        }
322    }
323
324    #[test]
325    fn module_builder_completion_policy_fluent_interface() {
326        let chapter1 = ChapterBuilder::new(1, "Chapter 1");
327        let chapter2 = ChapterBuilder::new(2, "Chapter 2");
328
329        let module = ModuleBuilder::new()
330            .order(1)
331            .name("Advanced Module")
332            .ects(3.5)
333            .register_to_open_university(true)
334            .automatic_completion(Some(10), Some(200), true)
335            .chapter(chapter1)
336            .chapter(chapter2);
337
338        assert_eq!(module.order, Some(1));
339        assert_eq!(module.name, Some("Advanced Module".to_string()));
340        assert_eq!(module.ects, Some(3.5));
341        assert!(module.register_to_open_university);
342        assert_eq!(module.chapters.len(), 2);
343
344        match module.completion_policy {
345            CompletionPolicy::Automatic(requirements) => {
346                assert_eq!(
347                    requirements.number_of_exercises_attempted_treshold,
348                    Some(10)
349                );
350                assert_eq!(requirements.number_of_points_treshold, Some(200));
351                assert!(requirements.requires_exam);
352            }
353            CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
354        }
355    }
356
357    #[test]
358    fn module_builder_completion_policy_override() {
359        let module1 = ModuleBuilder::new().manual_completion();
360        let module2 = ModuleBuilder::new()
361            .automatic_completion(Some(5), Some(100), false)
362            .manual_completion();
363
364        assert_eq!(module1.completion_policy, CompletionPolicy::Manual);
365        assert_eq!(module2.completion_policy, CompletionPolicy::Manual);
366    }
367}