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_instance_id: Uuid,
16 pub course_module_id: Uuid,
17 pub user_id: Uuid,
18 pub completion_date: DateTime<Utc>,
19 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
20 pub completion_language: String,
21 pub eligible_for_ects: bool,
22 pub email: String,
23 pub grade: Option<i32>,
24 pub passed: bool,
25 pub prerequisite_modules_completed: bool,
26 pub completion_granter_user_id: Option<Uuid>,
27 pub needs_to_be_reviewed: bool,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
31#[cfg_attr(feature = "ts_rs", derive(TS))]
32pub struct CourseModuleAverage {
33 pub id: Uuid,
34 pub course_instance_id: Uuid,
35 pub created_at: DateTime<Utc>,
36 pub updated_at: DateTime<Utc>,
37 pub deleted_at: Option<DateTime<Utc>>,
38 pub average_duration: Option<u64>,
39 pub average_points: i32,
40 pub total_points: i32,
41 pub total_student: i32,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
46#[cfg_attr(feature = "ts_rs", derive(TS))]
47pub struct CourseModulePointsAverage {
48 pub course_instance_id: Uuid,
49 pub average_points: Option<f32>,
50 pub total_points: Option<i32>,
51 pub total_student: Option<i32>,
52}
53
54#[derive(Clone, PartialEq, Deserialize, Serialize)]
55pub enum CourseModuleCompletionGranter {
56 Automatic,
57 User(Uuid),
58}
59
60impl CourseModuleCompletionGranter {
61 fn to_database_field(&self) -> Option<Uuid> {
62 match self {
63 CourseModuleCompletionGranter::Automatic => None,
64 CourseModuleCompletionGranter::User(user_id) => Some(*user_id),
65 }
66 }
67}
68
69#[derive(Clone, PartialEq, Deserialize, Serialize)]
70#[cfg_attr(feature = "ts_rs", derive(TS))]
71pub struct NewCourseModuleCompletion {
72 pub course_id: Uuid,
73 pub course_instance_id: Uuid,
74 pub course_module_id: Uuid,
75 pub user_id: Uuid,
76 pub completion_date: DateTime<Utc>,
77 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
78 pub completion_language: String,
79 pub eligible_for_ects: bool,
80 pub email: String,
81 pub grade: Option<i32>,
82 pub passed: bool,
83}
84
85pub async fn insert(
86 conn: &mut PgConnection,
87 pkey_policy: PKeyPolicy<Uuid>,
88 new_course_module_completion: &NewCourseModuleCompletion,
89 completion_granter: CourseModuleCompletionGranter,
90) -> ModelResult<CourseModuleCompletion> {
91 let res = sqlx::query_as!(
92 CourseModuleCompletion,
93 "
94INSERT INTO course_module_completions (
95 id,
96 course_id,
97 course_instance_id,
98 course_module_id,
99 user_id,
100 completion_date,
101 completion_registration_attempt_date,
102 completion_language,
103 eligible_for_ects,
104 email,
105 grade,
106 passed,
107 completion_granter_user_id
108 )
109VALUES (
110 $1,
111 $2,
112 $3,
113 $4,
114 $5,
115 $6,
116 $7,
117 $8,
118 $9,
119 $10,
120 $11,
121 $12,
122 $13
123 )
124RETURNING *
125 ",
126 pkey_policy.into_uuid(),
127 new_course_module_completion.course_id,
128 new_course_module_completion.course_instance_id,
129 new_course_module_completion.course_module_id,
130 new_course_module_completion.user_id,
131 new_course_module_completion.completion_date,
132 new_course_module_completion.completion_registration_attempt_date,
133 new_course_module_completion.completion_language,
134 new_course_module_completion.eligible_for_ects,
135 new_course_module_completion.email,
136 new_course_module_completion.grade,
137 new_course_module_completion.passed,
138 completion_granter.to_database_field(),
139 )
140 .fetch_one(conn)
141 .await?;
142 Ok(res)
143}
144
145pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CourseModuleCompletion> {
146 let res = sqlx::query_as!(
147 CourseModuleCompletion,
148 r#"
149SELECT *
150FROM course_module_completions
151WHERE id = $1
152 AND deleted_at IS NULL
153 "#,
154 id,
155 )
156 .fetch_one(conn)
157 .await?;
158 Ok(res)
159}
160
161pub async fn get_by_ids(
163 conn: &mut PgConnection,
164 ids: &[Uuid],
165) -> ModelResult<Vec<CourseModuleCompletion>> {
166 let res = sqlx::query_as!(
167 CourseModuleCompletion,
168 "
169SELECT *
170FROM course_module_completions
171WHERE id = ANY($1)
172 ",
173 ids,
174 )
175 .fetch_all(conn)
176 .await?;
177 Ok(res)
178}
179
180pub async fn get_by_ids_as_map(
181 conn: &mut PgConnection,
182 ids: &[Uuid],
183) -> ModelResult<HashMap<Uuid, CourseModuleCompletion>> {
184 let res = get_by_ids(conn, ids)
185 .await?
186 .into_iter()
187 .map(|x| (x.id, x))
188 .collect();
189 Ok(res)
190}
191
192pub async fn get_all_by_course_instance_id(
193 conn: &mut PgConnection,
194 course_instance_id: Uuid,
195) -> ModelResult<Vec<CourseModuleCompletion>> {
196 let res = sqlx::query_as!(
197 CourseModuleCompletion,
198 "
199SELECT *
200FROM course_module_completions
201WHERE course_instance_id = $1
202 AND deleted_at IS NULL
203 ",
204 course_instance_id,
205 )
206 .fetch_all(conn)
207 .await?;
208 Ok(res)
209}
210
211#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
212#[cfg_attr(feature = "ts_rs", derive(TS))]
213pub struct CourseModuleCompletionWithRegistrationInfo {
214 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
216 pub course_module_id: Uuid,
218 pub created_at: DateTime<Utc>,
220 pub grade: Option<i32>,
222 pub passed: bool,
224 pub prerequisite_modules_completed: bool,
226 pub registered: bool,
228 pub user_id: Uuid,
230 pub completion_date: DateTime<Utc>,
232}
233
234pub async fn get_all_with_registration_information_by_course_instance_id(
236 conn: &mut PgConnection,
237 course_instance_id: Uuid,
238) -> ModelResult<Vec<CourseModuleCompletionWithRegistrationInfo>> {
239 let res = sqlx::query_as!(
240 CourseModuleCompletionWithRegistrationInfo,
241 r#"
242SELECT completions.completion_registration_attempt_date,
243 completions.course_module_id,
244 completions.created_at,
245 completions.grade,
246 completions.passed,
247 completions.prerequisite_modules_completed,
248 (registered.id IS NOT NULL) AS "registered!",
249 completions.user_id,
250 completions.completion_date
251FROM course_module_completions completions
252 LEFT JOIN course_module_completion_registered_to_study_registries registered ON (
253 completions.id = registered.course_module_completion_id
254 )
255WHERE completions.course_instance_id = $1
256 AND completions.deleted_at IS NULL
257 AND registered.deleted_at IS NULL
258 "#,
259 course_instance_id,
260 )
261 .fetch_all(conn)
262 .await?;
263 Ok(res)
264}
265
266pub async fn get_all_by_course_instance_and_user_id(
269 conn: &mut PgConnection,
270 course_instance_id: Uuid,
271 user_id: Uuid,
272) -> ModelResult<Vec<CourseModuleCompletion>> {
273 let res = sqlx::query_as!(
274 CourseModuleCompletion,
275 "
276SELECT *
277FROM course_module_completions
278WHERE course_instance_id = $1
279 AND user_id = $2
280 AND deleted_at IS NULL
281 ",
282 course_instance_id,
283 user_id,
284 )
285 .fetch_all(conn)
286 .await?;
287 Ok(res)
288}
289
290pub async fn get_all_by_user_id(
291 conn: &mut PgConnection,
292 user_id: Uuid,
293) -> ModelResult<Vec<CourseModuleCompletion>> {
294 let res = sqlx::query_as!(
295 CourseModuleCompletion,
296 "
297SELECT *
298FROM course_module_completions
299WHERE user_id = $1
300 AND deleted_at IS NULL
301 ",
302 user_id,
303 )
304 .fetch_all(conn)
305 .await?;
306 Ok(res)
307}
308
309pub async fn get_all_by_user_id_and_course_module_id(
310 conn: &mut PgConnection,
311 user_id: Uuid,
312 course_module_id: Uuid,
313) -> ModelResult<Vec<CourseModuleCompletion>> {
314 let res = sqlx::query_as!(
315 CourseModuleCompletion,
316 "
317SELECT *
318FROM course_module_completions
319WHERE user_id = $1
320 AND course_module_id = $2
321 AND deleted_at IS NULL
322 ",
323 user_id,
324 course_module_id,
325 )
326 .fetch_all(conn)
327 .await?;
328 Ok(res)
329}
330
331pub async fn get_all_by_course_module_instance_and_user_ids(
332 conn: &mut PgConnection,
333 course_module_id: Uuid,
334 course_instance_id: Uuid,
335 user_id: Uuid,
336) -> ModelResult<Vec<CourseModuleCompletion>> {
337 let res = sqlx::query_as!(
338 CourseModuleCompletion,
339 "
340SELECT *
341FROM course_module_completions
342WHERE course_module_id = $1
343 AND course_instance_id = $2
344 AND user_id = $3
345 AND deleted_at IS NULL
346 ",
347 course_module_id,
348 course_instance_id,
349 user_id,
350 )
351 .fetch_all(conn)
352 .await?;
353 Ok(res)
354}
355
356pub async fn get_latest_by_course_module_instance_and_user_ids(
358 conn: &mut PgConnection,
359 course_module_id: Uuid,
360 course_instance_id: Uuid,
361 user_id: Uuid,
362) -> ModelResult<CourseModuleCompletion> {
363 let res = sqlx::query_as!(
364 CourseModuleCompletion,
365 "
366SELECT *
367FROM course_module_completions
368WHERE course_module_id = $1
369 AND course_instance_id = $2
370 AND user_id = $3
371 AND deleted_at IS NULL
372ORDER BY created_at DESC
373LIMIT 1
374 ",
375 course_module_id,
376 course_instance_id,
377 user_id,
378 )
379 .fetch_one(conn)
380 .await?;
381 Ok(res)
382}
383
384pub async fn get_best_completion_by_user_and_course_module_id(
385 conn: &mut PgConnection,
386 user_id: Uuid,
387 course_module_id: Uuid,
388) -> ModelResult<Option<CourseModuleCompletion>> {
389 let completions = sqlx::query_as!(
390 CourseModuleCompletion,
391 r#"
392SELECT *
393FROM course_module_completions
394WHERE user_id = $1
395 AND course_module_id = $2
396 AND deleted_at IS NULL
397 "#,
398 user_id,
399 course_module_id,
400 )
401 .fetch_all(conn)
402 .await?;
403
404 let best_grade = completions
405 .into_iter()
406 .max_by(|completion_a, completion_b| {
407 let score_a = match completion_a.grade {
408 Some(grade) => grade as f32,
409 None => match completion_a.passed {
410 true => 0.5,
411 false => -1.0,
412 },
413 };
414
415 let score_b = match completion_b.grade {
416 Some(grade) => grade as f32,
417 None => match completion_b.passed {
418 true => 0.5,
419 false => -1.0,
420 },
421 };
422
423 score_a
424 .partial_cmp(&score_b)
425 .unwrap_or(std::cmp::Ordering::Equal)
426 });
427
428 Ok(best_grade)
429}
430
431pub fn select_best_completion(
433 completions: Vec<CourseModuleCompletion>,
434) -> Option<CourseModuleCompletion> {
435 completions.into_iter().max_by(|a, b| {
436 let score_a = match a.grade {
437 Some(grade) => grade as f32,
438 None => {
439 if a.passed {
440 0.5
441 } else {
442 -1.0
443 }
444 }
445 };
446 let score_b = match b.grade {
447 Some(grade) => grade as f32,
448 None => {
449 if b.passed {
450 0.5
451 } else {
452 -1.0
453 }
454 }
455 };
456 score_a
457 .partial_cmp(&score_b)
458 .unwrap_or(std::cmp::Ordering::Equal)
459 })
460}
461
462pub async fn get_count_of_distinct_completors_by_course_id(
464 conn: &mut PgConnection,
465 course_id: Uuid,
466) -> ModelResult<i64> {
467 let res = sqlx::query!(
468 "
469SELECT COUNT(DISTINCT user_id) as count
470FROM course_module_completions
471WHERE course_id = $1
472 AND deleted_at IS NULL
473",
474 course_id,
475 )
476 .fetch_one(conn)
477 .await?;
478 Ok(res.count.unwrap_or(0))
479}
480
481pub async fn get_automatic_completion_by_course_module_instance_and_user_ids(
485 conn: &mut PgConnection,
486 course_module_id: Uuid,
487 course_instance_id: Uuid,
488 user_id: Uuid,
489) -> ModelResult<CourseModuleCompletion> {
490 let res = sqlx::query_as!(
491 CourseModuleCompletion,
492 "
493SELECT *
494FROM course_module_completions
495WHERE course_module_id = $1
496 AND course_instance_id = $2
497 AND user_id = $3
498 AND completion_granter_user_id IS NULL
499 AND deleted_at IS NULL
500 ",
501 course_module_id,
502 course_instance_id,
503 user_id,
504 )
505 .fetch_one(conn)
506 .await?;
507 Ok(res)
508}
509
510pub async fn update_completion_registration_attempt_date(
511 conn: &mut PgConnection,
512 id: Uuid,
513 completion_registration_attempt_date: DateTime<Utc>,
514) -> ModelResult<bool> {
515 let res = sqlx::query!(
516 "
517UPDATE course_module_completions
518SET completion_registration_attempt_date = $1
519WHERE id = $2
520 AND deleted_at IS NULL
521 ",
522 Some(completion_registration_attempt_date),
523 id,
524 )
525 .execute(conn)
526 .await?;
527 Ok(res.rows_affected() > 0)
528}
529
530pub async fn update_prerequisite_modules_completed(
531 conn: &mut PgConnection,
532 id: Uuid,
533 prerequisite_modules_completed: bool,
534) -> ModelResult<bool> {
535 let res = sqlx::query!(
536 "
537UPDATE course_module_completions SET prerequisite_modules_completed = $1
538WHERE id = $2 AND deleted_at IS NULL
539 ",
540 prerequisite_modules_completed,
541 id
542 )
543 .execute(conn)
544 .await?;
545 Ok(res.rows_affected() > 0)
546}
547
548pub async fn update_passed_and_grade_status(
549 conn: &mut PgConnection,
550 course_id: Uuid,
551 user_id: Uuid,
552 passed: bool,
553 grade: i32,
554) -> ModelResult<bool> {
555 let res = sqlx::query!(
556 "
557UPDATE course_module_completions SET passed = $1, grade = $2
558WHERE user_id = $3 AND course_id = $4 AND deleted_at IS NULL
559 ",
560 passed,
561 grade,
562 user_id,
563 course_id
564 )
565 .execute(conn)
566 .await?;
567 Ok(res.rows_affected() > 0)
568}
569
570pub async fn update_needs_to_be_reviewed(
571 conn: &mut PgConnection,
572 id: Uuid,
573 needs_to_be_reviewed: bool,
574) -> ModelResult<bool> {
575 let res = sqlx::query!(
576 "
577UPDATE course_module_completions SET needs_to_be_reviewed = $1
578WHERE id = $2 AND deleted_at IS NULL
579 ",
580 needs_to_be_reviewed,
581 id
582 )
583 .execute(conn)
584 .await?;
585 Ok(res.rows_affected() > 0)
586}
587
588pub async fn user_has_completed_course_module_on_instance(
591 conn: &mut PgConnection,
592 user_id: Uuid,
593 course_module_id: Uuid,
594 course_instance_id: Uuid,
595) -> ModelResult<bool> {
596 let res = get_all_by_course_module_instance_and_user_ids(
597 conn,
598 course_module_id,
599 course_instance_id,
600 user_id,
601 )
602 .await?;
603 Ok(!res.is_empty())
604}
605
606#[derive(Clone, PartialEq, Deserialize, Serialize)]
608#[cfg_attr(feature = "ts_rs", derive(TS))]
609pub struct StudyRegistryCompletion {
610 pub completion_date: DateTime<Utc>,
616 pub completion_language: String,
618 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
620 pub email: String,
624 pub grade: StudyRegistryGrade,
626 pub id: Uuid,
628 pub user_id: Uuid,
630 pub tier: Option<i32>,
633}
634
635impl From<CourseModuleCompletion> for StudyRegistryCompletion {
636 fn from(completion: CourseModuleCompletion) -> Self {
637 Self {
638 completion_date: completion.completion_date,
639 completion_language: completion.completion_language,
640 completion_registration_attempt_date: completion.completion_registration_attempt_date,
641 email: completion.email,
642 grade: StudyRegistryGrade::new(completion.passed, completion.grade),
643 id: completion.id,
644 user_id: completion.user_id,
645 tier: None,
646 }
647 }
648}
649
650#[derive(Clone, PartialEq, Deserialize, Serialize)]
689#[cfg_attr(feature = "ts_rs", derive(TS))]
690pub struct StudyRegistryGrade {
691 pub scale: String,
692 pub grade: String,
693}
694
695impl StudyRegistryGrade {
696 pub fn new(passed: bool, grade: Option<i32>) -> Self {
697 match grade {
698 Some(grade) => Self {
699 scale: "sis-0-5".to_string(),
700 grade: grade.to_string(),
701 },
702 None => Self {
703 scale: "sis-hyv-hyl".to_string(),
704 grade: if passed {
705 "1".to_string()
706 } else {
707 "0".to_string()
708 },
709 },
710 }
711 }
712}
713pub fn stream_by_course_module_id<'a>(
717 conn: &'a mut PgConnection,
718 course_module_ids: &'a [Uuid],
719 no_completions_registered_by_this_study_registry_registrar: &'a Option<StudyRegistryRegistrar>,
720) -> impl Stream<Item = sqlx::Result<StudyRegistryCompletion>> + Send + 'a {
721 let study_module_registrar_id = no_completions_registered_by_this_study_registry_registrar
723 .clone()
724 .map(|o| o.id)
725 .unwrap_or(Uuid::nil());
726
727 sqlx::query_as!(
728 CourseModuleCompletion,
729 r#"
730SELECT *
731FROM course_module_completions
732WHERE course_module_id = ANY($1)
733 AND prerequisite_modules_completed
734 AND eligible_for_ects IS TRUE
735 AND deleted_at IS NULL
736 AND id NOT IN (
737 SELECT course_module_completion_id
738 FROM course_module_completion_registered_to_study_registries
739 WHERE course_module_id = ANY($1)
740 AND study_registry_registrar_id = $2
741 AND deleted_at IS NULL
742 )
743 "#,
744 course_module_ids,
745 study_module_registrar_id,
746 )
747 .map(StudyRegistryCompletion::from)
748 .fetch(conn)
749}
750
751pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
752 sqlx::query!(
753 "
754
755UPDATE course_module_completions
756SET deleted_at = now()
757WHERE id = $1
758 ",
759 id,
760 )
761 .execute(conn)
762 .await?;
763 Ok(())
764}