1use std::collections::HashMap;
2
3use utoipa::ToSchema;
4
5use crate::{chapters, prelude::*};
6
7struct CourseModulesSchema {
9 id: Uuid,
10 created_at: DateTime<Utc>,
11 updated_at: DateTime<Utc>,
12 deleted_at: Option<DateTime<Utc>>,
13 name: Option<String>,
14 course_id: Uuid,
15 order_number: i32,
16 copied_from: Option<Uuid>,
17 uh_course_code: Option<String>,
18 automatic_completion: bool,
19 automatic_completion_number_of_exercises_attempted_treshold: Option<i32>,
20 automatic_completion_number_of_points_treshold: Option<i32>,
21 automatic_completion_requires_exam: bool,
22 completion_registration_link_override: Option<String>,
23 ects_credits: Option<f32>,
24 enable_registering_completion_to_uh_open_university: bool,
25 certification_enabled: bool,
26}
27#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
31
32pub struct CourseModule {
33 pub id: Uuid,
34 pub created_at: DateTime<Utc>,
35 pub updated_at: DateTime<Utc>,
36 pub deleted_at: Option<DateTime<Utc>>,
37 pub name: Option<String>,
38 pub course_id: Uuid,
39 pub order_number: i32,
40 pub copied_from: Option<Uuid>,
41 pub uh_course_code: Option<String>,
42 pub completion_policy: CompletionPolicy,
43 pub completion_registration_link_override: Option<String>,
45 pub ects_credits: Option<f32>,
46 pub enable_registering_completion_to_uh_open_university: bool,
47 pub certification_enabled: bool,
48}
49
50impl CourseModule {
51 pub fn new(id: Uuid, course_id: Uuid) -> Self {
52 Self {
53 id,
54 created_at: Utc::now(),
55 updated_at: Utc::now(),
56 deleted_at: None,
57 name: None,
58 course_id,
59 order_number: 0,
60 copied_from: None,
61 uh_course_code: None,
62 completion_policy: CompletionPolicy::Manual,
63 completion_registration_link_override: None,
64 ects_credits: None,
65 enable_registering_completion_to_uh_open_university: false,
66 certification_enabled: false,
67 }
68 }
69 pub fn set_timestamps(
70 mut self,
71 created_at: DateTime<Utc>,
72 updated_at: DateTime<Utc>,
73 deleted_at: Option<DateTime<Utc>>,
74 ) -> Self {
75 self.created_at = created_at;
76 self.updated_at = updated_at;
77 self.deleted_at = deleted_at;
78 self
79 }
80
81 pub fn set_name_and_order_number(mut self, name: Option<String>, order_number: i32) -> Self {
83 self.name = name;
84 self.order_number = order_number;
85 self
86 }
87
88 pub fn set_completion_policy(mut self, completion_policy: CompletionPolicy) -> Self {
89 self.completion_policy = completion_policy;
90 self
91 }
92
93 pub fn set_registration_info(
94 mut self,
95 uh_course_code: Option<String>,
96 ects_credits: Option<f32>,
97 completion_registration_link_override: Option<String>,
98 enable_registering_completion_to_uh_open_university: bool,
99 ) -> Self {
100 self.uh_course_code = uh_course_code;
101 self.ects_credits = ects_credits;
102 self.completion_registration_link_override = completion_registration_link_override;
103 self.enable_registering_completion_to_uh_open_university =
104 enable_registering_completion_to_uh_open_university;
105 self
106 }
107
108 pub fn set_certification_enabled(mut self, certification_enabled: bool) -> Self {
109 self.certification_enabled = certification_enabled;
110 self
111 }
112
113 pub fn is_default_module(&self) -> bool {
114 self.name.is_none()
115 }
116}
117
118impl From<CourseModulesSchema> for CourseModule {
119 fn from(schema: CourseModulesSchema) -> Self {
120 let completion_policy = if schema.automatic_completion {
121 CompletionPolicy::Automatic(AutomaticCompletionRequirements {
122 course_module_id: schema.id,
123 number_of_exercises_attempted_treshold: schema
124 .automatic_completion_number_of_exercises_attempted_treshold,
125 number_of_points_treshold: schema.automatic_completion_number_of_points_treshold,
126 requires_exam: schema.automatic_completion_requires_exam,
127 })
128 } else {
129 CompletionPolicy::Manual
130 };
131 Self {
132 id: schema.id,
133 created_at: schema.created_at,
134 updated_at: schema.updated_at,
135 deleted_at: schema.deleted_at,
136 name: schema.name,
137 course_id: schema.course_id,
138 order_number: schema.order_number,
139 copied_from: schema.copied_from,
140 uh_course_code: schema.uh_course_code,
141 completion_policy,
142 completion_registration_link_override: schema.completion_registration_link_override,
143 ects_credits: schema.ects_credits,
144 enable_registering_completion_to_uh_open_university: schema
145 .enable_registering_completion_to_uh_open_university,
146 certification_enabled: schema.certification_enabled,
147 }
148 }
149}
150
151#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
152
153pub struct NewCourseModule {
154 completion_policy: CompletionPolicy,
155 completion_registration_link_override: Option<String>,
156 course_id: Uuid,
157 ects_credits: Option<f32>,
158 name: Option<String>,
159 order_number: i32,
160 uh_course_code: Option<String>,
161 enable_registering_completion_to_uh_open_university: bool,
162}
163
164impl NewCourseModule {
165 pub fn new(course_id: Uuid, name: Option<String>, order_number: i32) -> Self {
166 Self {
167 completion_policy: CompletionPolicy::Manual,
168 completion_registration_link_override: None,
169 course_id,
170 ects_credits: None,
171 name,
172 order_number,
173 uh_course_code: None,
174 enable_registering_completion_to_uh_open_university: false,
175 }
176 }
177
178 pub fn new_course_default(course_id: Uuid) -> Self {
179 Self::new(course_id, None, 0)
180 }
181
182 pub fn set_uh_course_code(mut self, uh_course_code: Option<String>) -> Self {
183 self.uh_course_code = uh_course_code;
184 self
185 }
186
187 pub fn set_completion_policy(mut self, completion_policy: CompletionPolicy) -> Self {
188 self.completion_policy = completion_policy;
189 self
190 }
191
192 pub fn set_completion_registration_link_override(
193 mut self,
194 completion_registration_link_override: Option<String>,
195 ) -> Self {
196 self.completion_registration_link_override = completion_registration_link_override;
197 self
198 }
199
200 pub fn set_ects_credits(mut self, ects_credits: Option<f32>) -> Self {
201 self.ects_credits = ects_credits;
202 self
203 }
204
205 pub fn set_enable_registering_completion_to_uh_open_university(
206 mut self,
207 enable_registering_completion_to_uh_open_university: bool,
208 ) -> Self {
209 self.enable_registering_completion_to_uh_open_university =
210 enable_registering_completion_to_uh_open_university;
211 self
212 }
213}
214
215pub async fn insert(
216 conn: &mut PgConnection,
217 pkey_policy: PKeyPolicy<Uuid>,
218 new_course_module: &NewCourseModule,
219) -> ModelResult<CourseModule> {
220 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
221 new_course_module.completion_policy.to_database_fields();
222 let res = sqlx::query_as!(
223 CourseModulesSchema,
224 "
225INSERT INTO course_modules (
226 id,
227 course_id,
228 name,
229 order_number,
230 automatic_completion,
231 automatic_completion_number_of_exercises_attempted_treshold,
232 automatic_completion_number_of_points_treshold,
233 automatic_completion_requires_exam,
234 ects_credits,
235 enable_registering_completion_to_uh_open_university
236 )
237VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
238RETURNING *
239 ",
240 pkey_policy.into_uuid(),
241 new_course_module.course_id,
242 new_course_module.name,
243 new_course_module.order_number,
244 automatic_completion,
245 exercises_treshold,
246 points_treshold,
247 requires_exam,
248 new_course_module.ects_credits,
249 new_course_module.enable_registering_completion_to_uh_open_university
250 )
251 .fetch_one(conn)
252 .await?;
253 Ok(res.into())
254}
255
256pub async fn rename(conn: &mut PgConnection, id: Uuid, name: &str) -> ModelResult<()> {
257 sqlx::query!(
258 "
259UPDATE course_modules
260SET name = $1
261WHERE id = $2
262",
263 name,
264 id
265 )
266 .execute(conn)
267 .await?;
268 Ok(())
269}
270
271pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
272 let associated_chapters = chapters::get_for_module(conn, id).await?;
273 if !associated_chapters.is_empty() {
274 return Err(ModelError::new(
275 ModelErrorType::InvalidRequest,
276 format!(
277 "Cannot remove module {id} because it has {} chapters associated with it",
278 associated_chapters.len()
279 ),
280 None,
281 ));
282 }
283 sqlx::query!(
284 "
285UPDATE course_modules
286SET deleted_at = now()
287WHERE id = $1
288AND deleted_at IS NULL
289",
290 id
291 )
292 .execute(conn)
293 .await?;
294 Ok(())
295}
296
297pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CourseModule> {
298 let res = sqlx::query_as!(
299 CourseModulesSchema,
300 "
301SELECT *
302FROM course_modules
303WHERE id = $1
304 AND deleted_at IS NULL
305 ",
306 id,
307 )
308 .fetch_one(conn)
309 .await?;
310 Ok(res.into())
311}
312
313pub async fn get_by_ids(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<Vec<CourseModule>> {
314 let res = sqlx::query_as!(
315 CourseModulesSchema,
316 "
317SELECT *
318FROM course_modules
319WHERE id = ANY($1)
320 AND deleted_at IS NULL
321 ",
322 ids,
323 )
324 .map(|x| x.into())
325 .fetch_all(conn)
326 .await?;
327 Ok(res)
328}
329
330pub async fn get_by_course_id(
331 conn: &mut PgConnection,
332 course_id: Uuid,
333) -> ModelResult<Vec<CourseModule>> {
334 let res = sqlx::query_as!(
335 CourseModulesSchema,
336 "
337SELECT *
338FROM course_modules
339WHERE course_id = $1
340AND deleted_at IS NULL
341",
342 course_id
343 )
344 .map(|x| x.into())
345 .fetch_all(conn)
346 .await?;
347 Ok(res)
348}
349
350pub async fn get_by_course_id_only_with_open_chapters(
351 conn: &mut PgConnection,
352 course_id: Uuid,
353) -> ModelResult<Vec<CourseModule>> {
354 let res = sqlx::query_as!(
355 CourseModulesSchema,
356 "
357SELECT *
358FROM course_modules as cm
359WHERE EXISTS (
360 SELECT 1
361 FROM chapters as ch
362 WHERE ch.course_module_id = cm.id
363 AND ((ch.opens_at < now()) OR ch.opens_at IS NULL)
364 AND ch.deleted_at IS NULL
365)
366 AND cm.course_id = $1
367 AND cm.deleted_at IS NULL
368",
369 course_id
370 )
371 .map(|x| x.into())
372 .fetch_all(conn)
373 .await?;
374 Ok(res)
375}
376
377pub async fn get_by_exercise_id(
380 conn: &mut PgConnection,
381 exercise_id: Uuid,
382) -> ModelResult<CourseModule> {
383 let res = sqlx::query_as!(
384 CourseModulesSchema,
385 "
386SELECT course_modules.*
387FROM exercises
388 LEFT JOIN chapters ON (exercises.chapter_id = chapters.id)
389 LEFT JOIN course_modules ON (chapters.course_module_id = course_modules.id)
390WHERE exercises.id = $1
391AND chapters.deleted_at IS NULL
392AND course_modules.deleted_at IS NULL
393 ",
394 exercise_id,
395 )
396 .fetch_one(conn)
397 .await?;
398 Ok(res.into())
399}
400
401pub async fn get_course_module_id_by_chapter(
402 conn: &mut PgConnection,
403 chapter_id: Uuid,
404) -> ModelResult<Uuid> {
405 let res: Uuid = sqlx::query!(
406 r#"
407SELECT c.course_module_id
408from chapters c
409where c.id = $1
410 AND deleted_at IS NULL
411 "#,
412 chapter_id
413 )
414 .map(|record| record.course_module_id)
415 .fetch_one(conn)
416 .await?;
417 Ok(res)
418}
419
420pub async fn get_default_by_course_id(
421 conn: &mut PgConnection,
422 course_id: Uuid,
423) -> ModelResult<CourseModule> {
424 let res = sqlx::query_as!(
425 CourseModulesSchema,
426 "
427SELECT *
428FROM course_modules
429WHERE course_id = $1
430 AND name IS NULL
431 AND deleted_at IS NULL
432 ",
433 course_id,
434 )
435 .fetch_one(conn)
436 .await?;
437 Ok(res.into())
438}
439
440pub async fn get_ids_by_course_slug_or_uh_course_code(
445 conn: &mut PgConnection,
446 course_slug_or_code: &str,
447) -> ModelResult<Vec<Uuid>> {
448 let res = sqlx::query!(
449 "
450SELECT course_modules.id
451FROM course_modules
452 LEFT JOIN courses ON (course_modules.course_id = courses.id)
453WHERE (
454 course_modules.uh_course_code = $1
455 OR courses.slug = $1
456 )
457 AND course_modules.deleted_at IS NULL
458 ",
459 course_slug_or_code,
460 )
461 .map(|record| record.id)
462 .fetch_all(conn)
463 .await?;
464 Ok(res)
465}
466
467pub async fn get_by_course_id_as_map(
469 conn: &mut PgConnection,
470 course_id: Uuid,
471) -> ModelResult<HashMap<Uuid, CourseModule>> {
472 let res = get_by_course_id(conn, course_id)
473 .await?
474 .into_iter()
475 .map(|course_module| (course_module.id, course_module))
476 .collect();
477 Ok(res)
478}
479
480pub async fn get_all_uh_course_codes_for_open_university(
481 conn: &mut PgConnection,
482) -> ModelResult<Vec<String>> {
483 let res = sqlx::query!(
484 "
485SELECT DISTINCT uh_course_code
486FROM course_modules
487WHERE uh_course_code IS NOT NULL
488 AND enable_registering_completion_to_uh_open_university = true
489 AND deleted_at IS NULL
490"
491 )
492 .fetch_all(conn)
493 .await?
494 .into_iter()
495 .filter_map(|x| x.uh_course_code)
496 .collect();
497 Ok(res)
498}
499
500#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
501
502pub struct AutomaticCompletionRequirements {
503 pub course_module_id: Uuid,
505 pub number_of_exercises_attempted_treshold: Option<i32>,
506 pub number_of_points_treshold: Option<i32>,
507 pub requires_exam: bool,
508}
509
510impl AutomaticCompletionRequirements {
511 pub fn passes_exercise_tresholds(
514 &self,
515 exercises_attempted: i32,
516 exercise_points: i32,
517 ) -> bool {
518 self.passes_number_of_exercises_attempted_treshold(exercises_attempted)
519 && self.passes_number_of_exercise_points_treshold(exercise_points)
520 }
521
522 pub fn passes_number_of_exercises_attempted_treshold(&self, exercises_attempted: i32) -> bool {
525 self.number_of_exercises_attempted_treshold
526 .map(|x| x <= exercises_attempted)
527 .unwrap_or(true)
528 }
529
530 pub fn passes_number_of_exercise_points_treshold(&self, exercise_points: i32) -> bool {
533 self.number_of_points_treshold
534 .map(|x| x <= exercise_points)
535 .unwrap_or(true)
536 }
537}
538
539#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ToSchema)]
540#[serde(tag = "policy", rename_all = "kebab-case")]
541pub enum CompletionPolicy {
542 Automatic(AutomaticCompletionRequirements),
543 Manual,
544}
545
546impl CompletionPolicy {
547 pub fn automatic(&self) -> Option<&AutomaticCompletionRequirements> {
549 match self {
550 CompletionPolicy::Automatic(requirements) => Some(requirements),
551 CompletionPolicy::Manual => None,
552 }
553 }
554
555 fn to_database_fields(&self) -> (bool, Option<i32>, Option<i32>, bool) {
556 match self {
557 CompletionPolicy::Automatic(requirements) => (
558 true,
559 requirements.number_of_exercises_attempted_treshold,
560 requirements.number_of_points_treshold,
561 requirements.requires_exam,
562 ),
563 CompletionPolicy::Manual => (false, None, None, false),
564 }
565 }
566}
567
568pub async fn update_automatic_completion_status(
569 conn: &mut PgConnection,
570 id: Uuid,
571 automatic_completion_policy: &CompletionPolicy,
572) -> ModelResult<CourseModule> {
573 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
574 automatic_completion_policy.to_database_fields();
575 let res = sqlx::query_as!(
576 CourseModulesSchema,
577 "
578UPDATE course_modules
579SET automatic_completion = $1,
580 automatic_completion_number_of_exercises_attempted_treshold = $2,
581 automatic_completion_number_of_points_treshold = $3,
582 automatic_completion_requires_exam = $4
583WHERE id = $5
584 AND deleted_at IS NULL
585RETURNING *
586 ",
587 automatic_completion,
588 exercises_treshold,
589 points_treshold,
590 requires_exam,
591 id,
592 )
593 .fetch_one(conn)
594 .await?;
595 Ok(res.into())
596}
597
598pub async fn update_uh_course_code(
599 conn: &mut PgConnection,
600 id: Uuid,
601 uh_course_code: Option<String>,
602) -> ModelResult<CourseModule> {
603 let res = sqlx::query_as!(
604 CourseModulesSchema,
605 "
606UPDATE course_modules
607SET uh_course_code = $1
608WHERE id = $2
609 AND deleted_at IS NULL
610RETURNING *
611 ",
612 uh_course_code,
613 id,
614 )
615 .fetch_one(conn)
616 .await?;
617 Ok(res.into())
618}
619
620pub async fn update_enable_registering_completion_to_uh_open_university(
621 conn: &mut PgConnection,
622 id: Uuid,
623 enable_registering_completion_to_uh_open_university: bool,
624) -> ModelResult<CourseModule> {
625 let res = sqlx::query_as!(
626 CourseModulesSchema,
627 "
628UPDATE course_modules
629SET enable_registering_completion_to_uh_open_university = $1
630WHERE id = $2
631 AND deleted_at IS NULL
632RETURNING *
633 ",
634 enable_registering_completion_to_uh_open_university,
635 id,
636 )
637 .fetch_one(conn)
638 .await?;
639 Ok(res.into())
640}
641
642#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
643
644pub struct NewModule {
645 name: String,
646 order_number: i32,
647 chapters: Vec<Uuid>,
648 uh_course_code: Option<String>,
649 ects_credits: Option<f32>,
650 completion_policy: CompletionPolicy,
651 completion_registration_link_override: Option<String>,
652 enable_registering_completion_to_uh_open_university: bool,
653}
654
655#[derive(Debug, Deserialize, ToSchema)]
656
657pub struct ModifiedModule {
658 id: Uuid,
659 name: Option<String>,
660 order_number: i32,
661 uh_course_code: Option<String>,
662 ects_credits: Option<f32>,
663 completion_policy: CompletionPolicy,
664 completion_registration_link_override: Option<String>,
665 enable_registering_completion_to_uh_open_university: bool,
666}
667
668#[derive(Debug, Deserialize, ToSchema)]
669
670pub struct ModuleUpdates {
671 new_modules: Vec<NewModule>,
672 deleted_modules: Vec<Uuid>,
673 modified_modules: Vec<ModifiedModule>,
674 moved_chapters: Vec<(Uuid, Uuid)>,
675}
676
677pub async fn update_with_order_number(
678 conn: &mut PgConnection,
679 id: Uuid,
680 name: Option<&str>,
681 order_number: i32,
682) -> ModelResult<()> {
683 sqlx::query!(
684 "
685UPDATE course_modules
686SET name = COALESCE($1, name),
687 order_number = $2
688WHERE id = $3
689",
690 name,
691 order_number,
692 id,
693 )
694 .execute(conn)
695 .await?;
696 Ok(())
697}
698
699pub async fn update(
700 conn: &mut PgConnection,
701 id: Uuid,
702 updated_course_module: &NewCourseModule,
703) -> ModelResult<()> {
704 let NewCourseModule {
706 completion_policy: _,
707 course_id: _,
708 ects_credits,
709 order_number,
710 name,
711 uh_course_code,
712 completion_registration_link_override,
713 enable_registering_completion_to_uh_open_university,
714 } = updated_course_module;
715 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
716 updated_course_module.completion_policy.to_database_fields();
717 sqlx::query!(
718 "
719UPDATE course_modules
720SET name = COALESCE($2, name),
721 order_number = $3,
722 uh_course_code = $4,
723 ects_credits = $5,
724 automatic_completion = $6,
725 automatic_completion_number_of_exercises_attempted_treshold = $7,
726 automatic_completion_number_of_points_treshold = $8,
727 automatic_completion_requires_exam = $9,
728 completion_registration_link_override = $10,
729 enable_registering_completion_to_uh_open_university = $11
730WHERE id = $1
731 ",
732 id,
733 name.as_ref(),
734 order_number,
735 uh_course_code.as_ref(),
736 ects_credits.as_ref(),
737 automatic_completion,
738 exercises_treshold,
739 points_treshold,
740 requires_exam,
741 completion_registration_link_override.as_ref(),
742 enable_registering_completion_to_uh_open_university
743 )
744 .execute(conn)
745 .await?;
746 Ok(())
747}
748
749pub async fn update_modules(
750 conn: &mut PgConnection,
751 course_id: Uuid,
752 updates: ModuleUpdates,
753) -> ModelResult<()> {
754 let mut tx = conn.begin().await?;
755
756 for module_id in updates
758 .modified_modules
759 .iter()
760 .filter(|m| m.order_number != 0)
762 .map(|m| m.id)
763 .chain(updates.deleted_modules.iter().copied())
764 {
765 update_with_order_number(&mut tx, module_id, None, rand::random()).await?;
766 }
767 let mut modified_and_new_modules = updates.modified_modules;
768 for new in updates.new_modules {
769 let NewModule {
771 name,
772 order_number,
773 chapters,
774 uh_course_code,
775 ects_credits,
776 completion_policy,
777 completion_registration_link_override,
778 enable_registering_completion_to_uh_open_university,
779 } = new;
780 let new_course_module = NewCourseModule::new(course_id, Some(name.clone()), rand::random())
782 .set_completion_policy(completion_policy.clone())
783 .set_completion_registration_link_override(completion_registration_link_override)
784 .set_ects_credits(ects_credits)
785 .set_uh_course_code(uh_course_code)
786 .set_enable_registering_completion_to_uh_open_university(
787 enable_registering_completion_to_uh_open_university,
788 );
789 let module = insert(&mut tx, PKeyPolicy::Generate, &new_course_module).await?;
790 for chapter in chapters {
791 chapters::set_module(&mut tx, chapter, module.id).await?;
792 }
793 modified_and_new_modules.push(ModifiedModule {
795 id: module.id,
796 name: None,
797 order_number,
798 uh_course_code: module.uh_course_code,
799 ects_credits,
800 completion_policy,
801 completion_registration_link_override: module.completion_registration_link_override,
802 enable_registering_completion_to_uh_open_university: module
803 .enable_registering_completion_to_uh_open_university,
804 })
805 }
806 for module in modified_and_new_modules {
808 let ModifiedModule {
810 id,
811 name,
812 order_number,
813 uh_course_code,
814 ects_credits,
815 completion_policy,
816 completion_registration_link_override,
817 enable_registering_completion_to_uh_open_university,
818 } = module;
819 update(
820 &mut tx,
821 id,
822 &NewCourseModule::new(course_id, name.clone(), order_number)
823 .set_completion_policy(completion_policy)
824 .set_completion_registration_link_override(completion_registration_link_override)
825 .set_ects_credits(ects_credits)
826 .set_uh_course_code(uh_course_code)
827 .set_enable_registering_completion_to_uh_open_university(
828 enable_registering_completion_to_uh_open_university,
829 ),
830 )
831 .await?;
832 }
833 for (chapter, module) in updates.moved_chapters {
834 chapters::set_module(&mut tx, chapter, module).await?;
835 }
836 for deleted in updates.deleted_modules {
837 delete(&mut tx, deleted).await?;
838 }
839
840 tx.commit().await?;
841 Ok(())
842}
843
844pub async fn update_certification_enabled(
845 conn: &mut PgConnection,
846 id: Uuid,
847 enabled: bool,
848) -> ModelResult<()> {
849 sqlx::query!(
850 "
851UPDATE course_modules
852SET certification_enabled = $1
853WHERE id = $2
854",
855 enabled,
856 id
857 )
858 .execute(conn)
859 .await?;
860 Ok(())
861}
862
863#[cfg(test)]
864mod tests {
865
866 mod automatic_completion_requirements {
867 use uuid::Uuid;
868
869 use super::super::AutomaticCompletionRequirements;
870
871 #[test]
872 fn passes_exercise_tresholds() {
873 let requirements1 = AutomaticCompletionRequirements {
874 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
875 number_of_exercises_attempted_treshold: Some(10),
876 number_of_points_treshold: Some(50),
877 requires_exam: false,
878 };
879 let requirements2 = AutomaticCompletionRequirements {
880 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
881 number_of_exercises_attempted_treshold: Some(50),
882 number_of_points_treshold: Some(10),
883 requires_exam: false,
884 };
885
886 let requirements3 = AutomaticCompletionRequirements {
887 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
888 number_of_exercises_attempted_treshold: Some(0),
889 number_of_points_treshold: Some(0),
890 requires_exam: false,
891 };
892
893 let requirements4 = AutomaticCompletionRequirements {
894 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
895 number_of_exercises_attempted_treshold: Some(10),
896 number_of_points_treshold: None,
897 requires_exam: false,
898 };
899
900 let requirements5 = AutomaticCompletionRequirements {
901 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
902 number_of_exercises_attempted_treshold: None,
903 number_of_points_treshold: Some(10),
904 requires_exam: false,
905 };
906 assert!(requirements1.passes_exercise_tresholds(10, 50));
907 assert!(requirements2.passes_exercise_tresholds(50, 10));
908
909 assert!(!requirements1.passes_exercise_tresholds(50, 10));
910 assert!(!requirements2.passes_exercise_tresholds(10, 50));
911
912 assert!(!requirements1.passes_exercise_tresholds(100, 0));
913 assert!(!requirements2.passes_exercise_tresholds(100, 0));
914
915 assert!(requirements3.passes_exercise_tresholds(1, 1));
916 assert!(requirements3.passes_exercise_tresholds(0, 0));
917
918 assert!(requirements4.passes_exercise_tresholds(10, 1));
919 assert!(!requirements4.passes_exercise_tresholds(1, 10));
920
921 assert!(requirements5.passes_exercise_tresholds(0, 10));
922 assert!(!requirements5.passes_exercise_tresholds(10, 0));
923 }
924 }
925}