Skip to main content

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};
9use chrono::{DateTime, Utc};
10use headless_lms_models::{
11    course_module_completion_registered_to_study_registries, course_module_completions,
12    course_module_completions::NewCourseModuleCompletionSeed,
13};
14
15use uuid::Uuid;
16
17#[derive(Debug, Clone)]
18pub struct CompletionRegisteredBuilder {
19    pub registrar_id: Option<Uuid>,
20    pub real_student_number: Option<String>,
21}
22
23impl CompletionRegisteredBuilder {
24    pub fn new() -> Self {
25        Self {
26            registrar_id: None,
27            real_student_number: None,
28        }
29    }
30
31    pub fn registrar_id(mut self, id: Uuid) -> Self {
32        self.registrar_id = Some(id);
33        self
34    }
35
36    pub fn real_student_number(mut self, num: impl Into<String>) -> Self {
37        self.real_student_number = Some(num.into());
38        self
39    }
40}
41
42impl Default for CompletionRegisteredBuilder {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48/// Builder for seeding a single course_module_completion row.
49#[derive(Debug, Clone)]
50pub struct CompletionBuilder {
51    user_id: Uuid,
52    email: Option<String>,
53    grade: Option<i32>,
54    passed: Option<bool>,
55    completion_date: Option<DateTime<Utc>>,
56    completion_language: Option<String>,
57    eligible_for_ects: Option<bool>,
58    prerequisite_modules_completed: Option<bool>,
59    needs_to_be_reviewed: Option<bool>,
60    register: Option<CompletionRegisteredBuilder>,
61}
62
63impl CompletionBuilder {
64    pub fn new(user_id: Uuid) -> Self {
65        Self {
66            user_id,
67            email: Some(format!("{}@example.com", user_id)),
68            grade: None,
69            passed: Some(true),
70            completion_date: Some(chrono::Utc::now()),
71            completion_language: Some("en-US".to_string()),
72            eligible_for_ects: Some(true),
73            prerequisite_modules_completed: Some(false),
74            needs_to_be_reviewed: Some(false),
75            register: None,
76        }
77    }
78
79    pub fn registered(mut self, r: CompletionRegisteredBuilder) -> Self {
80        self.register = Some(r);
81        self
82    }
83
84    pub fn email(mut self, v: impl Into<String>) -> Self {
85        self.email = Some(v.into());
86        self
87    }
88
89    pub fn grade(mut self, v: i32) -> Self {
90        self.grade = Some(v);
91        self
92    }
93
94    pub fn passed(mut self, v: bool) -> Self {
95        self.passed = Some(v);
96        self
97    }
98
99    pub fn completion_date(mut self, v: DateTime<Utc>) -> Self {
100        self.completion_date = Some(v);
101        self
102    }
103
104    pub fn completion_language(mut self, v: impl Into<String>) -> Self {
105        self.completion_language = Some(v.into());
106        self
107    }
108
109    pub fn eligible_for_ects(mut self, v: bool) -> Self {
110        self.eligible_for_ects = Some(v);
111        self
112    }
113
114    pub fn prerequisite_modules_completed(mut self, v: bool) -> Self {
115        self.prerequisite_modules_completed = Some(v);
116        self
117    }
118
119    pub fn needs_to_be_reviewed(mut self, v: bool) -> Self {
120        self.needs_to_be_reviewed = Some(v);
121        self
122    }
123
124    pub async fn seed(
125        &self,
126        conn: &mut sqlx::PgConnection,
127        course_id: Uuid,
128        course_module_id: Uuid,
129        default_registrar_id: Option<Uuid>,
130    ) -> anyhow::Result<()> {
131        let seed = NewCourseModuleCompletionSeed {
132            course_id,
133            course_module_id,
134            user_id: self.user_id,
135            completion_date: self.completion_date,
136            completion_language: self.completion_language.clone(),
137            eligible_for_ects: self.eligible_for_ects,
138            email: self.email.clone(),
139            grade: self.grade,
140            passed: self.passed,
141            prerequisite_modules_completed: self.prerequisite_modules_completed,
142            needs_to_be_reviewed: self.needs_to_be_reviewed,
143        };
144
145        let completion_id = course_module_completions::insert_seed_row(conn, &seed).await?;
146
147        // Mark registration attempt (seed always sets this)
148        course_module_completions::update_registration_attempt(conn, completion_id)
149            .await
150            .ok();
151
152        if let Some(r) = &self.register {
153            let registrar_id = if let Some(id) = r.registrar_id {
154                id
155            } else if let Some(def) = default_registrar_id {
156                def
157            } else {
158                return Ok(());
159            };
160
161            if let Some(student_number) = &r.real_student_number {
162                course_module_completion_registered_to_study_registries::insert_record(
163                    conn,
164                    course_id,
165                    completion_id,
166                    course_module_id,
167                    registrar_id,
168                    self.user_id,
169                    student_number,
170                )
171                .await
172                .context("insert registry record")?;
173            }
174        }
175
176        Ok(())
177    }
178}
179
180/// Builder for course modules that group chapters with ECTS credits and Open University registration.
181#[derive(Debug, Clone)]
182pub struct ModuleBuilder {
183    pub name: Option<String>,
184    pub order: Option<i32>,
185    pub ects: Option<f32>,
186    pub chapters: Vec<ChapterBuilder>,
187    pub register_to_open_university: bool,
188    pub completion_policy: CompletionPolicy,
189    pub completions: Vec<CompletionBuilder>,
190    pub default_registrar_id: Option<Uuid>,
191    pub uh_course_code: Option<String>,
192}
193
194impl Default for ModuleBuilder {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200impl ModuleBuilder {
201    pub fn new() -> Self {
202        Self {
203            name: None,
204            order: None,
205            ects: None,
206            chapters: vec![],
207            register_to_open_university: false,
208            completion_policy: CompletionPolicy::Manual,
209            completions: vec![],
210            default_registrar_id: None,
211            uh_course_code: None,
212        }
213    }
214    pub fn default_registrar(mut self, id: Uuid) -> Self {
215        self.default_registrar_id = Some(id);
216        self
217    }
218    pub fn completion(mut self, c: CompletionBuilder) -> Self {
219        self.completions.push(c);
220        self
221    }
222
223    pub fn completions<I: IntoIterator<Item = CompletionBuilder>>(mut self, it: I) -> Self {
224        self.completions.extend(it);
225        self
226    }
227
228    pub fn order(mut self, n: i32) -> Self {
229        self.order = Some(n);
230        self
231    }
232    pub fn name(mut self, n: impl Into<String>) -> Self {
233        self.name = Some(n.into());
234        self
235    }
236    pub fn ects(mut self, e: f32) -> Self {
237        self.ects = Some(e);
238        self
239    }
240    pub fn uh_course_code(mut self, c: String) -> Self {
241        self.uh_course_code = Some(c);
242        self
243    }
244    pub fn register_to_open_university(mut self, v: bool) -> Self {
245        self.register_to_open_university = v;
246        self
247    }
248    pub fn chapter(mut self, c: ChapterBuilder) -> Self {
249        self.chapters.push(c);
250        self
251    }
252    pub fn chapters<I: IntoIterator<Item = ChapterBuilder>>(mut self, it: I) -> Self {
253        self.chapters.extend(it);
254        self
255    }
256    pub fn completion_policy(mut self, policy: CompletionPolicy) -> Self {
257        self.completion_policy = policy;
258        self
259    }
260    pub fn manual_completion(mut self) -> Self {
261        self.completion_policy = CompletionPolicy::Manual;
262        self
263    }
264    pub fn automatic_completion(
265        mut self,
266        exercises_threshold: Option<i32>,
267        points_threshold: Option<i32>,
268        requires_exam: bool,
269    ) -> Self {
270        // We'll set the course_module_id when seeding
271        self.completion_policy = CompletionPolicy::Automatic(AutomaticCompletionRequirements {
272            course_module_id: uuid::Uuid::new_v4(), // Temporary, will be updated during seeding
273            number_of_exercises_attempted_treshold: exercises_threshold,
274            number_of_points_treshold: points_threshold,
275            requires_exam,
276        });
277        self
278    }
279
280    pub(crate) async fn seed(
281        self,
282        conn: &mut PgConnection,
283        cx: &SeedContext,
284        course_id: uuid::Uuid,
285        fallback_order: i32,
286    ) -> Result<CourseModule> {
287        let order = self.order.unwrap_or(fallback_order);
288
289        let module = course_modules::insert(
290            conn,
291            headless_lms_models::PKeyPolicy::Generate,
292            &NewCourseModule::new(course_id, self.name, order)
293                .set_ects_credits(self.ects)
294                .set_completion_policy(self.completion_policy.clone())
295                .set_uh_course_code(self.uh_course_code),
296        )
297        .await
298        .with_context(|| format!("inserting module (order {:?})", order))?;
299
300        // Update completion policy if it's automatic to set the correct module ID
301        if let CompletionPolicy::Automatic(mut requirements) = self.completion_policy {
302            requirements.course_module_id = module.id;
303            let updated_policy = CompletionPolicy::Automatic(requirements);
304            course_modules::update_automatic_completion_status(conn, module.id, &updated_policy)
305                .await
306                .context("updating automatic completion policy")?;
307        }
308
309        if self.register_to_open_university {
310            course_modules::update_enable_registering_completion_to_uh_open_university(
311                conn, module.id, true,
312            )
313            .await
314            .context("enabling OU registration for module")?;
315        }
316
317        for comp in &self.completions {
318            comp.seed(conn, course_id, module.id, self.default_registrar_id)
319                .await?;
320        }
321
322        for ch in self.chapters {
323            ch.seed(conn, cx, course_id, module.id).await?;
324        }
325
326        Ok(module)
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use crate::programs::seed::builder::chapter::ChapterBuilder;
333
334    use super::*;
335
336    #[test]
337    fn module_builder_new() {
338        let module = ModuleBuilder::new();
339
340        assert!(module.order.is_none());
341        assert!(module.name.is_none());
342        assert!(module.ects.is_none());
343        assert!(module.chapters.is_empty());
344        assert!(!module.register_to_open_university);
345        assert_eq!(module.completion_policy, CompletionPolicy::Manual);
346    }
347
348    #[test]
349    fn module_builder_order() {
350        let module = ModuleBuilder::new().order(5);
351
352        assert_eq!(module.order, Some(5));
353    }
354
355    #[test]
356    fn module_builder_name() {
357        let module = ModuleBuilder::new().name("Test Module");
358
359        assert_eq!(module.name, Some("Test Module".to_string()));
360    }
361
362    #[test]
363    fn module_builder_name_string_conversion() {
364        let module1 = ModuleBuilder::new().name("String literal");
365        let module2 = ModuleBuilder::new().name(String::from("Owned string"));
366
367        assert_eq!(module1.name, Some("String literal".to_string()));
368        assert_eq!(module2.name, Some("Owned string".to_string()));
369    }
370
371    #[test]
372    fn module_builder_ects() {
373        let module = ModuleBuilder::new().ects(5.0);
374
375        assert_eq!(module.ects, Some(5.0));
376    }
377
378    #[test]
379    fn module_builder_ects_fractional() {
380        let module = ModuleBuilder::new().ects(2.5);
381
382        assert_eq!(module.ects, Some(2.5));
383    }
384
385    #[test]
386    fn module_builder_register_to_open_university_true() {
387        let module = ModuleBuilder::new().register_to_open_university(true);
388
389        assert!(module.register_to_open_university);
390    }
391
392    #[test]
393    fn module_builder_register_to_open_university_false() {
394        let module = ModuleBuilder::new().register_to_open_university(false);
395
396        assert!(!module.register_to_open_university);
397    }
398
399    #[test]
400    fn module_builder_chapter() {
401        let chapter = ChapterBuilder::new(1, "Test Chapter");
402        let module = ModuleBuilder::new().chapter(chapter);
403
404        assert_eq!(module.chapters.len(), 1);
405        assert_eq!(module.chapters[0].number, 1);
406        assert_eq!(module.chapters[0].name, "Test Chapter");
407    }
408
409    #[test]
410    fn module_builder_multiple_chapters() {
411        let chapter1 = ChapterBuilder::new(1, "Chapter 1");
412        let chapter2 = ChapterBuilder::new(2, "Chapter 2");
413        let module = ModuleBuilder::new().chapter(chapter1).chapter(chapter2);
414
415        assert_eq!(module.chapters.len(), 2);
416        assert_eq!(module.chapters[0].number, 1);
417        assert_eq!(module.chapters[0].name, "Chapter 1");
418        assert_eq!(module.chapters[1].number, 2);
419        assert_eq!(module.chapters[1].name, "Chapter 2");
420    }
421
422    #[test]
423    fn module_builder_fluent_interface() {
424        let chapter1 = ChapterBuilder::new(1, "Chapter 1");
425        let chapter2 = ChapterBuilder::new(2, "Chapter 2");
426
427        let module = ModuleBuilder::new()
428            .order(1)
429            .name("Advanced Module")
430            .ects(3.5)
431            .register_to_open_university(true)
432            .chapter(chapter1)
433            .chapter(chapter2);
434
435        assert_eq!(module.order, Some(1));
436        assert_eq!(module.name, Some("Advanced Module".to_string()));
437        assert_eq!(module.ects, Some(3.5));
438        assert!(module.register_to_open_university);
439        assert_eq!(module.chapters.len(), 2);
440    }
441
442    #[test]
443    fn module_builder_method_chaining_order() {
444        let chapter1 = ChapterBuilder::new(1, "Test Chapter");
445        let chapter2 = ChapterBuilder::new(1, "Test Chapter");
446
447        let module1 = ModuleBuilder::new()
448            .order(1)
449            .name("Module 1")
450            .ects(2.0)
451            .register_to_open_university(true)
452            .chapter(chapter1);
453
454        let module2 = ModuleBuilder::new()
455            .register_to_open_university(true)
456            .chapter(chapter2)
457            .ects(2.0)
458            .name("Module 1")
459            .order(1);
460
461        assert_eq!(module1.name, module2.name);
462        assert_eq!(module1.ects, module2.ects);
463        assert_eq!(
464            module1.register_to_open_university,
465            module2.register_to_open_university
466        );
467        assert_eq!(module1.chapters.len(), module2.chapters.len());
468        assert_eq!(module1.order, module2.order);
469    }
470
471    #[test]
472    fn module_builder_default_values() {
473        let module = ModuleBuilder::new();
474
475        assert!(module.order.is_none());
476        assert!(module.name.is_none());
477        assert!(module.ects.is_none());
478        assert!(module.chapters.is_empty());
479        assert!(!module.register_to_open_university);
480    }
481
482    #[test]
483    fn module_builder_order_preservation() {
484        let module = ModuleBuilder::new().order(999);
485
486        assert_eq!(module.order, Some(999));
487    }
488
489    #[test]
490    fn module_builder_manual_completion() {
491        let module = ModuleBuilder::new().manual_completion();
492
493        assert_eq!(module.completion_policy, CompletionPolicy::Manual);
494    }
495
496    #[test]
497    fn module_builder_automatic_completion() {
498        let module = ModuleBuilder::new().automatic_completion(Some(5), Some(100), true);
499
500        match module.completion_policy {
501            CompletionPolicy::Automatic(requirements) => {
502                assert_eq!(requirements.number_of_exercises_attempted_treshold, Some(5));
503                assert_eq!(requirements.number_of_points_treshold, Some(100));
504                assert!(requirements.requires_exam);
505            }
506            CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
507        }
508    }
509
510    #[test]
511    fn module_builder_automatic_completion_no_thresholds() {
512        let module = ModuleBuilder::new().automatic_completion(None, None, false);
513
514        match module.completion_policy {
515            CompletionPolicy::Automatic(requirements) => {
516                assert_eq!(requirements.number_of_exercises_attempted_treshold, None);
517                assert_eq!(requirements.number_of_points_treshold, None);
518                assert!(!requirements.requires_exam);
519            }
520            CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
521        }
522    }
523
524    #[test]
525    fn module_builder_completion_policy_fluent_interface() {
526        let chapter1 = ChapterBuilder::new(1, "Chapter 1");
527        let chapter2 = ChapterBuilder::new(2, "Chapter 2");
528
529        let module = ModuleBuilder::new()
530            .order(1)
531            .name("Advanced Module")
532            .ects(3.5)
533            .register_to_open_university(true)
534            .automatic_completion(Some(10), Some(200), true)
535            .chapter(chapter1)
536            .chapter(chapter2);
537
538        assert_eq!(module.order, Some(1));
539        assert_eq!(module.name, Some("Advanced Module".to_string()));
540        assert_eq!(module.ects, Some(3.5));
541        assert!(module.register_to_open_university);
542        assert_eq!(module.chapters.len(), 2);
543
544        match module.completion_policy {
545            CompletionPolicy::Automatic(requirements) => {
546                assert_eq!(
547                    requirements.number_of_exercises_attempted_treshold,
548                    Some(10)
549                );
550                assert_eq!(requirements.number_of_points_treshold, Some(200));
551                assert!(requirements.requires_exam);
552            }
553            CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
554        }
555    }
556
557    #[test]
558    fn module_builder_completion_policy_override() {
559        let module1 = ModuleBuilder::new().manual_completion();
560        let module2 = ModuleBuilder::new()
561            .automatic_completion(Some(5), Some(100), false)
562            .manual_completion();
563
564        assert_eq!(module1.completion_policy, CompletionPolicy::Manual);
565        assert_eq!(module2.completion_policy, CompletionPolicy::Manual);
566    }
567}