headless_lms_server/programs/seed/builder/
module.rs1use 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};
9
10#[derive(Debug, Clone)]
12pub struct ModuleBuilder {
13 pub name: Option<String>,
14 pub order: Option<i32>,
15 pub ects: Option<f32>,
16 pub chapters: Vec<ChapterBuilder>,
17 pub register_to_open_university: bool,
18 pub completion_policy: CompletionPolicy,
19}
20
21impl Default for ModuleBuilder {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl ModuleBuilder {
28 pub fn new() -> Self {
29 Self {
30 name: None,
31 order: None,
32 ects: None,
33 chapters: vec![],
34 register_to_open_university: false,
35 completion_policy: CompletionPolicy::Manual,
36 }
37 }
38 pub fn order(mut self, n: i32) -> Self {
39 self.order = Some(n);
40 self
41 }
42 pub fn name(mut self, n: impl Into<String>) -> Self {
43 self.name = Some(n.into());
44 self
45 }
46 pub fn ects(mut self, e: f32) -> Self {
47 self.ects = Some(e);
48 self
49 }
50 pub fn register_to_open_university(mut self, v: bool) -> Self {
51 self.register_to_open_university = v;
52 self
53 }
54 pub fn chapter(mut self, c: ChapterBuilder) -> Self {
55 self.chapters.push(c);
56 self
57 }
58 pub fn chapters<I: IntoIterator<Item = ChapterBuilder>>(mut self, it: I) -> Self {
59 self.chapters.extend(it);
60 self
61 }
62 pub fn completion_policy(mut self, policy: CompletionPolicy) -> Self {
63 self.completion_policy = policy;
64 self
65 }
66 pub fn manual_completion(mut self) -> Self {
67 self.completion_policy = CompletionPolicy::Manual;
68 self
69 }
70 pub fn automatic_completion(
71 mut self,
72 exercises_threshold: Option<i32>,
73 points_threshold: Option<i32>,
74 requires_exam: bool,
75 ) -> Self {
76 self.completion_policy = CompletionPolicy::Automatic(AutomaticCompletionRequirements {
78 course_module_id: uuid::Uuid::new_v4(), number_of_exercises_attempted_treshold: exercises_threshold,
80 number_of_points_treshold: points_threshold,
81 requires_exam,
82 });
83 self
84 }
85
86 pub(crate) async fn seed(
87 self,
88 conn: &mut PgConnection,
89 cx: &SeedContext,
90 course_id: uuid::Uuid,
91 fallback_order: i32,
92 ) -> Result<CourseModule> {
93 let order = self.order.unwrap_or(fallback_order);
94
95 let module = course_modules::insert(
96 conn,
97 headless_lms_models::PKeyPolicy::Generate,
98 &NewCourseModule::new(course_id, self.name, order)
99 .set_ects_credits(self.ects)
100 .set_completion_policy(self.completion_policy.clone()),
101 )
102 .await
103 .with_context(|| format!("inserting module (order {:?})", order))?;
104
105 if let CompletionPolicy::Automatic(mut requirements) = self.completion_policy {
107 requirements.course_module_id = module.id;
108 let updated_policy = CompletionPolicy::Automatic(requirements);
109 course_modules::update_automatic_completion_status(conn, module.id, &updated_policy)
110 .await
111 .context("updating automatic completion policy")?;
112 }
113
114 if self.register_to_open_university {
115 course_modules::update_enable_registering_completion_to_uh_open_university(
116 conn, module.id, true,
117 )
118 .await
119 .context("enabling OU registration for module")?;
120 }
121
122 for ch in self.chapters {
123 ch.seed(conn, cx, course_id, module.id).await?;
124 }
125
126 Ok(module)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use crate::programs::seed::builder::chapter::ChapterBuilder;
133
134 use super::*;
135
136 #[test]
137 fn module_builder_new() {
138 let module = ModuleBuilder::new();
139
140 assert!(module.order.is_none());
141 assert!(module.name.is_none());
142 assert!(module.ects.is_none());
143 assert!(module.chapters.is_empty());
144 assert!(!module.register_to_open_university);
145 assert_eq!(module.completion_policy, CompletionPolicy::Manual);
146 }
147
148 #[test]
149 fn module_builder_order() {
150 let module = ModuleBuilder::new().order(5);
151
152 assert_eq!(module.order, Some(5));
153 }
154
155 #[test]
156 fn module_builder_name() {
157 let module = ModuleBuilder::new().name("Test Module");
158
159 assert_eq!(module.name, Some("Test Module".to_string()));
160 }
161
162 #[test]
163 fn module_builder_name_string_conversion() {
164 let module1 = ModuleBuilder::new().name("String literal");
165 let module2 = ModuleBuilder::new().name(String::from("Owned string"));
166
167 assert_eq!(module1.name, Some("String literal".to_string()));
168 assert_eq!(module2.name, Some("Owned string".to_string()));
169 }
170
171 #[test]
172 fn module_builder_ects() {
173 let module = ModuleBuilder::new().ects(5.0);
174
175 assert_eq!(module.ects, Some(5.0));
176 }
177
178 #[test]
179 fn module_builder_ects_fractional() {
180 let module = ModuleBuilder::new().ects(2.5);
181
182 assert_eq!(module.ects, Some(2.5));
183 }
184
185 #[test]
186 fn module_builder_register_to_open_university_true() {
187 let module = ModuleBuilder::new().register_to_open_university(true);
188
189 assert!(module.register_to_open_university);
190 }
191
192 #[test]
193 fn module_builder_register_to_open_university_false() {
194 let module = ModuleBuilder::new().register_to_open_university(false);
195
196 assert!(!module.register_to_open_university);
197 }
198
199 #[test]
200 fn module_builder_chapter() {
201 let chapter = ChapterBuilder::new(1, "Test Chapter");
202 let module = ModuleBuilder::new().chapter(chapter);
203
204 assert_eq!(module.chapters.len(), 1);
205 assert_eq!(module.chapters[0].number, 1);
206 assert_eq!(module.chapters[0].name, "Test Chapter");
207 }
208
209 #[test]
210 fn module_builder_multiple_chapters() {
211 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
212 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
213 let module = ModuleBuilder::new().chapter(chapter1).chapter(chapter2);
214
215 assert_eq!(module.chapters.len(), 2);
216 assert_eq!(module.chapters[0].number, 1);
217 assert_eq!(module.chapters[0].name, "Chapter 1");
218 assert_eq!(module.chapters[1].number, 2);
219 assert_eq!(module.chapters[1].name, "Chapter 2");
220 }
221
222 #[test]
223 fn module_builder_fluent_interface() {
224 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
225 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
226
227 let module = ModuleBuilder::new()
228 .order(1)
229 .name("Advanced Module")
230 .ects(3.5)
231 .register_to_open_university(true)
232 .chapter(chapter1)
233 .chapter(chapter2);
234
235 assert_eq!(module.order, Some(1));
236 assert_eq!(module.name, Some("Advanced Module".to_string()));
237 assert_eq!(module.ects, Some(3.5));
238 assert!(module.register_to_open_university);
239 assert_eq!(module.chapters.len(), 2);
240 }
241
242 #[test]
243 fn module_builder_method_chaining_order() {
244 let chapter1 = ChapterBuilder::new(1, "Test Chapter");
245 let chapter2 = ChapterBuilder::new(1, "Test Chapter");
246
247 let module1 = ModuleBuilder::new()
248 .order(1)
249 .name("Module 1")
250 .ects(2.0)
251 .register_to_open_university(true)
252 .chapter(chapter1);
253
254 let module2 = ModuleBuilder::new()
255 .register_to_open_university(true)
256 .chapter(chapter2)
257 .ects(2.0)
258 .name("Module 1")
259 .order(1);
260
261 assert_eq!(module1.name, module2.name);
262 assert_eq!(module1.ects, module2.ects);
263 assert_eq!(
264 module1.register_to_open_university,
265 module2.register_to_open_university
266 );
267 assert_eq!(module1.chapters.len(), module2.chapters.len());
268 assert_eq!(module1.order, module2.order);
269 }
270
271 #[test]
272 fn module_builder_default_values() {
273 let module = ModuleBuilder::new();
274
275 assert!(module.order.is_none());
276 assert!(module.name.is_none());
277 assert!(module.ects.is_none());
278 assert!(module.chapters.is_empty());
279 assert!(!module.register_to_open_university);
280 }
281
282 #[test]
283 fn module_builder_order_preservation() {
284 let module = ModuleBuilder::new().order(999);
285
286 assert_eq!(module.order, Some(999));
287 }
288
289 #[test]
290 fn module_builder_manual_completion() {
291 let module = ModuleBuilder::new().manual_completion();
292
293 assert_eq!(module.completion_policy, CompletionPolicy::Manual);
294 }
295
296 #[test]
297 fn module_builder_automatic_completion() {
298 let module = ModuleBuilder::new().automatic_completion(Some(5), Some(100), true);
299
300 match module.completion_policy {
301 CompletionPolicy::Automatic(requirements) => {
302 assert_eq!(requirements.number_of_exercises_attempted_treshold, Some(5));
303 assert_eq!(requirements.number_of_points_treshold, Some(100));
304 assert!(requirements.requires_exam);
305 }
306 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
307 }
308 }
309
310 #[test]
311 fn module_builder_automatic_completion_no_thresholds() {
312 let module = ModuleBuilder::new().automatic_completion(None, None, false);
313
314 match module.completion_policy {
315 CompletionPolicy::Automatic(requirements) => {
316 assert_eq!(requirements.number_of_exercises_attempted_treshold, None);
317 assert_eq!(requirements.number_of_points_treshold, None);
318 assert!(!requirements.requires_exam);
319 }
320 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
321 }
322 }
323
324 #[test]
325 fn module_builder_completion_policy_fluent_interface() {
326 let chapter1 = ChapterBuilder::new(1, "Chapter 1");
327 let chapter2 = ChapterBuilder::new(2, "Chapter 2");
328
329 let module = ModuleBuilder::new()
330 .order(1)
331 .name("Advanced Module")
332 .ects(3.5)
333 .register_to_open_university(true)
334 .automatic_completion(Some(10), Some(200), true)
335 .chapter(chapter1)
336 .chapter(chapter2);
337
338 assert_eq!(module.order, Some(1));
339 assert_eq!(module.name, Some("Advanced Module".to_string()));
340 assert_eq!(module.ects, Some(3.5));
341 assert!(module.register_to_open_university);
342 assert_eq!(module.chapters.len(), 2);
343
344 match module.completion_policy {
345 CompletionPolicy::Automatic(requirements) => {
346 assert_eq!(
347 requirements.number_of_exercises_attempted_treshold,
348 Some(10)
349 );
350 assert_eq!(requirements.number_of_points_treshold, Some(200));
351 assert!(requirements.requires_exam);
352 }
353 CompletionPolicy::Manual => panic!("Expected automatic completion policy"),
354 }
355 }
356
357 #[test]
358 fn module_builder_completion_policy_override() {
359 let module1 = ModuleBuilder::new().manual_completion();
360 let module2 = ModuleBuilder::new()
361 .automatic_completion(Some(5), Some(100), false)
362 .manual_completion();
363
364 assert_eq!(module1.completion_policy, CompletionPolicy::Manual);
365 assert_eq!(module2.completion_policy, CompletionPolicy::Manual);
366 }
367}