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#[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 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#[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}
192
193impl Default for ModuleBuilder {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199impl ModuleBuilder {
200 pub fn new() -> Self {
201 Self {
202 name: None,
203 order: None,
204 ects: None,
205 chapters: vec![],
206 register_to_open_university: false,
207 completion_policy: CompletionPolicy::Manual,
208 completions: vec![],
209 default_registrar_id: None,
210 }
211 }
212 pub fn default_registrar(mut self, id: Uuid) -> Self {
213 self.default_registrar_id = Some(id);
214 self
215 }
216 pub fn completion(mut self, c: CompletionBuilder) -> Self {
217 self.completions.push(c);
218 self
219 }
220
221 pub fn completions<I: IntoIterator<Item = CompletionBuilder>>(mut self, it: I) -> Self {
222 self.completions.extend(it);
223 self
224 }
225
226 pub fn order(mut self, n: i32) -> Self {
227 self.order = Some(n);
228 self
229 }
230 pub fn name(mut self, n: impl Into<String>) -> Self {
231 self.name = Some(n.into());
232 self
233 }
234 pub fn ects(mut self, e: f32) -> Self {
235 self.ects = Some(e);
236 self
237 }
238 pub fn register_to_open_university(mut self, v: bool) -> Self {
239 self.register_to_open_university = v;
240 self
241 }
242 pub fn chapter(mut self, c: ChapterBuilder) -> Self {
243 self.chapters.push(c);
244 self
245 }
246 pub fn chapters<I: IntoIterator<Item = ChapterBuilder>>(mut self, it: I) -> Self {
247 self.chapters.extend(it);
248 self
249 }
250 pub fn completion_policy(mut self, policy: CompletionPolicy) -> Self {
251 self.completion_policy = policy;
252 self
253 }
254 pub fn manual_completion(mut self) -> Self {
255 self.completion_policy = CompletionPolicy::Manual;
256 self
257 }
258 pub fn automatic_completion(
259 mut self,
260 exercises_threshold: Option<i32>,
261 points_threshold: Option<i32>,
262 requires_exam: bool,
263 ) -> Self {
264 self.completion_policy = CompletionPolicy::Automatic(AutomaticCompletionRequirements {
266 course_module_id: uuid::Uuid::new_v4(), number_of_exercises_attempted_treshold: exercises_threshold,
268 number_of_points_treshold: points_threshold,
269 requires_exam,
270 });
271 self
272 }
273
274 pub(crate) async fn seed(
275 self,
276 conn: &mut PgConnection,
277 cx: &SeedContext,
278 course_id: uuid::Uuid,
279 fallback_order: i32,
280 ) -> Result<CourseModule> {
281 let order = self.order.unwrap_or(fallback_order);
282
283 let module = course_modules::insert(
284 conn,
285 headless_lms_models::PKeyPolicy::Generate,
286 &NewCourseModule::new(course_id, self.name, order)
287 .set_ects_credits(self.ects)
288 .set_completion_policy(self.completion_policy.clone()),
289 )
290 .await
291 .with_context(|| format!("inserting module (order {:?})", order))?;
292
293 if let CompletionPolicy::Automatic(mut requirements) = self.completion_policy {
295 requirements.course_module_id = module.id;
296 let updated_policy = CompletionPolicy::Automatic(requirements);
297 course_modules::update_automatic_completion_status(conn, module.id, &updated_policy)
298 .await
299 .context("updating automatic completion policy")?;
300 }
301
302 if self.register_to_open_university {
303 course_modules::update_enable_registering_completion_to_uh_open_university(
304 conn, module.id, true,
305 )
306 .await
307 .context("enabling OU registration for module")?;
308 }
309
310 for comp in &self.completions {
311 comp.seed(conn, course_id, module.id, self.default_registrar_id)
312 .await?;
313 }
314
315 for ch in self.chapters {
316 ch.seed(conn, cx, course_id, module.id).await?;
317 }
318
319 Ok(module)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use crate::programs::seed::builder::chapter::ChapterBuilder;
326
327 use super::*;
328
329 #[test]
330 fn module_builder_new() {
331 let module = ModuleBuilder::new();
332
333 assert!(module.order.is_none());
334 assert!(module.name.is_none());
335 assert!(module.ects.is_none());
336 assert!(module.chapters.is_empty());
337 assert!(!module.register_to_open_university);
338 assert_eq!(module.completion_policy, CompletionPolicy::Manual);
339 }
340
341 #[test]
342 fn module_builder_order() {
343 let module = ModuleBuilder::new().order(5);
344
345 assert_eq!(module.order, Some(5));
346 }
347
348 #[test]
349 fn module_builder_name() {
350 let module = ModuleBuilder::new().name("Test Module");
351
352 assert_eq!(module.name, Some("Test Module".to_string()));
353 }
354
355 #[test]
356 fn module_builder_name_string_conversion() {
357 let module1 = ModuleBuilder::new().name("String literal");
358 let module2 = ModuleBuilder::new().name(String::from("Owned string"));
359
360 assert_eq!(module1.name, Some("String literal".to_string()));
361 assert_eq!(module2.name, Some("Owned string".to_string()));
362 }
363
364 #[test]
365 fn module_builder_ects() {
366 let module = ModuleBuilder::new().ects(5.0);
367
368 assert_eq!(module.ects, Some(5.0));
369 }
370
371 #[test]
372 fn module_builder_ects_fractional() {
373 let module = ModuleBuilder::new().ects(2.5);
374
375 assert_eq!(module.ects, Some(2.5));
376 }
377
378 #[test]
379 fn module_builder_register_to_open_university_true() {
380 let module = ModuleBuilder::new().register_to_open_university(true);
381
382 assert!(module.register_to_open_university);
383 }
384
385 #[test]
386 fn module_builder_register_to_open_university_false() {
387 let module = ModuleBuilder::new().register_to_open_university(false);
388
389 assert!(!module.register_to_open_university);
390 }
391
392 #[test]
393 fn module_builder_chapter() {
394 let chapter = ChapterBuilder::new(1, "Test Chapter");
395 let module = ModuleBuilder::new().chapter(chapter);
396
397 assert_eq!(module.chapters.len(), 1);
398 assert_eq!(module.chapters[0].number, 1);
399 assert_eq!(module.chapters[0].name, "Test Chapter");
400 }
401
402 #[test]
403 fn module_builder_multiple_chapters() {
404 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
405 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
406 let module = ModuleBuilder::new().chapter(chapter1).chapter(chapter2);
407
408 assert_eq!(module.chapters.len(), 2);
409 assert_eq!(module.chapters[0].number, 1);
410 assert_eq!(module.chapters[0].name, "Chapter 1");
411 assert_eq!(module.chapters[1].number, 2);
412 assert_eq!(module.chapters[1].name, "Chapter 2");
413 }
414
415 #[test]
416 fn module_builder_fluent_interface() {
417 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
418 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
419
420 let module = ModuleBuilder::new()
421 .order(1)
422 .name("Advanced Module")
423 .ects(3.5)
424 .register_to_open_university(true)
425 .chapter(chapter1)
426 .chapter(chapter2);
427
428 assert_eq!(module.order, Some(1));
429 assert_eq!(module.name, Some("Advanced Module".to_string()));
430 assert_eq!(module.ects, Some(3.5));
431 assert!(module.register_to_open_university);
432 assert_eq!(module.chapters.len(), 2);
433 }
434
435 #[test]
436 fn module_builder_method_chaining_order() {
437 let chapter1 = ChapterBuilder::new(1, "Test Chapter");
438 let chapter2 = ChapterBuilder::new(1, "Test Chapter");
439
440 let module1 = ModuleBuilder::new()
441 .order(1)
442 .name("Module 1")
443 .ects(2.0)
444 .register_to_open_university(true)
445 .chapter(chapter1);
446
447 let module2 = ModuleBuilder::new()
448 .register_to_open_university(true)
449 .chapter(chapter2)
450 .ects(2.0)
451 .name("Module 1")
452 .order(1);
453
454 assert_eq!(module1.name, module2.name);
455 assert_eq!(module1.ects, module2.ects);
456 assert_eq!(
457 module1.register_to_open_university,
458 module2.register_to_open_university
459 );
460 assert_eq!(module1.chapters.len(), module2.chapters.len());
461 assert_eq!(module1.order, module2.order);
462 }
463
464 #[test]
465 fn module_builder_default_values() {
466 let module = ModuleBuilder::new();
467
468 assert!(module.order.is_none());
469 assert!(module.name.is_none());
470 assert!(module.ects.is_none());
471 assert!(module.chapters.is_empty());
472 assert!(!module.register_to_open_university);
473 }
474
475 #[test]
476 fn module_builder_order_preservation() {
477 let module = ModuleBuilder::new().order(999);
478
479 assert_eq!(module.order, Some(999));
480 }
481
482 #[test]
483 fn module_builder_manual_completion() {
484 let module = ModuleBuilder::new().manual_completion();
485
486 assert_eq!(module.completion_policy, CompletionPolicy::Manual);
487 }
488
489 #[test]
490 fn module_builder_automatic_completion() {
491 let module = ModuleBuilder::new().automatic_completion(Some(5), Some(100), true);
492
493 match module.completion_policy {
494 CompletionPolicy::Automatic(requirements) => {
495 assert_eq!(requirements.number_of_exercises_attempted_treshold, Some(5));
496 assert_eq!(requirements.number_of_points_treshold, Some(100));
497 assert!(requirements.requires_exam);
498 }
499 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
500 }
501 }
502
503 #[test]
504 fn module_builder_automatic_completion_no_thresholds() {
505 let module = ModuleBuilder::new().automatic_completion(None, None, false);
506
507 match module.completion_policy {
508 CompletionPolicy::Automatic(requirements) => {
509 assert_eq!(requirements.number_of_exercises_attempted_treshold, None);
510 assert_eq!(requirements.number_of_points_treshold, None);
511 assert!(!requirements.requires_exam);
512 }
513 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
514 }
515 }
516
517 #[test]
518 fn module_builder_completion_policy_fluent_interface() {
519 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
520 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
521
522 let module = ModuleBuilder::new()
523 .order(1)
524 .name("Advanced Module")
525 .ects(3.5)
526 .register_to_open_university(true)
527 .automatic_completion(Some(10), Some(200), true)
528 .chapter(chapter1)
529 .chapter(chapter2);
530
531 assert_eq!(module.order, Some(1));
532 assert_eq!(module.name, Some("Advanced Module".to_string()));
533 assert_eq!(module.ects, Some(3.5));
534 assert!(module.register_to_open_university);
535 assert_eq!(module.chapters.len(), 2);
536
537 match module.completion_policy {
538 CompletionPolicy::Automatic(requirements) => {
539 assert_eq!(
540 requirements.number_of_exercises_attempted_treshold,
541 Some(10)
542 );
543 assert_eq!(requirements.number_of_points_treshold, Some(200));
544 assert!(requirements.requires_exam);
545 }
546 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
547 }
548 }
549
550 #[test]
551 fn module_builder_completion_policy_override() {
552 let module1 = ModuleBuilder::new().manual_completion();
553 let module2 = ModuleBuilder::new()
554 .automatic_completion(Some(5), Some(100), false)
555 .manual_completion();
556
557 assert_eq!(module1.completion_policy, CompletionPolicy::Manual);
558 assert_eq!(module2.completion_policy, CompletionPolicy::Manual);
559 }
560}