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 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 self.completion_policy = CompletionPolicy::Automatic(AutomaticCompletionRequirements {
272 course_module_id: uuid::Uuid::new_v4(), 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 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}