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 r#"
386SELECT
387 course_modules.id AS "id!",
388 course_modules.created_at AS "created_at!",
389 course_modules.updated_at AS "updated_at!",
390 course_modules.deleted_at,
391 course_modules.name,
392 course_modules.course_id AS "course_id!",
393 course_modules.order_number AS "order_number!",
394 course_modules.copied_from,
395 course_modules.uh_course_code,
396 course_modules.automatic_completion AS "automatic_completion!",
397 course_modules.automatic_completion_number_of_exercises_attempted_treshold,
398 course_modules.automatic_completion_number_of_points_treshold,
399 course_modules.automatic_completion_requires_exam AS "automatic_completion_requires_exam!",
400 course_modules.completion_registration_link_override,
401 course_modules.ects_credits,
402 course_modules.enable_registering_completion_to_uh_open_university AS "enable_registering_completion_to_uh_open_university!",
403 course_modules.certification_enabled AS "certification_enabled!"
404FROM exercises
405 LEFT JOIN chapters ON (exercises.chapter_id = chapters.id)
406 LEFT JOIN course_modules ON (chapters.course_module_id = course_modules.id)
407WHERE exercises.id = $1
408AND chapters.deleted_at IS NULL
409AND course_modules.deleted_at IS NULL
410 "#,
411 exercise_id,
412 )
413 .fetch_one(conn)
414 .await?;
415 Ok(res.into())
416}
417
418pub async fn get_course_module_id_by_chapter(
419 conn: &mut PgConnection,
420 chapter_id: Uuid,
421) -> ModelResult<Uuid> {
422 let res: Uuid = sqlx::query!(
423 r#"
424SELECT c.course_module_id
425from chapters c
426where c.id = $1
427 AND deleted_at IS NULL
428 "#,
429 chapter_id
430 )
431 .map(|record| record.course_module_id)
432 .fetch_one(conn)
433 .await?;
434 Ok(res)
435}
436
437pub struct ModuleSizeCounts {
440 pub chapters: i64,
441 pub exercises: i64,
442}
443
444pub async fn get_chapter_and_exercise_counts(
445 conn: &mut PgConnection,
446 course_module_id: Uuid,
447) -> ModelResult<ModuleSizeCounts> {
448 let res = sqlx::query_as!(
449 ModuleSizeCounts,
450 r#"
451SELECT COUNT(DISTINCT c.id) AS "chapters!",
452 COUNT(e.id) AS "exercises!"
453FROM chapters c
454 LEFT JOIN exercises e ON e.chapter_id = c.id
455 AND e.deleted_at IS NULL
456WHERE c.course_module_id = $1
457 AND c.deleted_at IS NULL
458 "#,
459 course_module_id
460 )
461 .fetch_one(conn)
462 .await?;
463 Ok(res)
464}
465
466pub async fn get_default_by_course_id(
467 conn: &mut PgConnection,
468 course_id: Uuid,
469) -> ModelResult<CourseModule> {
470 let res = sqlx::query_as!(
471 CourseModulesSchema,
472 "
473SELECT *
474FROM course_modules
475WHERE course_id = $1
476 AND name IS NULL
477 AND deleted_at IS NULL
478 ",
479 course_id,
480 )
481 .fetch_one(conn)
482 .await?;
483 Ok(res.into())
484}
485
486pub async fn get_ids_by_course_slug_or_uh_course_code(
491 conn: &mut PgConnection,
492 course_slug_or_code: &str,
493) -> ModelResult<Vec<Uuid>> {
494 let res = sqlx::query!(
495 "
496SELECT course_modules.id
497FROM course_modules
498 LEFT JOIN courses ON (course_modules.course_id = courses.id)
499WHERE (
500 course_modules.uh_course_code = $1
501 OR courses.slug = $1
502 )
503 AND course_modules.deleted_at IS NULL
504 ",
505 course_slug_or_code,
506 )
507 .map(|record| record.id)
508 .fetch_all(conn)
509 .await?;
510 Ok(res)
511}
512
513pub async fn get_by_course_id_as_map(
515 conn: &mut PgConnection,
516 course_id: Uuid,
517) -> ModelResult<HashMap<Uuid, CourseModule>> {
518 let res = get_by_course_id(conn, course_id)
519 .await?
520 .into_iter()
521 .map(|course_module| (course_module.id, course_module))
522 .collect();
523 Ok(res)
524}
525
526pub async fn get_all_uh_course_codes_for_open_university(
527 conn: &mut PgConnection,
528) -> ModelResult<Vec<String>> {
529 let res = sqlx::query!(
530 "
531SELECT DISTINCT uh_course_code
532FROM course_modules
533WHERE uh_course_code IS NOT NULL
534 AND enable_registering_completion_to_uh_open_university = true
535 AND deleted_at IS NULL
536"
537 )
538 .fetch_all(conn)
539 .await?
540 .into_iter()
541 .filter_map(|x| x.uh_course_code)
542 .collect();
543 Ok(res)
544}
545
546#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
547
548pub struct AutomaticCompletionRequirements {
549 pub course_module_id: Uuid,
551 pub number_of_exercises_attempted_treshold: Option<i32>,
552 pub number_of_points_treshold: Option<i32>,
553 pub requires_exam: bool,
554}
555
556impl AutomaticCompletionRequirements {
557 pub fn passes_exercise_tresholds(
560 &self,
561 exercises_attempted: i32,
562 exercise_points: i32,
563 ) -> bool {
564 self.passes_number_of_exercises_attempted_treshold(exercises_attempted)
565 && self.passes_number_of_exercise_points_treshold(exercise_points)
566 }
567
568 pub fn passes_number_of_exercises_attempted_treshold(&self, exercises_attempted: i32) -> bool {
571 self.number_of_exercises_attempted_treshold
572 .map(|x| x <= exercises_attempted)
573 .unwrap_or(true)
574 }
575
576 pub fn passes_number_of_exercise_points_treshold(&self, exercise_points: i32) -> bool {
579 self.number_of_points_treshold
580 .map(|x| x <= exercise_points)
581 .unwrap_or(true)
582 }
583}
584
585#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ToSchema)]
586#[serde(tag = "policy", rename_all = "kebab-case")]
587pub enum CompletionPolicy {
588 Automatic(AutomaticCompletionRequirements),
589 Manual,
590}
591
592impl CompletionPolicy {
593 pub fn automatic(&self) -> Option<&AutomaticCompletionRequirements> {
595 match self {
596 CompletionPolicy::Automatic(requirements) => Some(requirements),
597 CompletionPolicy::Manual => None,
598 }
599 }
600
601 fn to_database_fields(&self) -> (bool, Option<i32>, Option<i32>, bool) {
602 match self {
603 CompletionPolicy::Automatic(requirements) => (
604 true,
605 requirements.number_of_exercises_attempted_treshold,
606 requirements.number_of_points_treshold,
607 requirements.requires_exam,
608 ),
609 CompletionPolicy::Manual => (false, None, None, false),
610 }
611 }
612}
613
614pub async fn update_automatic_completion_status(
615 conn: &mut PgConnection,
616 id: Uuid,
617 automatic_completion_policy: &CompletionPolicy,
618) -> ModelResult<CourseModule> {
619 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
620 automatic_completion_policy.to_database_fields();
621 let res = sqlx::query_as!(
622 CourseModulesSchema,
623 "
624UPDATE course_modules
625SET automatic_completion = $1,
626 automatic_completion_number_of_exercises_attempted_treshold = $2,
627 automatic_completion_number_of_points_treshold = $3,
628 automatic_completion_requires_exam = $4
629WHERE id = $5
630 AND deleted_at IS NULL
631RETURNING *
632 ",
633 automatic_completion,
634 exercises_treshold,
635 points_treshold,
636 requires_exam,
637 id,
638 )
639 .fetch_one(conn)
640 .await?;
641 Ok(res.into())
642}
643
644pub async fn update_uh_course_code(
645 conn: &mut PgConnection,
646 id: Uuid,
647 uh_course_code: Option<String>,
648) -> ModelResult<CourseModule> {
649 let res = sqlx::query_as!(
650 CourseModulesSchema,
651 "
652UPDATE course_modules
653SET uh_course_code = $1
654WHERE id = $2
655 AND deleted_at IS NULL
656RETURNING *
657 ",
658 uh_course_code,
659 id,
660 )
661 .fetch_one(conn)
662 .await?;
663 Ok(res.into())
664}
665
666pub async fn update_enable_registering_completion_to_uh_open_university(
667 conn: &mut PgConnection,
668 id: Uuid,
669 enable_registering_completion_to_uh_open_university: bool,
670) -> ModelResult<CourseModule> {
671 let res = sqlx::query_as!(
672 CourseModulesSchema,
673 "
674UPDATE course_modules
675SET enable_registering_completion_to_uh_open_university = $1
676WHERE id = $2
677 AND deleted_at IS NULL
678RETURNING *
679 ",
680 enable_registering_completion_to_uh_open_university,
681 id,
682 )
683 .fetch_one(conn)
684 .await?;
685 Ok(res.into())
686}
687
688#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
689
690pub struct NewModule {
691 name: String,
692 order_number: i32,
693 chapters: Vec<Uuid>,
694 uh_course_code: Option<String>,
695 ects_credits: Option<f32>,
696 completion_policy: CompletionPolicy,
697 completion_registration_link_override: Option<String>,
698 enable_registering_completion_to_uh_open_university: bool,
699}
700
701#[derive(Debug, Deserialize, ToSchema)]
702
703pub struct ModifiedModule {
704 id: Uuid,
705 name: Option<String>,
706 order_number: i32,
707 uh_course_code: Option<String>,
708 ects_credits: Option<f32>,
709 completion_policy: CompletionPolicy,
710 completion_registration_link_override: Option<String>,
711 enable_registering_completion_to_uh_open_university: bool,
712}
713
714#[derive(Debug, Deserialize, ToSchema)]
715
716pub struct ModuleUpdates {
717 new_modules: Vec<NewModule>,
718 deleted_modules: Vec<Uuid>,
719 modified_modules: Vec<ModifiedModule>,
720 moved_chapters: Vec<(Uuid, Uuid)>,
721}
722
723pub async fn update_with_order_number(
724 conn: &mut PgConnection,
725 id: Uuid,
726 name: Option<&str>,
727 order_number: i32,
728) -> ModelResult<()> {
729 sqlx::query!(
730 "
731UPDATE course_modules
732SET name = COALESCE($1, name),
733 order_number = $2
734WHERE id = $3
735",
736 name,
737 order_number,
738 id,
739 )
740 .execute(conn)
741 .await?;
742 Ok(())
743}
744
745pub async fn update(
746 conn: &mut PgConnection,
747 id: Uuid,
748 updated_course_module: &NewCourseModule,
749) -> ModelResult<()> {
750 let NewCourseModule {
752 completion_policy: _,
753 course_id: _,
754 ects_credits,
755 order_number,
756 name,
757 uh_course_code,
758 completion_registration_link_override,
759 enable_registering_completion_to_uh_open_university,
760 } = updated_course_module;
761 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
762 updated_course_module.completion_policy.to_database_fields();
763 sqlx::query!(
764 "
765UPDATE course_modules
766SET name = COALESCE($2, name),
767 order_number = $3,
768 uh_course_code = $4,
769 ects_credits = $5,
770 automatic_completion = $6,
771 automatic_completion_number_of_exercises_attempted_treshold = $7,
772 automatic_completion_number_of_points_treshold = $8,
773 automatic_completion_requires_exam = $9,
774 completion_registration_link_override = $10,
775 enable_registering_completion_to_uh_open_university = $11
776WHERE id = $1
777 ",
778 id,
779 name.as_ref(),
780 order_number,
781 uh_course_code.as_ref(),
782 ects_credits.as_ref(),
783 automatic_completion,
784 exercises_treshold,
785 points_treshold,
786 requires_exam,
787 completion_registration_link_override.as_ref(),
788 enable_registering_completion_to_uh_open_university
789 )
790 .execute(conn)
791 .await?;
792 Ok(())
793}
794
795pub async fn update_modules(
796 conn: &mut PgConnection,
797 course_id: Uuid,
798 updates: ModuleUpdates,
799) -> ModelResult<()> {
800 let mut tx = conn.begin().await?;
801
802 for module_id in updates
804 .modified_modules
805 .iter()
806 .filter(|m| m.order_number != 0)
808 .map(|m| m.id)
809 .chain(updates.deleted_modules.iter().copied())
810 {
811 update_with_order_number(&mut tx, module_id, None, rand::random()).await?;
812 }
813 let mut modified_and_new_modules = updates.modified_modules;
814 for new in updates.new_modules {
815 let NewModule {
817 name,
818 order_number,
819 chapters,
820 uh_course_code,
821 ects_credits,
822 completion_policy,
823 completion_registration_link_override,
824 enable_registering_completion_to_uh_open_university,
825 } = new;
826 let new_course_module = NewCourseModule::new(course_id, Some(name.clone()), rand::random())
828 .set_completion_policy(completion_policy.clone())
829 .set_completion_registration_link_override(completion_registration_link_override)
830 .set_ects_credits(ects_credits)
831 .set_uh_course_code(uh_course_code)
832 .set_enable_registering_completion_to_uh_open_university(
833 enable_registering_completion_to_uh_open_university,
834 );
835 let module = insert(&mut tx, PKeyPolicy::Generate, &new_course_module).await?;
836 for chapter in chapters {
837 chapters::set_module(&mut tx, chapter, module.id).await?;
838 }
839 modified_and_new_modules.push(ModifiedModule {
841 id: module.id,
842 name: None,
843 order_number,
844 uh_course_code: module.uh_course_code,
845 ects_credits,
846 completion_policy,
847 completion_registration_link_override: module.completion_registration_link_override,
848 enable_registering_completion_to_uh_open_university: module
849 .enable_registering_completion_to_uh_open_university,
850 })
851 }
852 for module in modified_and_new_modules {
854 let ModifiedModule {
856 id,
857 name,
858 order_number,
859 uh_course_code,
860 ects_credits,
861 completion_policy,
862 completion_registration_link_override,
863 enable_registering_completion_to_uh_open_university,
864 } = module;
865 update(
866 &mut tx,
867 id,
868 &NewCourseModule::new(course_id, name.clone(), order_number)
869 .set_completion_policy(completion_policy)
870 .set_completion_registration_link_override(completion_registration_link_override)
871 .set_ects_credits(ects_credits)
872 .set_uh_course_code(uh_course_code)
873 .set_enable_registering_completion_to_uh_open_university(
874 enable_registering_completion_to_uh_open_university,
875 ),
876 )
877 .await?;
878 }
879 for (chapter, module) in updates.moved_chapters {
880 chapters::set_module(&mut tx, chapter, module).await?;
881 }
882 for deleted in updates.deleted_modules {
883 delete(&mut tx, deleted).await?;
884 }
885
886 tx.commit().await?;
887 Ok(())
888}
889
890pub async fn update_certification_enabled(
891 conn: &mut PgConnection,
892 id: Uuid,
893 enabled: bool,
894) -> ModelResult<()> {
895 sqlx::query!(
896 "
897UPDATE course_modules
898SET certification_enabled = $1
899WHERE id = $2
900",
901 enabled,
902 id
903 )
904 .execute(conn)
905 .await?;
906 Ok(())
907}
908
909#[cfg(test)]
910mod tests {
911
912 mod automatic_completion_requirements {
913 use uuid::Uuid;
914
915 use super::super::AutomaticCompletionRequirements;
916
917 #[test]
918 fn passes_exercise_tresholds() {
919 let requirements1 = AutomaticCompletionRequirements {
920 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
921 number_of_exercises_attempted_treshold: Some(10),
922 number_of_points_treshold: Some(50),
923 requires_exam: false,
924 };
925 let requirements2 = AutomaticCompletionRequirements {
926 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
927 number_of_exercises_attempted_treshold: Some(50),
928 number_of_points_treshold: Some(10),
929 requires_exam: false,
930 };
931
932 let requirements3 = AutomaticCompletionRequirements {
933 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
934 number_of_exercises_attempted_treshold: Some(0),
935 number_of_points_treshold: Some(0),
936 requires_exam: false,
937 };
938
939 let requirements4 = AutomaticCompletionRequirements {
940 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
941 number_of_exercises_attempted_treshold: Some(10),
942 number_of_points_treshold: None,
943 requires_exam: false,
944 };
945
946 let requirements5 = AutomaticCompletionRequirements {
947 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
948 number_of_exercises_attempted_treshold: None,
949 number_of_points_treshold: Some(10),
950 requires_exam: false,
951 };
952 assert!(requirements1.passes_exercise_tresholds(10, 50));
953 assert!(requirements2.passes_exercise_tresholds(50, 10));
954
955 assert!(!requirements1.passes_exercise_tresholds(50, 10));
956 assert!(!requirements2.passes_exercise_tresholds(10, 50));
957
958 assert!(!requirements1.passes_exercise_tresholds(100, 0));
959 assert!(!requirements2.passes_exercise_tresholds(100, 0));
960
961 assert!(requirements3.passes_exercise_tresholds(1, 1));
962 assert!(requirements3.passes_exercise_tresholds(0, 0));
963
964 assert!(requirements4.passes_exercise_tresholds(10, 1));
965 assert!(!requirements4.passes_exercise_tresholds(1, 10));
966
967 assert!(requirements5.passes_exercise_tresholds(0, 10));
968 assert!(!requirements5.passes_exercise_tresholds(10, 0));
969 }
970 }
971}