1use std::collections::HashMap;
2
3use utoipa::ToSchema;
4
5use crate::{chapters, prelude::*};
6
7struct CourseModulesSchema {
10 id: Uuid,
11 created_at: DateTime<Utc>,
12 updated_at: DateTime<Utc>,
13 deleted_at: Option<DateTime<Utc>>,
14 name: Option<String>,
15 course_id: Uuid,
16 order_number: i32,
17 copied_from: Option<Uuid>,
18 uh_course_code: Option<String>,
19 automatic_completion: bool,
20 automatic_completion_number_of_exercises_attempted_treshold: Option<i32>,
21 automatic_completion_number_of_points_treshold: Option<i32>,
22 automatic_completion_requires_exam: bool,
23 completion_registration_link_override: Option<String>,
24 ects_credits: Option<f32>,
25 enable_registering_completion_to_uh_open_university: bool,
26 certification_enabled: bool,
27}
28#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
32
33pub struct CourseModule {
34 pub id: Uuid,
35 pub created_at: DateTime<Utc>,
36 pub updated_at: DateTime<Utc>,
37 pub deleted_at: Option<DateTime<Utc>>,
38 pub name: Option<String>,
39 pub course_id: Uuid,
40 pub order_number: i32,
41 pub copied_from: Option<Uuid>,
42 pub uh_course_code: Option<String>,
43 pub completion_policy: CompletionPolicy,
44 pub completion_registration_link_override: Option<String>,
46 pub ects_credits: Option<f32>,
47 pub enable_registering_completion_to_uh_open_university: bool,
48 pub certification_enabled: bool,
49}
50
51impl CourseModule {
52 pub fn new(id: Uuid, course_id: Uuid) -> Self {
53 Self {
54 id,
55 created_at: Utc::now(),
56 updated_at: Utc::now(),
57 deleted_at: None,
58 name: None,
59 course_id,
60 order_number: 0,
61 copied_from: None,
62 uh_course_code: None,
63 completion_policy: CompletionPolicy::Manual,
64 completion_registration_link_override: None,
65 ects_credits: None,
66 enable_registering_completion_to_uh_open_university: false,
67 certification_enabled: false,
68 }
69 }
70 pub fn set_timestamps(
71 mut self,
72 created_at: DateTime<Utc>,
73 updated_at: DateTime<Utc>,
74 deleted_at: Option<DateTime<Utc>>,
75 ) -> Self {
76 self.created_at = created_at;
77 self.updated_at = updated_at;
78 self.deleted_at = deleted_at;
79 self
80 }
81
82 pub fn set_name_and_order_number(mut self, name: Option<String>, order_number: i32) -> Self {
84 self.name = name;
85 self.order_number = order_number;
86 self
87 }
88
89 pub fn set_completion_policy(mut self, completion_policy: CompletionPolicy) -> Self {
90 self.completion_policy = completion_policy;
91 self
92 }
93
94 pub fn set_registration_info(
95 mut self,
96 uh_course_code: Option<String>,
97 ects_credits: Option<f32>,
98 completion_registration_link_override: Option<String>,
99 enable_registering_completion_to_uh_open_university: bool,
100 ) -> Self {
101 self.uh_course_code = uh_course_code;
102 self.ects_credits = ects_credits;
103 self.completion_registration_link_override = completion_registration_link_override;
104 self.enable_registering_completion_to_uh_open_university =
105 enable_registering_completion_to_uh_open_university;
106 self
107 }
108
109 pub fn set_certification_enabled(mut self, certification_enabled: bool) -> Self {
110 self.certification_enabled = certification_enabled;
111 self
112 }
113
114 pub fn is_default_module(&self) -> bool {
115 self.name.is_none()
116 }
117}
118
119impl From<CourseModulesSchema> for CourseModule {
120 fn from(schema: CourseModulesSchema) -> Self {
121 let completion_policy = if schema.automatic_completion {
122 CompletionPolicy::Automatic(AutomaticCompletionRequirements {
123 course_module_id: schema.id,
124 number_of_exercises_attempted_treshold: schema
125 .automatic_completion_number_of_exercises_attempted_treshold,
126 number_of_points_treshold: schema.automatic_completion_number_of_points_treshold,
127 requires_exam: schema.automatic_completion_requires_exam,
128 })
129 } else {
130 CompletionPolicy::Manual
131 };
132 Self {
133 id: schema.id,
134 created_at: schema.created_at,
135 updated_at: schema.updated_at,
136 deleted_at: schema.deleted_at,
137 name: schema.name,
138 course_id: schema.course_id,
139 order_number: schema.order_number,
140 copied_from: schema.copied_from,
141 uh_course_code: schema.uh_course_code,
142 completion_policy,
143 completion_registration_link_override: schema.completion_registration_link_override,
144 ects_credits: schema.ects_credits,
145 enable_registering_completion_to_uh_open_university: schema
146 .enable_registering_completion_to_uh_open_university,
147 certification_enabled: schema.certification_enabled,
148 }
149 }
150}
151
152#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
153
154pub struct NewCourseModule {
155 completion_policy: CompletionPolicy,
156 completion_registration_link_override: Option<String>,
157 course_id: Uuid,
158 ects_credits: Option<f32>,
159 name: Option<String>,
160 order_number: i32,
161 uh_course_code: Option<String>,
162 enable_registering_completion_to_uh_open_university: bool,
163}
164
165impl NewCourseModule {
166 pub fn new(course_id: Uuid, name: Option<String>, order_number: i32) -> Self {
167 Self {
168 completion_policy: CompletionPolicy::Manual,
169 completion_registration_link_override: None,
170 course_id,
171 ects_credits: None,
172 name,
173 order_number,
174 uh_course_code: None,
175 enable_registering_completion_to_uh_open_university: false,
176 }
177 }
178
179 pub fn new_course_default(course_id: Uuid) -> Self {
180 Self::new(course_id, None, 0)
181 }
182
183 pub fn set_uh_course_code(mut self, uh_course_code: Option<String>) -> Self {
184 self.uh_course_code = uh_course_code;
185 self
186 }
187
188 pub fn set_completion_policy(mut self, completion_policy: CompletionPolicy) -> Self {
189 self.completion_policy = completion_policy;
190 self
191 }
192
193 pub fn set_completion_registration_link_override(
194 mut self,
195 completion_registration_link_override: Option<String>,
196 ) -> Self {
197 self.completion_registration_link_override = completion_registration_link_override;
198 self
199 }
200
201 pub fn set_ects_credits(mut self, ects_credits: Option<f32>) -> Self {
202 self.ects_credits = ects_credits;
203 self
204 }
205
206 pub fn set_enable_registering_completion_to_uh_open_university(
207 mut self,
208 enable_registering_completion_to_uh_open_university: bool,
209 ) -> Self {
210 self.enable_registering_completion_to_uh_open_university =
211 enable_registering_completion_to_uh_open_university;
212 self
213 }
214}
215
216pub async fn insert(
217 conn: &mut PgConnection,
218 pkey_policy: PKeyPolicy<Uuid>,
219 new_course_module: &NewCourseModule,
220) -> ModelResult<CourseModule> {
221 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
222 new_course_module.completion_policy.to_database_fields();
223 let res = sqlx::query_as!(
224 CourseModulesSchema,
225 "
226INSERT INTO course_modules (
227 id,
228 course_id,
229 name,
230 order_number,
231 automatic_completion,
232 automatic_completion_number_of_exercises_attempted_treshold,
233 automatic_completion_number_of_points_treshold,
234 automatic_completion_requires_exam,
235 ects_credits,
236 enable_registering_completion_to_uh_open_university,
237 uh_course_code
238 )
239VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
240RETURNING id,
241 created_at,
242 updated_at,
243 deleted_at,
244 name,
245 course_id,
246 order_number,
247 copied_from,
248 uh_course_code,
249 automatic_completion,
250 automatic_completion_number_of_exercises_attempted_treshold,
251 automatic_completion_number_of_points_treshold,
252 automatic_completion_requires_exam,
253 completion_registration_link_override,
254 ects_credits,
255 enable_registering_completion_to_uh_open_university,
256 certification_enabled
257 ",
258 pkey_policy.into_uuid(),
259 new_course_module.course_id,
260 new_course_module.name,
261 new_course_module.order_number,
262 automatic_completion,
263 exercises_treshold,
264 points_treshold,
265 requires_exam,
266 new_course_module.ects_credits,
267 new_course_module.enable_registering_completion_to_uh_open_university,
268 new_course_module.uh_course_code
269 )
270 .fetch_one(conn)
271 .await?;
272 Ok(res.into())
273}
274
275pub async fn rename(conn: &mut PgConnection, id: Uuid, name: &str) -> ModelResult<()> {
276 sqlx::query!(
277 "
278UPDATE course_modules
279SET name = $1
280WHERE id = $2
281",
282 name,
283 id
284 )
285 .execute(conn)
286 .await?;
287 Ok(())
288}
289
290pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
291 let associated_chapters = chapters::get_for_module(conn, id).await?;
292 if !associated_chapters.is_empty() {
293 return Err(ModelError::new(
294 ModelErrorType::InvalidRequest,
295 format!(
296 "Cannot remove module {id} because it has {} chapters associated with it",
297 associated_chapters.len()
298 ),
299 None,
300 ));
301 }
302 sqlx::query!(
303 "
304UPDATE course_modules
305SET deleted_at = now()
306WHERE id = $1
307AND deleted_at IS NULL
308",
309 id
310 )
311 .execute(conn)
312 .await?;
313 Ok(())
314}
315
316pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CourseModule> {
317 let res = sqlx::query_as!(
318 CourseModulesSchema,
319 "
320SELECT id,
321 created_at,
322 updated_at,
323 deleted_at,
324 name,
325 course_id,
326 order_number,
327 copied_from,
328 uh_course_code,
329 automatic_completion,
330 automatic_completion_number_of_exercises_attempted_treshold,
331 automatic_completion_number_of_points_treshold,
332 automatic_completion_requires_exam,
333 completion_registration_link_override,
334 ects_credits,
335 enable_registering_completion_to_uh_open_university,
336 certification_enabled
337FROM course_modules
338WHERE id = $1
339 AND deleted_at IS NULL
340 ",
341 id,
342 )
343 .fetch_one(conn)
344 .await?;
345 Ok(res.into())
346}
347
348pub async fn get_by_ids(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<Vec<CourseModule>> {
349 let res = sqlx::query_as!(
350 CourseModulesSchema,
351 "
352SELECT id,
353 created_at,
354 updated_at,
355 deleted_at,
356 name,
357 course_id,
358 order_number,
359 copied_from,
360 uh_course_code,
361 automatic_completion,
362 automatic_completion_number_of_exercises_attempted_treshold,
363 automatic_completion_number_of_points_treshold,
364 automatic_completion_requires_exam,
365 completion_registration_link_override,
366 ects_credits,
367 enable_registering_completion_to_uh_open_university,
368 certification_enabled
369FROM course_modules
370WHERE id = ANY($1)
371 AND deleted_at IS NULL
372 ",
373 ids,
374 )
375 .map(|x| x.into())
376 .fetch_all(conn)
377 .await?;
378 Ok(res)
379}
380
381pub async fn get_by_course_id(
382 conn: &mut PgConnection,
383 course_id: Uuid,
384) -> ModelResult<Vec<CourseModule>> {
385 let res = sqlx::query_as!(
386 CourseModulesSchema,
387 "
388SELECT id,
389 created_at,
390 updated_at,
391 deleted_at,
392 name,
393 course_id,
394 order_number,
395 copied_from,
396 uh_course_code,
397 automatic_completion,
398 automatic_completion_number_of_exercises_attempted_treshold,
399 automatic_completion_number_of_points_treshold,
400 automatic_completion_requires_exam,
401 completion_registration_link_override,
402 ects_credits,
403 enable_registering_completion_to_uh_open_university,
404 certification_enabled
405FROM course_modules
406WHERE course_id = $1
407AND deleted_at IS NULL
408",
409 course_id
410 )
411 .map(|x| x.into())
412 .fetch_all(conn)
413 .await?;
414 Ok(res)
415}
416
417pub async fn get_by_course_ids(
420 conn: &mut PgConnection,
421 course_ids: &[Uuid],
422) -> ModelResult<Vec<CourseModule>> {
423 let res = sqlx::query_as!(
424 CourseModulesSchema,
425 "
426SELECT id,
427 created_at,
428 updated_at,
429 deleted_at,
430 name,
431 course_id,
432 order_number,
433 copied_from,
434 uh_course_code,
435 automatic_completion,
436 automatic_completion_number_of_exercises_attempted_treshold,
437 automatic_completion_number_of_points_treshold,
438 automatic_completion_requires_exam,
439 completion_registration_link_override,
440 ects_credits,
441 enable_registering_completion_to_uh_open_university,
442 certification_enabled
443FROM course_modules
444WHERE course_id = ANY($1)
445AND deleted_at IS NULL
446",
447 course_ids
448 )
449 .map(|x| x.into())
450 .fetch_all(conn)
451 .await?;
452 Ok(res)
453}
454
455pub async fn get_by_course_id_only_with_open_chapters(
456 conn: &mut PgConnection,
457 course_id: Uuid,
458) -> ModelResult<Vec<CourseModule>> {
459 let res = sqlx::query_as!(
460 CourseModulesSchema,
461 "
462SELECT cm.id,
463 cm.created_at,
464 cm.updated_at,
465 cm.deleted_at,
466 cm.name,
467 cm.course_id,
468 cm.order_number,
469 cm.copied_from,
470 cm.uh_course_code,
471 cm.automatic_completion,
472 cm.automatic_completion_number_of_exercises_attempted_treshold,
473 cm.automatic_completion_number_of_points_treshold,
474 cm.automatic_completion_requires_exam,
475 cm.completion_registration_link_override,
476 cm.ects_credits,
477 cm.enable_registering_completion_to_uh_open_university,
478 cm.certification_enabled
479FROM course_modules as cm
480WHERE EXISTS (
481 SELECT 1
482 FROM chapters as ch
483 WHERE ch.course_module_id = cm.id
484 AND ((ch.opens_at < now()) OR ch.opens_at IS NULL)
485 AND ch.deleted_at IS NULL
486)
487 AND cm.course_id = $1
488 AND cm.deleted_at IS NULL
489",
490 course_id
491 )
492 .map(|x| x.into())
493 .fetch_all(conn)
494 .await?;
495 Ok(res)
496}
497
498pub async fn get_by_exercise_id(
501 conn: &mut PgConnection,
502 exercise_id: Uuid,
503) -> ModelResult<CourseModule> {
504 let res = sqlx::query_as!(
505 CourseModulesSchema,
506 r#"
507SELECT
508 course_modules.id AS "id!",
509 course_modules.created_at AS "created_at!",
510 course_modules.updated_at AS "updated_at!",
511 course_modules.deleted_at,
512 course_modules.name,
513 course_modules.course_id AS "course_id!",
514 course_modules.order_number AS "order_number!",
515 course_modules.copied_from,
516 course_modules.uh_course_code,
517 course_modules.automatic_completion AS "automatic_completion!",
518 course_modules.automatic_completion_number_of_exercises_attempted_treshold,
519 course_modules.automatic_completion_number_of_points_treshold,
520 course_modules.automatic_completion_requires_exam AS "automatic_completion_requires_exam!",
521 course_modules.completion_registration_link_override,
522 course_modules.ects_credits,
523 course_modules.enable_registering_completion_to_uh_open_university AS "enable_registering_completion_to_uh_open_university!",
524 course_modules.certification_enabled AS "certification_enabled!"
525FROM exercises
526 LEFT JOIN chapters ON (exercises.chapter_id = chapters.id)
527 LEFT JOIN course_modules ON (chapters.course_module_id = course_modules.id)
528WHERE exercises.id = $1
529AND chapters.deleted_at IS NULL
530AND course_modules.deleted_at IS NULL
531 "#,
532 exercise_id,
533 )
534 .fetch_one(conn)
535 .await?;
536 Ok(res.into())
537}
538
539pub async fn get_course_module_id_by_chapter(
540 conn: &mut PgConnection,
541 chapter_id: Uuid,
542) -> ModelResult<Uuid> {
543 let res: Uuid = sqlx::query!(
544 r#"
545SELECT c.course_module_id
546from chapters c
547where c.id = $1
548 AND deleted_at IS NULL
549 "#,
550 chapter_id
551 )
552 .map(|record| record.course_module_id)
553 .fetch_one(conn)
554 .await?;
555 Ok(res)
556}
557
558pub struct ModuleSizeCounts {
561 pub chapters: i64,
562 pub exercises: i64,
563}
564
565pub async fn get_chapter_and_exercise_counts(
566 conn: &mut PgConnection,
567 course_module_id: Uuid,
568) -> ModelResult<ModuleSizeCounts> {
569 let res = sqlx::query_as!(
570 ModuleSizeCounts,
571 r#"
572SELECT COUNT(DISTINCT c.id) AS "chapters!",
573 COUNT(e.id) AS "exercises!"
574FROM chapters c
575 LEFT JOIN exercises e ON e.chapter_id = c.id
576 AND e.deleted_at IS NULL
577WHERE c.course_module_id = $1
578 AND c.deleted_at IS NULL
579 "#,
580 course_module_id
581 )
582 .fetch_one(conn)
583 .await?;
584 Ok(res)
585}
586
587pub async fn get_default_by_course_id(
588 conn: &mut PgConnection,
589 course_id: Uuid,
590) -> ModelResult<CourseModule> {
591 let res = sqlx::query_as!(
592 CourseModulesSchema,
593 "
594SELECT id,
595 created_at,
596 updated_at,
597 deleted_at,
598 name,
599 course_id,
600 order_number,
601 copied_from,
602 uh_course_code,
603 automatic_completion,
604 automatic_completion_number_of_exercises_attempted_treshold,
605 automatic_completion_number_of_points_treshold,
606 automatic_completion_requires_exam,
607 completion_registration_link_override,
608 ects_credits,
609 enable_registering_completion_to_uh_open_university,
610 certification_enabled
611FROM course_modules
612WHERE course_id = $1
613 AND name IS NULL
614 AND deleted_at IS NULL
615 ",
616 course_id,
617 )
618 .fetch_one(conn)
619 .await?;
620 Ok(res.into())
621}
622
623pub async fn get_ids_by_course_slug_or_uh_course_code(
628 conn: &mut PgConnection,
629 course_slug_or_code: &str,
630) -> ModelResult<Vec<Uuid>> {
631 let res = sqlx::query!(
632 "
633SELECT course_modules.id
634FROM course_modules
635 LEFT JOIN courses ON (course_modules.course_id = courses.id)
636WHERE (
637 course_modules.uh_course_code = $1
638 OR courses.slug = $1
639 )
640 AND course_modules.deleted_at IS NULL
641 ",
642 course_slug_or_code,
643 )
644 .map(|record| record.id)
645 .fetch_all(conn)
646 .await?;
647 Ok(res)
648}
649
650pub async fn get_by_course_id_as_map(
652 conn: &mut PgConnection,
653 course_id: Uuid,
654) -> ModelResult<HashMap<Uuid, CourseModule>> {
655 let res = get_by_course_id(conn, course_id)
656 .await?
657 .into_iter()
658 .map(|course_module| (course_module.id, course_module))
659 .collect();
660 Ok(res)
661}
662
663pub async fn get_all_uh_course_codes_for_open_university(
664 conn: &mut PgConnection,
665) -> ModelResult<Vec<String>> {
666 let res = sqlx::query!(
667 "
668SELECT DISTINCT uh_course_code
669FROM course_modules
670WHERE uh_course_code IS NOT NULL
671 AND enable_registering_completion_to_uh_open_university = true
672 AND deleted_at IS NULL
673"
674 )
675 .fetch_all(conn)
676 .await?
677 .into_iter()
678 .filter_map(|x| x.uh_course_code)
679 .collect();
680 Ok(res)
681}
682
683#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
687pub struct CourseModuleCreditRegistrationConfig {
688 pub course_module_id: Uuid,
689 pub course_id: Uuid,
690 pub enable_credit_registration_via_suotar: bool,
691 pub uh_course_code: Option<String>,
692 pub ects_credits: Option<f32>,
693 pub open_university_product_id: Option<String>,
694 pub credit_registration_grade_scale_id: Option<String>,
696 pub credit_registration_paused_at: Option<DateTime<Utc>>,
697 pub credit_registration_paused_by_user_id: Option<Uuid>,
698 pub credit_registration_pause_reason: Option<String>,
699 pub credit_registration_config_checked_at: Option<DateTime<Utc>>,
700 pub credit_registration_course_code_resolves: Option<bool>,
702 pub credit_registration_product_token_found: Option<bool>,
703 pub credit_registration_config_check_message: Option<String>,
704}
705
706pub async fn get_credit_registration_config(
707 conn: &mut PgConnection,
708 course_module_id: Uuid,
709) -> ModelResult<CourseModuleCreditRegistrationConfig> {
710 let res = sqlx::query_as!(
711 CourseModuleCreditRegistrationConfig,
712 r#"
713SELECT cm.id AS course_module_id,
714 cm.course_id,
715 cm.enable_credit_registration_via_suotar,
716 cm.uh_course_code,
717 cm.ects_credits,
718 c.open_university_product_id AS "open_university_product_id?",
719 c.grade_scale_id AS "credit_registration_grade_scale_id?",
720 c.paused_at AS "credit_registration_paused_at?",
721 c.paused_by_user_id AS "credit_registration_paused_by_user_id?",
722 c.pause_reason AS "credit_registration_pause_reason?",
723 c.config_checked_at AS "credit_registration_config_checked_at?",
724 c.course_code_resolves AS "credit_registration_course_code_resolves?",
725 c.product_token_found AS "credit_registration_product_token_found?",
726 c.config_check_message AS "credit_registration_config_check_message?"
727FROM course_modules cm
728 LEFT JOIN course_module_suotar_configurations c ON c.course_module_id = cm.id
729 AND c.deleted_at IS NULL
730WHERE cm.id = $1
731 AND cm.deleted_at IS NULL
732 "#,
733 course_module_id
734 )
735 .fetch_one(conn)
736 .await?;
737 Ok(res)
738}
739
740pub async fn get_all_suotar_enabled(
742 conn: &mut PgConnection,
743) -> ModelResult<Vec<CourseModuleCreditRegistrationConfig>> {
744 let res = sqlx::query_as!(
745 CourseModuleCreditRegistrationConfig,
746 r#"
747SELECT cm.id AS course_module_id,
748 cm.course_id,
749 cm.enable_credit_registration_via_suotar,
750 cm.uh_course_code,
751 cm.ects_credits,
752 c.open_university_product_id AS "open_university_product_id?",
753 c.grade_scale_id AS "credit_registration_grade_scale_id?",
754 c.paused_at AS "credit_registration_paused_at?",
755 c.paused_by_user_id AS "credit_registration_paused_by_user_id?",
756 c.pause_reason AS "credit_registration_pause_reason?",
757 c.config_checked_at AS "credit_registration_config_checked_at?",
758 c.course_code_resolves AS "credit_registration_course_code_resolves?",
759 c.product_token_found AS "credit_registration_product_token_found?",
760 c.config_check_message AS "credit_registration_config_check_message?"
761FROM course_modules cm
762 LEFT JOIN course_module_suotar_configurations c ON c.course_module_id = cm.id
763 AND c.deleted_at IS NULL
764WHERE cm.enable_credit_registration_via_suotar
765 AND cm.deleted_at IS NULL
766ORDER BY cm.course_id,
767 cm.order_number
768 "#,
769 )
770 .fetch_all(conn)
771 .await?;
772 Ok(res)
773}
774
775#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
776
777pub struct AutomaticCompletionRequirements {
778 pub course_module_id: Uuid,
780 pub number_of_exercises_attempted_treshold: Option<i32>,
781 pub number_of_points_treshold: Option<i32>,
782 pub requires_exam: bool,
783}
784
785impl AutomaticCompletionRequirements {
786 pub fn passes_exercise_tresholds(
789 &self,
790 exercises_attempted: i32,
791 exercise_points: i32,
792 ) -> bool {
793 self.passes_number_of_exercises_attempted_treshold(exercises_attempted)
794 && self.passes_number_of_exercise_points_treshold(exercise_points)
795 }
796
797 pub fn passes_number_of_exercises_attempted_treshold(&self, exercises_attempted: i32) -> bool {
800 self.number_of_exercises_attempted_treshold
801 .map(|x| x <= exercises_attempted)
802 .unwrap_or(true)
803 }
804
805 pub fn passes_number_of_exercise_points_treshold(&self, exercise_points: i32) -> bool {
808 self.number_of_points_treshold
809 .map(|x| x <= exercise_points)
810 .unwrap_or(true)
811 }
812}
813
814#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ToSchema)]
815#[serde(tag = "policy", rename_all = "kebab-case")]
816pub enum CompletionPolicy {
817 Automatic(AutomaticCompletionRequirements),
818 Manual,
819}
820
821impl CompletionPolicy {
822 pub fn automatic(&self) -> Option<&AutomaticCompletionRequirements> {
824 match self {
825 CompletionPolicy::Automatic(requirements) => Some(requirements),
826 CompletionPolicy::Manual => None,
827 }
828 }
829
830 fn to_database_fields(&self) -> (bool, Option<i32>, Option<i32>, bool) {
831 match self {
832 CompletionPolicy::Automatic(requirements) => (
833 true,
834 requirements.number_of_exercises_attempted_treshold,
835 requirements.number_of_points_treshold,
836 requirements.requires_exam,
837 ),
838 CompletionPolicy::Manual => (false, None, None, false),
839 }
840 }
841}
842
843pub async fn update_automatic_completion_status(
844 conn: &mut PgConnection,
845 id: Uuid,
846 automatic_completion_policy: &CompletionPolicy,
847) -> ModelResult<CourseModule> {
848 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
849 automatic_completion_policy.to_database_fields();
850 let res = sqlx::query_as!(
851 CourseModulesSchema,
852 "
853UPDATE course_modules
854SET automatic_completion = $1,
855 automatic_completion_number_of_exercises_attempted_treshold = $2,
856 automatic_completion_number_of_points_treshold = $3,
857 automatic_completion_requires_exam = $4
858WHERE id = $5
859 AND deleted_at IS NULL
860RETURNING id,
861 created_at,
862 updated_at,
863 deleted_at,
864 name,
865 course_id,
866 order_number,
867 copied_from,
868 uh_course_code,
869 automatic_completion,
870 automatic_completion_number_of_exercises_attempted_treshold,
871 automatic_completion_number_of_points_treshold,
872 automatic_completion_requires_exam,
873 completion_registration_link_override,
874 ects_credits,
875 enable_registering_completion_to_uh_open_university,
876 certification_enabled
877 ",
878 automatic_completion,
879 exercises_treshold,
880 points_treshold,
881 requires_exam,
882 id,
883 )
884 .fetch_one(conn)
885 .await?;
886 Ok(res.into())
887}
888
889pub async fn update_uh_course_code(
890 conn: &mut PgConnection,
891 id: Uuid,
892 uh_course_code: Option<String>,
893) -> ModelResult<CourseModule> {
894 let res = sqlx::query_as!(
895 CourseModulesSchema,
896 "
897UPDATE course_modules
898SET uh_course_code = $1
899WHERE id = $2
900 AND deleted_at IS NULL
901RETURNING id,
902 created_at,
903 updated_at,
904 deleted_at,
905 name,
906 course_id,
907 order_number,
908 copied_from,
909 uh_course_code,
910 automatic_completion,
911 automatic_completion_number_of_exercises_attempted_treshold,
912 automatic_completion_number_of_points_treshold,
913 automatic_completion_requires_exam,
914 completion_registration_link_override,
915 ects_credits,
916 enable_registering_completion_to_uh_open_university,
917 certification_enabled
918 ",
919 uh_course_code,
920 id,
921 )
922 .fetch_one(conn)
923 .await?;
924 Ok(res.into())
925}
926
927pub async fn update_enable_registering_completion_to_uh_open_university(
928 conn: &mut PgConnection,
929 id: Uuid,
930 enable_registering_completion_to_uh_open_university: bool,
931) -> ModelResult<CourseModule> {
932 let res = sqlx::query_as!(
933 CourseModulesSchema,
934 "
935UPDATE course_modules
936SET enable_registering_completion_to_uh_open_university = $1
937WHERE id = $2
938 AND deleted_at IS NULL
939RETURNING id,
940 created_at,
941 updated_at,
942 deleted_at,
943 name,
944 course_id,
945 order_number,
946 copied_from,
947 uh_course_code,
948 automatic_completion,
949 automatic_completion_number_of_exercises_attempted_treshold,
950 automatic_completion_number_of_points_treshold,
951 automatic_completion_requires_exam,
952 completion_registration_link_override,
953 ects_credits,
954 enable_registering_completion_to_uh_open_university,
955 certification_enabled
956 ",
957 enable_registering_completion_to_uh_open_university,
958 id,
959 )
960 .fetch_one(conn)
961 .await?;
962 Ok(res.into())
963}
964
965#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
966
967pub struct NewModule {
968 name: String,
969 order_number: i32,
970 chapters: Vec<Uuid>,
971 uh_course_code: Option<String>,
972 ects_credits: Option<f32>,
973 completion_policy: CompletionPolicy,
974 completion_registration_link_override: Option<String>,
975 enable_registering_completion_to_uh_open_university: bool,
976}
977
978#[derive(Debug, Deserialize, ToSchema)]
979
980pub struct ModifiedModule {
981 id: Uuid,
982 name: Option<String>,
983 order_number: i32,
984 uh_course_code: Option<String>,
985 ects_credits: Option<f32>,
986 completion_policy: CompletionPolicy,
987 completion_registration_link_override: Option<String>,
988 enable_registering_completion_to_uh_open_university: bool,
989}
990
991#[derive(Debug, Deserialize, ToSchema)]
992
993pub struct ModuleUpdates {
994 new_modules: Vec<NewModule>,
995 deleted_modules: Vec<Uuid>,
996 modified_modules: Vec<ModifiedModule>,
997 moved_chapters: Vec<(Uuid, Uuid)>,
998}
999
1000pub async fn update_with_order_number(
1001 conn: &mut PgConnection,
1002 id: Uuid,
1003 name: Option<&str>,
1004 order_number: i32,
1005) -> ModelResult<()> {
1006 sqlx::query!(
1007 "
1008UPDATE course_modules
1009SET name = COALESCE($1, name),
1010 order_number = $2
1011WHERE id = $3
1012",
1013 name,
1014 order_number,
1015 id,
1016 )
1017 .execute(conn)
1018 .await?;
1019 Ok(())
1020}
1021
1022pub async fn update(
1023 conn: &mut PgConnection,
1024 id: Uuid,
1025 updated_course_module: &NewCourseModule,
1026) -> ModelResult<()> {
1027 let NewCourseModule {
1029 completion_policy: _,
1030 course_id: _,
1031 ects_credits,
1032 order_number,
1033 name,
1034 uh_course_code,
1035 completion_registration_link_override,
1036 enable_registering_completion_to_uh_open_university,
1037 } = updated_course_module;
1038 let (automatic_completion, exercises_treshold, points_treshold, requires_exam) =
1039 updated_course_module.completion_policy.to_database_fields();
1040 sqlx::query!(
1041 "
1042UPDATE course_modules
1043SET name = COALESCE($2, name),
1044 order_number = $3,
1045 uh_course_code = $4,
1046 ects_credits = $5,
1047 automatic_completion = $6,
1048 automatic_completion_number_of_exercises_attempted_treshold = $7,
1049 automatic_completion_number_of_points_treshold = $8,
1050 automatic_completion_requires_exam = $9,
1051 completion_registration_link_override = $10,
1052 enable_registering_completion_to_uh_open_university = $11
1053WHERE id = $1
1054 ",
1055 id,
1056 name.as_ref(),
1057 order_number,
1058 uh_course_code.as_ref(),
1059 ects_credits.as_ref(),
1060 automatic_completion,
1061 exercises_treshold,
1062 points_treshold,
1063 requires_exam,
1064 completion_registration_link_override.as_ref(),
1065 enable_registering_completion_to_uh_open_university
1066 )
1067 .execute(conn)
1068 .await?;
1069 Ok(())
1070}
1071
1072pub async fn update_modules(
1073 conn: &mut PgConnection,
1074 course_id: Uuid,
1075 updates: ModuleUpdates,
1076) -> ModelResult<()> {
1077 let mut tx = conn.begin().await?;
1078
1079 for module_id in updates
1081 .modified_modules
1082 .iter()
1083 .filter(|m| m.order_number != 0)
1085 .map(|m| m.id)
1086 .chain(updates.deleted_modules.iter().copied())
1087 {
1088 update_with_order_number(&mut tx, module_id, None, rand::random()).await?;
1089 }
1090 let mut modified_and_new_modules = updates.modified_modules;
1091 for new in updates.new_modules {
1092 let NewModule {
1094 name,
1095 order_number,
1096 chapters,
1097 uh_course_code,
1098 ects_credits,
1099 completion_policy,
1100 completion_registration_link_override,
1101 enable_registering_completion_to_uh_open_university,
1102 } = new;
1103 let new_course_module = NewCourseModule::new(course_id, Some(name.clone()), rand::random())
1105 .set_completion_policy(completion_policy.clone())
1106 .set_completion_registration_link_override(completion_registration_link_override)
1107 .set_ects_credits(ects_credits)
1108 .set_uh_course_code(uh_course_code)
1109 .set_enable_registering_completion_to_uh_open_university(
1110 enable_registering_completion_to_uh_open_university,
1111 );
1112 let module = insert(&mut tx, PKeyPolicy::Generate, &new_course_module).await?;
1113 for chapter in chapters {
1114 chapters::set_module(&mut tx, chapter, module.id).await?;
1115 }
1116 modified_and_new_modules.push(ModifiedModule {
1118 id: module.id,
1119 name: None,
1120 order_number,
1121 uh_course_code: module.uh_course_code,
1122 ects_credits,
1123 completion_policy,
1124 completion_registration_link_override: module.completion_registration_link_override,
1125 enable_registering_completion_to_uh_open_university: module
1126 .enable_registering_completion_to_uh_open_university,
1127 })
1128 }
1129 for module in modified_and_new_modules {
1131 let ModifiedModule {
1133 id,
1134 name,
1135 order_number,
1136 uh_course_code,
1137 ects_credits,
1138 completion_policy,
1139 completion_registration_link_override,
1140 enable_registering_completion_to_uh_open_university,
1141 } = module;
1142 update(
1143 &mut tx,
1144 id,
1145 &NewCourseModule::new(course_id, name.clone(), order_number)
1146 .set_completion_policy(completion_policy)
1147 .set_completion_registration_link_override(completion_registration_link_override)
1148 .set_ects_credits(ects_credits)
1149 .set_uh_course_code(uh_course_code)
1150 .set_enable_registering_completion_to_uh_open_university(
1151 enable_registering_completion_to_uh_open_university,
1152 ),
1153 )
1154 .await?;
1155 }
1156 for (chapter, module) in updates.moved_chapters {
1157 chapters::set_module(&mut tx, chapter, module).await?;
1158 }
1159 for deleted in updates.deleted_modules {
1160 delete(&mut tx, deleted).await?;
1161 }
1162
1163 tx.commit().await?;
1164 Ok(())
1165}
1166
1167pub async fn update_certification_enabled(
1168 conn: &mut PgConnection,
1169 id: Uuid,
1170 enabled: bool,
1171) -> ModelResult<()> {
1172 sqlx::query!(
1173 "
1174UPDATE course_modules
1175SET certification_enabled = $1
1176WHERE id = $2
1177",
1178 enabled,
1179 id
1180 )
1181 .execute(conn)
1182 .await?;
1183 Ok(())
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188
1189 mod automatic_completion_requirements {
1190 use uuid::Uuid;
1191
1192 use super::super::AutomaticCompletionRequirements;
1193
1194 #[test]
1195 fn passes_exercise_tresholds() {
1196 let requirements1 = AutomaticCompletionRequirements {
1197 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
1198 number_of_exercises_attempted_treshold: Some(10),
1199 number_of_points_treshold: Some(50),
1200 requires_exam: false,
1201 };
1202 let requirements2 = AutomaticCompletionRequirements {
1203 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
1204 number_of_exercises_attempted_treshold: Some(50),
1205 number_of_points_treshold: Some(10),
1206 requires_exam: false,
1207 };
1208
1209 let requirements3 = AutomaticCompletionRequirements {
1210 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
1211 number_of_exercises_attempted_treshold: Some(0),
1212 number_of_points_treshold: Some(0),
1213 requires_exam: false,
1214 };
1215
1216 let requirements4 = AutomaticCompletionRequirements {
1217 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
1218 number_of_exercises_attempted_treshold: Some(10),
1219 number_of_points_treshold: None,
1220 requires_exam: false,
1221 };
1222
1223 let requirements5 = AutomaticCompletionRequirements {
1224 course_module_id: Uuid::parse_str("66d98fc6-784a-4b39-a494-24ae9b1c9b14").unwrap(),
1225 number_of_exercises_attempted_treshold: None,
1226 number_of_points_treshold: Some(10),
1227 requires_exam: false,
1228 };
1229 assert!(requirements1.passes_exercise_tresholds(10, 50));
1230 assert!(requirements2.passes_exercise_tresholds(50, 10));
1231
1232 assert!(!requirements1.passes_exercise_tresholds(50, 10));
1233 assert!(!requirements2.passes_exercise_tresholds(10, 50));
1234
1235 assert!(!requirements1.passes_exercise_tresholds(100, 0));
1236 assert!(!requirements2.passes_exercise_tresholds(100, 0));
1237
1238 assert!(requirements3.passes_exercise_tresholds(1, 1));
1239 assert!(requirements3.passes_exercise_tresholds(0, 0));
1240
1241 assert!(requirements4.passes_exercise_tresholds(10, 1));
1242 assert!(!requirements4.passes_exercise_tresholds(1, 10));
1243
1244 assert!(requirements5.passes_exercise_tresholds(0, 10));
1245 assert!(!requirements5.passes_exercise_tresholds(10, 0));
1246 }
1247 }
1248}