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