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 register_credits_via_suotar: bool,
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 register_credits_via_suotar,
146 };
147
148 let completion_id = course_module_completions::insert_seed_row(conn, &seed).await?;
149
150 course_module_completions::update_completion_registration_attempt_date(
152 conn,
153 completion_id,
154 Utc::now(),
155 )
156 .await
157 .ok();
158
159 if let Some(r) = &self.register {
160 let registrar_id = if let Some(id) = r.registrar_id {
161 id
162 } else if let Some(def) = default_registrar_id {
163 def
164 } else {
165 return Ok(());
166 };
167
168 if let Some(student_number) = &r.real_student_number {
169 course_module_completion_registered_to_study_registries::insert_or_ignore(
170 conn,
171 &course_module_completion_registered_to_study_registries::NewCourseModuleCompletionRegisteredToStudyRegistry {
172 course_id,
173 course_module_completion_id: completion_id,
174 course_module_id,
175 study_registry_registrar_id: registrar_id,
176 user_id: self.user_id,
177 real_student_number: student_number.clone(),
178 },
179 )
180 .await
181 .context("insert registry record")?;
182 }
183 }
184
185 Ok(())
186 }
187}
188
189#[derive(Debug, Clone, Default)]
191pub struct CreditRegistrationSeed {
192 pub enrolment_link: Option<String>,
194 pub paused_reason: Option<String>,
197 pub registers_seeded_completions: bool,
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 completion_policy: CompletionPolicy,
210 pub completions: Vec<CompletionBuilder>,
211 pub default_registrar_id: Option<Uuid>,
212 pub uh_course_code: Option<String>,
213 pub credit_registration: Option<CreditRegistrationSeed>,
214}
215
216impl Default for ModuleBuilder {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222impl ModuleBuilder {
223 pub fn new() -> Self {
224 Self {
225 name: None,
226 order: None,
227 ects: None,
228 chapters: vec![],
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 chapter(mut self, c: ChapterBuilder) -> Self {
271 self.chapters.push(c);
272 self
273 }
274 pub fn chapters<I: IntoIterator<Item = ChapterBuilder>>(mut self, it: I) -> Self {
275 self.chapters.extend(it);
276 self
277 }
278 pub fn completion_policy(mut self, policy: CompletionPolicy) -> Self {
279 self.completion_policy = policy;
280 self
281 }
282 pub fn manual_completion(mut self) -> Self {
283 self.completion_policy = CompletionPolicy::Manual;
284 self
285 }
286 pub fn automatic_completion(
287 mut self,
288 exercises_threshold: Option<i32>,
289 points_threshold: Option<i32>,
290 requires_exam: bool,
291 ) -> Self {
292 self.completion_policy = CompletionPolicy::Automatic(AutomaticCompletionRequirements {
294 course_module_id: uuid::Uuid::new_v4(), number_of_exercises_attempted_treshold: exercises_threshold,
296 number_of_points_treshold: points_threshold,
297 requires_exam,
298 });
299 self
300 }
301
302 pub(crate) async fn seed(
303 self,
304 conn: &mut PgConnection,
305 cx: &SeedContext,
306 course_id: uuid::Uuid,
307 fallback_order: i32,
308 ) -> Result<CourseModule> {
309 let order = self.order.unwrap_or(fallback_order);
310
311 let module = course_modules::insert(
312 conn,
313 headless_lms_models::PKeyPolicy::Generate,
314 &NewCourseModule::new(course_id, self.name, order)
315 .set_ects_credits(self.ects)
316 .set_completion_policy(self.completion_policy.clone())
317 .set_uh_course_code(self.uh_course_code)
318 .set_completion_registration_link_override(
319 self.credit_registration
320 .as_ref()
321 .and_then(|seed| seed.enrolment_link.clone()),
322 ),
323 )
324 .await
325 .with_context(|| format!("inserting module (order {:?})", order))?;
326
327 if let CompletionPolicy::Automatic(mut requirements) = self.completion_policy {
329 requirements.course_module_id = module.id;
330 let updated_policy = CompletionPolicy::Automatic(requirements);
331 course_modules::update_automatic_completion_status(conn, module.id, &updated_policy)
332 .await
333 .context("updating automatic completion policy")?;
334 }
335
336 if let Some(credit_registration) = &self.credit_registration {
337 course_module_suotar_configurations::ensure_exists(conn, module.id)
338 .await
339 .context("writing the module's credit registration configuration")?;
340 if let Some(reason) = &credit_registration.paused_reason {
341 course_module_suotar_configurations::set_paused(
342 conn,
343 module.id,
344 Some(SuotarPause {
345 paused_at: Utc::now(),
346 paused_by_user_id: cx.teacher,
347 reason: Some(reason),
348 }),
349 )
350 .await
351 .context("pausing the module's credit registration")?;
352 }
353 }
354
355 let registers_seeded_completions = self
356 .credit_registration
357 .as_ref()
358 .is_some_and(|seed| seed.registers_seeded_completions);
359 for comp in &self.completions {
360 comp.seed(
361 conn,
362 course_id,
363 module.id,
364 self.default_registrar_id,
365 registers_seeded_completions,
366 )
367 .await?;
368 }
369
370 for ch in self.chapters {
371 ch.seed(conn, cx, course_id, module.id).await?;
372 }
373
374 Ok(module)
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use crate::programs::seed::builder::chapter::ChapterBuilder;
381
382 use super::*;
383
384 #[test]
385 fn module_builder_new() {
386 let module = ModuleBuilder::new();
387
388 assert!(module.order.is_none());
389 assert!(module.name.is_none());
390 assert!(module.ects.is_none());
391 assert!(module.chapters.is_empty());
392 assert_eq!(module.completion_policy, CompletionPolicy::Manual);
393 }
394
395 #[test]
396 fn module_builder_order() {
397 let module = ModuleBuilder::new().order(5);
398
399 assert_eq!(module.order, Some(5));
400 }
401
402 #[test]
403 fn module_builder_name() {
404 let module = ModuleBuilder::new().name("Test Module");
405
406 assert_eq!(module.name, Some("Test Module".to_string()));
407 }
408
409 #[test]
410 fn module_builder_name_string_conversion() {
411 let module1 = ModuleBuilder::new().name("String literal");
412 let module2 = ModuleBuilder::new().name(String::from("Owned string"));
413
414 assert_eq!(module1.name, Some("String literal".to_string()));
415 assert_eq!(module2.name, Some("Owned string".to_string()));
416 }
417
418 #[test]
419 fn module_builder_ects() {
420 let module = ModuleBuilder::new().ects(5.0);
421
422 assert_eq!(module.ects, Some(5.0));
423 }
424
425 #[test]
426 fn module_builder_ects_fractional() {
427 let module = ModuleBuilder::new().ects(2.5);
428
429 assert_eq!(module.ects, Some(2.5));
430 }
431
432 #[test]
433 fn module_builder_chapter() {
434 let chapter = ChapterBuilder::new(1, "Test Chapter");
435 let module = ModuleBuilder::new().chapter(chapter);
436
437 assert_eq!(module.chapters.len(), 1);
438 assert_eq!(module.chapters[0].number, 1);
439 assert_eq!(module.chapters[0].name, "Test Chapter");
440 }
441
442 #[test]
443 fn module_builder_multiple_chapters() {
444 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
445 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
446 let module = ModuleBuilder::new().chapter(chapter1).chapter(chapter2);
447
448 assert_eq!(module.chapters.len(), 2);
449 assert_eq!(module.chapters[0].number, 1);
450 assert_eq!(module.chapters[0].name, "Chapter 1");
451 assert_eq!(module.chapters[1].number, 2);
452 assert_eq!(module.chapters[1].name, "Chapter 2");
453 }
454
455 #[test]
456 fn module_builder_fluent_interface() {
457 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
458 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
459
460 let module = ModuleBuilder::new()
461 .order(1)
462 .name("Advanced Module")
463 .ects(3.5)
464 .chapter(chapter1)
465 .chapter(chapter2);
466
467 assert_eq!(module.order, Some(1));
468 assert_eq!(module.name, Some("Advanced Module".to_string()));
469 assert_eq!(module.ects, Some(3.5));
470 assert_eq!(module.chapters.len(), 2);
471 }
472
473 #[test]
474 fn module_builder_method_chaining_order() {
475 let chapter1 = ChapterBuilder::new(1, "Test Chapter");
476 let chapter2 = ChapterBuilder::new(1, "Test Chapter");
477
478 let module1 = ModuleBuilder::new()
479 .order(1)
480 .name("Module 1")
481 .ects(2.0)
482 .chapter(chapter1);
483
484 let module2 = ModuleBuilder::new()
485 .chapter(chapter2)
486 .ects(2.0)
487 .name("Module 1")
488 .order(1);
489
490 assert_eq!(module1.name, module2.name);
491 assert_eq!(module1.ects, module2.ects);
492 assert_eq!(module1.chapters.len(), module2.chapters.len());
493 assert_eq!(module1.order, module2.order);
494 }
495
496 #[test]
497 fn module_builder_default_values() {
498 let module = ModuleBuilder::new();
499
500 assert!(module.order.is_none());
501 assert!(module.name.is_none());
502 assert!(module.ects.is_none());
503 assert!(module.chapters.is_empty());
504 }
505
506 #[test]
507 fn module_builder_order_preservation() {
508 let module = ModuleBuilder::new().order(999);
509
510 assert_eq!(module.order, Some(999));
511 }
512
513 #[test]
514 fn module_builder_manual_completion() {
515 let module = ModuleBuilder::new().manual_completion();
516
517 assert_eq!(module.completion_policy, CompletionPolicy::Manual);
518 }
519
520 #[test]
521 fn module_builder_automatic_completion() {
522 let module = ModuleBuilder::new().automatic_completion(Some(5), Some(100), true);
523
524 match module.completion_policy {
525 CompletionPolicy::Automatic(requirements) => {
526 assert_eq!(requirements.number_of_exercises_attempted_treshold, Some(5));
527 assert_eq!(requirements.number_of_points_treshold, Some(100));
528 assert!(requirements.requires_exam);
529 }
530 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
531 }
532 }
533
534 #[test]
535 fn module_builder_automatic_completion_no_thresholds() {
536 let module = ModuleBuilder::new().automatic_completion(None, None, false);
537
538 match module.completion_policy {
539 CompletionPolicy::Automatic(requirements) => {
540 assert_eq!(requirements.number_of_exercises_attempted_treshold, None);
541 assert_eq!(requirements.number_of_points_treshold, None);
542 assert!(!requirements.requires_exam);
543 }
544 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
545 }
546 }
547
548 #[test]
549 fn module_builder_completion_policy_fluent_interface() {
550 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
551 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
552
553 let module = ModuleBuilder::new()
554 .order(1)
555 .name("Advanced Module")
556 .ects(3.5)
557 .automatic_completion(Some(10), Some(200), true)
558 .chapter(chapter1)
559 .chapter(chapter2);
560
561 assert_eq!(module.order, Some(1));
562 assert_eq!(module.name, Some("Advanced Module".to_string()));
563 assert_eq!(module.ects, Some(3.5));
564 assert_eq!(module.chapters.len(), 2);
565
566 match module.completion_policy {
567 CompletionPolicy::Automatic(requirements) => {
568 assert_eq!(
569 requirements.number_of_exercises_attempted_treshold,
570 Some(10)
571 );
572 assert_eq!(requirements.number_of_points_treshold, Some(200));
573 assert!(requirements.requires_exam);
574 }
575 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
576 }
577 }
578
579 #[test]
580 fn module_builder_completion_policy_override() {
581 let module1 = ModuleBuilder::new().manual_completion();
582 let module2 = ModuleBuilder::new()
583 .automatic_completion(Some(5), Some(100), false)
584 .manual_completion();
585
586 assert_eq!(module1.completion_policy, CompletionPolicy::Manual);
587 assert_eq!(module2.completion_policy, CompletionPolicy::Manual);
588 }
589}