1use std::collections::HashMap;
2
3use futures::Stream;
4
5use crate::{prelude::*, study_registry_registrars::StudyRegistryRegistrar};
6
7#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
8#[cfg_attr(feature = "ts_rs", derive(TS))]
9pub struct CourseModuleCompletion {
10 pub id: Uuid,
11 pub created_at: DateTime<Utc>,
12 pub updated_at: DateTime<Utc>,
13 pub deleted_at: Option<DateTime<Utc>>,
14 pub course_id: Uuid,
15 pub course_module_id: Uuid,
16 pub user_id: Uuid,
17 pub completion_date: DateTime<Utc>,
18 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
19 pub completion_language: String,
20 pub eligible_for_ects: bool,
21 pub email: String,
22 pub grade: Option<i32>,
23 pub passed: bool,
24 pub prerequisite_modules_completed: bool,
25 pub completion_granter_user_id: Option<Uuid>,
26 pub needs_to_be_reviewed: bool,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
30#[cfg_attr(feature = "ts_rs", derive(TS))]
31pub struct CourseModuleAverage {
32 pub id: Uuid,
33 pub course_id: Uuid,
34 pub created_at: DateTime<Utc>,
35 pub updated_at: DateTime<Utc>,
36 pub deleted_at: Option<DateTime<Utc>>,
37 pub average_duration: Option<u64>,
38 pub average_points: i32,
39 pub total_points: i32,
40 pub total_student: i32,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
45#[cfg_attr(feature = "ts_rs", derive(TS))]
46pub struct CourseModulePointsAverage {
47 pub course_id: Uuid,
48 pub average_points: Option<f32>,
49 pub total_points: Option<i32>,
50 pub total_student: Option<i32>,
51}
52
53#[derive(Clone, PartialEq, Deserialize, Serialize)]
54pub enum CourseModuleCompletionGranter {
55 Automatic,
56 User(Uuid),
57}
58
59impl CourseModuleCompletionGranter {
60 fn to_database_field(&self) -> Option<Uuid> {
61 match self {
62 CourseModuleCompletionGranter::Automatic => None,
63 CourseModuleCompletionGranter::User(user_id) => Some(*user_id),
64 }
65 }
66}
67
68#[derive(Clone, PartialEq, Deserialize, Serialize)]
69#[cfg_attr(feature = "ts_rs", derive(TS))]
70pub struct NewCourseModuleCompletion {
71 pub course_id: Uuid,
72 pub course_module_id: Uuid,
73 pub user_id: Uuid,
74 pub completion_date: DateTime<Utc>,
75 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
76 pub completion_language: String,
77 pub eligible_for_ects: bool,
78 pub email: String,
79 pub grade: Option<i32>,
80 pub passed: bool,
81}
82
83pub async fn insert(
84 conn: &mut PgConnection,
85 pkey_policy: PKeyPolicy<Uuid>,
86 new_course_module_completion: &NewCourseModuleCompletion,
87 completion_granter: CourseModuleCompletionGranter,
88) -> ModelResult<CourseModuleCompletion> {
89 let res = sqlx::query_as!(
90 CourseModuleCompletion,
91 "
92INSERT INTO course_module_completions (
93 id,
94 course_id,
95 course_module_id,
96 user_id,
97 completion_date,
98 completion_registration_attempt_date,
99 completion_language,
100 eligible_for_ects,
101 email,
102 grade,
103 passed,
104 completion_granter_user_id
105 )
106VALUES (
107 $1,
108 $2,
109 $3,
110 $4,
111 $5,
112 $6,
113 $7,
114 $8,
115 $9,
116 $10,
117 $11,
118 $12
119 )
120RETURNING *
121 ",
122 pkey_policy.into_uuid(),
123 new_course_module_completion.course_id,
124 new_course_module_completion.course_module_id,
125 new_course_module_completion.user_id,
126 new_course_module_completion.completion_date,
127 new_course_module_completion.completion_registration_attempt_date,
128 new_course_module_completion.completion_language,
129 new_course_module_completion.eligible_for_ects,
130 new_course_module_completion.email,
131 new_course_module_completion.grade,
132 new_course_module_completion.passed,
133 completion_granter.to_database_field(),
134 )
135 .fetch_one(conn)
136 .await?;
137 Ok(res)
138}
139
140#[derive(Debug, Clone)]
141pub struct NewCourseModuleCompletionSeed {
142 pub course_id: Uuid,
143 pub course_module_id: Uuid,
144 pub user_id: Uuid,
145 pub completion_date: Option<DateTime<Utc>>,
146 pub completion_language: Option<String>,
147 pub eligible_for_ects: Option<bool>,
148 pub email: Option<String>,
149 pub grade: Option<i32>,
150 pub passed: Option<bool>,
151 pub prerequisite_modules_completed: Option<bool>,
152 pub needs_to_be_reviewed: Option<bool>,
153}
154
155pub async fn insert_seed_row(
156 conn: &mut PgConnection,
157 seed: &NewCourseModuleCompletionSeed,
158) -> ModelResult<Uuid> {
159 let res = sqlx::query!(
160 r#"
161 INSERT INTO course_module_completions (
162 course_id,
163 course_module_id,
164 user_id,
165 completion_date,
166 completion_language,
167 eligible_for_ects,
168 email,
169 grade,
170 passed,
171 prerequisite_modules_completed,
172 needs_to_be_reviewed
173 )
174 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
175 RETURNING id
176 "#,
177 seed.course_id,
178 seed.course_module_id,
179 seed.user_id,
180 seed.completion_date,
181 seed.completion_language.as_deref(),
182 seed.eligible_for_ects,
183 seed.email.as_deref(),
184 seed.grade,
185 seed.passed,
186 seed.prerequisite_modules_completed,
187 seed.needs_to_be_reviewed,
188 )
189 .fetch_one(conn)
190 .await?;
191
192 Ok(res.id)
193}
194
195pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CourseModuleCompletion> {
196 let res = sqlx::query_as!(
197 CourseModuleCompletion,
198 r#"
199SELECT *
200FROM course_module_completions
201WHERE id = $1
202 AND deleted_at IS NULL
203 "#,
204 id,
205 )
206 .fetch_one(conn)
207 .await?;
208 Ok(res)
209}
210
211pub async fn get_by_ids(
213 conn: &mut PgConnection,
214 ids: &[Uuid],
215) -> ModelResult<Vec<CourseModuleCompletion>> {
216 let res = sqlx::query_as!(
217 CourseModuleCompletion,
218 "
219SELECT *
220FROM course_module_completions
221WHERE id = ANY($1)
222 ",
223 ids,
224 )
225 .fetch_all(conn)
226 .await?;
227 Ok(res)
228}
229
230pub async fn get_by_ids_as_map(
231 conn: &mut PgConnection,
232 ids: &[Uuid],
233) -> ModelResult<HashMap<Uuid, CourseModuleCompletion>> {
234 let res = get_by_ids(conn, ids)
235 .await?
236 .into_iter()
237 .map(|x| (x.id, x))
238 .collect();
239 Ok(res)
240}
241
242#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
243#[cfg_attr(feature = "ts_rs", derive(TS))]
244pub struct CourseModuleCompletionWithRegistrationInfo {
245 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
247 pub course_module_id: Uuid,
249 pub created_at: DateTime<Utc>,
251 pub grade: Option<i32>,
253 pub passed: bool,
255 pub prerequisite_modules_completed: bool,
257 pub registered: bool,
259 pub user_id: Uuid,
261 pub completion_date: DateTime<Utc>,
263}
264
265pub async fn get_all_with_registration_information_by_course_instance_id(
267 conn: &mut PgConnection,
268 course_instance_id: Uuid,
269 course_id: Uuid,
270) -> ModelResult<Vec<CourseModuleCompletionWithRegistrationInfo>> {
271 let res = sqlx::query_as!(
272 CourseModuleCompletionWithRegistrationInfo,
273 r#"
274SELECT completions.completion_registration_attempt_date,
275 completions.course_module_id,
276 completions.created_at,
277 completions.grade,
278 completions.passed,
279 completions.prerequisite_modules_completed,
280 (registered.id IS NOT NULL) AS "registered!",
281 completions.user_id,
282 completions.completion_date
283FROM course_module_completions completions
284 LEFT JOIN course_module_completion_registered_to_study_registries registered ON (
285 completions.id = registered.course_module_completion_id
286 )
287 JOIN user_course_settings settings ON (
288 completions.user_id = settings.user_id
289 AND settings.current_course_id = completions.course_id
290 )
291WHERE settings.current_course_instance_id = $1
292 AND completions.deleted_at IS NULL
293 AND registered.deleted_at IS NULL
294 AND settings.deleted_at IS NULL
295 AND settings.current_course_id = $2
296 "#,
297 course_instance_id,
298 course_id
299 )
300 .fetch_all(conn)
301 .await?;
302 Ok(res)
303}
304
305pub async fn get_all_by_course_id_and_user_id(
308 conn: &mut PgConnection,
309 course_id: Uuid,
310 user_id: Uuid,
311) -> ModelResult<Vec<CourseModuleCompletion>> {
312 let res = sqlx::query_as!(
313 CourseModuleCompletion,
314 "
315SELECT *
316FROM course_module_completions
317WHERE course_id = $1
318 AND user_id = $2
319 AND deleted_at IS NULL
320 ",
321 course_id,
322 user_id,
323 )
324 .fetch_all(conn)
325 .await?;
326 Ok(res)
327}
328
329pub async fn get_all_by_user_id(
330 conn: &mut PgConnection,
331 user_id: Uuid,
332) -> ModelResult<Vec<CourseModuleCompletion>> {
333 let res = sqlx::query_as!(
334 CourseModuleCompletion,
335 "
336SELECT *
337FROM course_module_completions
338WHERE user_id = $1
339 AND deleted_at IS NULL
340 ",
341 user_id,
342 )
343 .fetch_all(conn)
344 .await?;
345 Ok(res)
346}
347
348pub async fn get_all_by_user_id_and_course_module_id(
349 conn: &mut PgConnection,
350 user_id: Uuid,
351 course_module_id: Uuid,
352) -> ModelResult<Vec<CourseModuleCompletion>> {
353 let res = sqlx::query_as!(
354 CourseModuleCompletion,
355 "
356SELECT *
357FROM course_module_completions
358WHERE user_id = $1
359 AND course_module_id = $2
360 AND deleted_at IS NULL
361 ",
362 user_id,
363 course_module_id,
364 )
365 .fetch_all(conn)
366 .await?;
367 Ok(res)
368}
369
370pub async fn get_all_by_course_module_and_user_ids(
371 conn: &mut PgConnection,
372 course_module_id: Uuid,
373 user_id: Uuid,
374) -> ModelResult<Vec<CourseModuleCompletion>> {
375 let res = sqlx::query_as!(
376 CourseModuleCompletion,
377 "
378SELECT *
379FROM course_module_completions
380WHERE course_module_id = $1
381 AND user_id = $2
382 AND deleted_at IS NULL
383 ",
384 course_module_id,
385 user_id,
386 )
387 .fetch_all(conn)
388 .await?;
389 Ok(res)
390}
391
392pub async fn get_latest_by_course_and_user_ids(
394 conn: &mut PgConnection,
395 course_module_id: Uuid,
396 user_id: Uuid,
397) -> ModelResult<CourseModuleCompletion> {
398 let res = sqlx::query_as!(
399 CourseModuleCompletion,
400 "
401SELECT *
402FROM course_module_completions
403WHERE course_module_id = $1
404 AND user_id = $2
405 AND deleted_at IS NULL
406ORDER BY created_at DESC
407LIMIT 1
408 ",
409 course_module_id,
410 user_id,
411 )
412 .fetch_one(conn)
413 .await?;
414 Ok(res)
415}
416
417pub async fn get_best_completion_by_user_and_course_module_id(
418 conn: &mut PgConnection,
419 user_id: Uuid,
420 course_module_id: Uuid,
421) -> ModelResult<Option<CourseModuleCompletion>> {
422 let completions = sqlx::query_as!(
423 CourseModuleCompletion,
424 r#"
425SELECT *
426FROM course_module_completions
427WHERE user_id = $1
428 AND course_module_id = $2
429 AND deleted_at IS NULL
430 "#,
431 user_id,
432 course_module_id,
433 )
434 .fetch_all(conn)
435 .await?;
436
437 let best_grade = completions
438 .into_iter()
439 .max_by(|completion_a, completion_b| {
440 let score_a = match completion_a.grade {
441 Some(grade) => grade as f32,
442 None => match completion_a.passed {
443 true => 0.5,
444 false => -1.0,
445 },
446 };
447
448 let score_b = match completion_b.grade {
449 Some(grade) => grade as f32,
450 None => match completion_b.passed {
451 true => 0.5,
452 false => -1.0,
453 },
454 };
455
456 score_a
457 .partial_cmp(&score_b)
458 .unwrap_or(std::cmp::Ordering::Equal)
459 });
460
461 Ok(best_grade)
462}
463
464pub fn select_best_completion(
466 completions: Vec<CourseModuleCompletion>,
467) -> Option<CourseModuleCompletion> {
468 completions.into_iter().max_by(|a, b| {
469 let score_a = match a.grade {
470 Some(grade) => grade as f32,
471 None => {
472 if a.passed {
473 0.5
474 } else {
475 -1.0
476 }
477 }
478 };
479 let score_b = match b.grade {
480 Some(grade) => grade as f32,
481 None => {
482 if b.passed {
483 0.5
484 } else {
485 -1.0
486 }
487 }
488 };
489 score_a
490 .partial_cmp(&score_b)
491 .unwrap_or(std::cmp::Ordering::Equal)
492 })
493}
494
495pub async fn get_count_of_distinct_completors_by_course_id(
497 conn: &mut PgConnection,
498 course_id: Uuid,
499) -> ModelResult<i64> {
500 let res = sqlx::query!(
501 "
502SELECT COUNT(DISTINCT user_id) as count
503FROM course_module_completions
504WHERE course_id = $1
505 AND deleted_at IS NULL
506",
507 course_id,
508 )
509 .fetch_one(conn)
510 .await?;
511 Ok(res.count.unwrap_or(0))
512}
513
514pub async fn get_automatic_completion_by_course_module_course_and_user_ids(
518 conn: &mut PgConnection,
519 course_module_id: Uuid,
520 course_id: Uuid,
521 user_id: Uuid,
522) -> ModelResult<CourseModuleCompletion> {
523 let res = sqlx::query_as!(
524 CourseModuleCompletion,
525 "
526SELECT *
527FROM course_module_completions
528WHERE course_module_id = $1
529 AND course_id = $2
530 AND user_id = $3
531 AND completion_granter_user_id IS NULL
532 AND deleted_at IS NULL
533 ",
534 course_module_id,
535 course_id,
536 user_id,
537 )
538 .fetch_one(conn)
539 .await?;
540 Ok(res)
541}
542
543pub async fn update_completion_registration_attempt_date(
544 conn: &mut PgConnection,
545 id: Uuid,
546 completion_registration_attempt_date: DateTime<Utc>,
547) -> ModelResult<bool> {
548 let res = sqlx::query!(
549 "
550UPDATE course_module_completions
551SET completion_registration_attempt_date = $1
552WHERE id = $2
553 AND deleted_at IS NULL
554 ",
555 Some(completion_registration_attempt_date),
556 id,
557 )
558 .execute(conn)
559 .await?;
560 Ok(res.rows_affected() > 0)
561}
562
563pub async fn update_prerequisite_modules_completed(
564 conn: &mut PgConnection,
565 id: Uuid,
566 prerequisite_modules_completed: bool,
567) -> ModelResult<bool> {
568 let res = sqlx::query!(
569 "
570UPDATE course_module_completions SET prerequisite_modules_completed = $1
571WHERE id = $2 AND deleted_at IS NULL
572 ",
573 prerequisite_modules_completed,
574 id
575 )
576 .execute(conn)
577 .await?;
578 Ok(res.rows_affected() > 0)
579}
580
581pub async fn update_passed_and_grade_status(
582 conn: &mut PgConnection,
583 course_id: Uuid,
584 user_id: Uuid,
585 passed: bool,
586 grade: i32,
587) -> ModelResult<bool> {
588 let res = sqlx::query!(
589 "
590UPDATE course_module_completions SET passed = $1, grade = $2
591WHERE user_id = $3 AND course_id = $4 AND deleted_at IS NULL
592 ",
593 passed,
594 grade,
595 user_id,
596 course_id
597 )
598 .execute(conn)
599 .await?;
600 Ok(res.rows_affected() > 0)
601}
602
603pub async fn update_needs_to_be_reviewed(
604 conn: &mut PgConnection,
605 id: Uuid,
606 needs_to_be_reviewed: bool,
607) -> ModelResult<bool> {
608 let res = sqlx::query!(
609 "
610UPDATE course_module_completions SET needs_to_be_reviewed = $1
611WHERE id = $2 AND deleted_at IS NULL
612 ",
613 needs_to_be_reviewed,
614 id
615 )
616 .execute(conn)
617 .await?;
618 Ok(res.rows_affected() > 0)
619}
620
621pub async fn user_has_completed_course_module(
624 conn: &mut PgConnection,
625 user_id: Uuid,
626 course_module_id: Uuid,
627) -> ModelResult<bool> {
628 let res = get_all_by_course_module_and_user_ids(conn, course_module_id, user_id).await?;
629 Ok(!res.is_empty())
630}
631
632#[derive(Clone, PartialEq, Deserialize, Serialize)]
634#[cfg_attr(feature = "ts_rs", derive(TS))]
635pub struct StudyRegistryCompletion {
636 pub completion_date: DateTime<Utc>,
642 pub completion_language: String,
644 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
646 pub email: String,
650 pub grade: StudyRegistryGrade,
652 pub id: Uuid,
654 pub user_id: Uuid,
656 pub tier: Option<i32>,
659}
660
661impl From<CourseModuleCompletion> for StudyRegistryCompletion {
662 fn from(completion: CourseModuleCompletion) -> Self {
663 Self {
664 completion_date: completion.completion_date,
665 completion_language: completion.completion_language,
666 completion_registration_attempt_date: completion.completion_registration_attempt_date,
667 email: completion.email,
668 grade: StudyRegistryGrade::new(completion.passed, completion.grade),
669 id: completion.id,
670 user_id: completion.user_id,
671 tier: None,
672 }
673 }
674}
675
676#[derive(Clone, PartialEq, Deserialize, Serialize)]
715#[cfg_attr(feature = "ts_rs", derive(TS))]
716pub struct StudyRegistryGrade {
717 pub scale: String,
718 pub grade: String,
719}
720
721impl StudyRegistryGrade {
722 pub fn new(passed: bool, grade: Option<i32>) -> Self {
723 match grade {
724 Some(grade) => Self {
725 scale: "sis-0-5".to_string(),
726 grade: grade.to_string(),
727 },
728 None => Self {
729 scale: "sis-hyv-hyl".to_string(),
730 grade: if passed {
731 "1".to_string()
732 } else {
733 "0".to_string()
734 },
735 },
736 }
737 }
738}
739pub fn stream_by_course_module_id<'a>(
743 conn: &'a mut PgConnection,
744 course_module_ids: &'a [Uuid],
745 no_completions_registered_by_this_study_registry_registrar: &'a Option<StudyRegistryRegistrar>,
746) -> impl Stream<Item = sqlx::Result<StudyRegistryCompletion>> + Send + 'a {
747 let study_module_registrar_id = no_completions_registered_by_this_study_registry_registrar
749 .clone()
750 .map(|o| o.id)
751 .unwrap_or(Uuid::nil());
752
753 sqlx::query_as!(
754 CourseModuleCompletion,
755 r#"
756SELECT *
757FROM course_module_completions
758WHERE course_module_id = ANY($1)
759 AND prerequisite_modules_completed
760 AND eligible_for_ects IS TRUE
761 AND deleted_at IS NULL
762 AND id NOT IN (
763 SELECT course_module_completion_id
764 FROM course_module_completion_registered_to_study_registries
765 WHERE course_module_id = ANY($1)
766 AND study_registry_registrar_id = $2
767 AND deleted_at IS NULL
768 )
769 "#,
770 course_module_ids,
771 study_module_registrar_id,
772 )
773 .map(StudyRegistryCompletion::from)
774 .fetch(conn)
775}
776
777pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
778 sqlx::query!(
779 "
780
781UPDATE course_module_completions
782SET deleted_at = now()
783WHERE id = $1
784AND deleted_at IS NULL
785 ",
786 id,
787 )
788 .execute(conn)
789 .await?;
790 Ok(())
791}
792
793pub async fn find_existing(
794 conn: &mut PgConnection,
795 course_id: Uuid,
796 course_module_id: Uuid,
797 user_id: Uuid,
798) -> ModelResult<Uuid> {
799 let row = sqlx::query!(
800 r#"
801 SELECT id
802 FROM course_module_completions
803 WHERE course_id = $1
804 AND course_module_id = $2
805 AND user_id = $3
806 AND completion_granter_user_id IS NULL
807 AND deleted_at IS NULL
808 "#,
809 course_id,
810 course_module_id,
811 user_id,
812 )
813 .fetch_one(conn)
814 .await?;
815
816 Ok(row.id)
817}
818
819pub async fn update_registration_attempt(
820 conn: &mut PgConnection,
821 completion_id: Uuid,
822) -> ModelResult<()> {
823 sqlx::query!(
824 r#"
825 UPDATE course_module_completions
826 SET completion_registration_attempt_date = now()
827 WHERE id = $1
828 "#,
829 completion_id
830 )
831 .execute(conn)
832 .await?;
833
834 Ok(())
835}