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