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