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