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
140pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CourseModuleCompletion> {
141 let res = sqlx::query_as!(
142 CourseModuleCompletion,
143 r#"
144SELECT *
145FROM course_module_completions
146WHERE id = $1
147 AND deleted_at IS NULL
148 "#,
149 id,
150 )
151 .fetch_one(conn)
152 .await?;
153 Ok(res)
154}
155
156pub async fn get_by_ids(
158 conn: &mut PgConnection,
159 ids: &[Uuid],
160) -> ModelResult<Vec<CourseModuleCompletion>> {
161 let res = sqlx::query_as!(
162 CourseModuleCompletion,
163 "
164SELECT *
165FROM course_module_completions
166WHERE id = ANY($1)
167 ",
168 ids,
169 )
170 .fetch_all(conn)
171 .await?;
172 Ok(res)
173}
174
175pub async fn get_by_ids_as_map(
176 conn: &mut PgConnection,
177 ids: &[Uuid],
178) -> ModelResult<HashMap<Uuid, CourseModuleCompletion>> {
179 let res = get_by_ids(conn, ids)
180 .await?
181 .into_iter()
182 .map(|x| (x.id, x))
183 .collect();
184 Ok(res)
185}
186
187#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
188#[cfg_attr(feature = "ts_rs", derive(TS))]
189pub struct CourseModuleCompletionWithRegistrationInfo {
190 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
192 pub course_module_id: Uuid,
194 pub created_at: DateTime<Utc>,
196 pub grade: Option<i32>,
198 pub passed: bool,
200 pub prerequisite_modules_completed: bool,
202 pub registered: bool,
204 pub user_id: Uuid,
206 pub completion_date: DateTime<Utc>,
208}
209
210pub async fn get_all_with_registration_information_by_course_instance_id(
212 conn: &mut PgConnection,
213 course_instance_id: Uuid,
214 course_id: Uuid,
215) -> ModelResult<Vec<CourseModuleCompletionWithRegistrationInfo>> {
216 let res = sqlx::query_as!(
217 CourseModuleCompletionWithRegistrationInfo,
218 r#"
219SELECT completions.completion_registration_attempt_date,
220 completions.course_module_id,
221 completions.created_at,
222 completions.grade,
223 completions.passed,
224 completions.prerequisite_modules_completed,
225 (registered.id IS NOT NULL) AS "registered!",
226 completions.user_id,
227 completions.completion_date
228FROM course_module_completions completions
229 LEFT JOIN course_module_completion_registered_to_study_registries registered ON (
230 completions.id = registered.course_module_completion_id
231 )
232 JOIN user_course_settings settings ON (
233 completions.user_id = settings.user_id
234 AND settings.current_course_id = completions.course_id
235 )
236WHERE settings.current_course_instance_id = $1
237 AND completions.deleted_at IS NULL
238 AND registered.deleted_at IS NULL
239 AND settings.deleted_at IS NULL
240 AND settings.current_course_id = $2
241 "#,
242 course_instance_id,
243 course_id
244 )
245 .fetch_all(conn)
246 .await?;
247 Ok(res)
248}
249
250pub async fn get_all_by_course_id_and_user_id(
253 conn: &mut PgConnection,
254 course_id: Uuid,
255 user_id: Uuid,
256) -> ModelResult<Vec<CourseModuleCompletion>> {
257 let res = sqlx::query_as!(
258 CourseModuleCompletion,
259 "
260SELECT *
261FROM course_module_completions
262WHERE course_id = $1
263 AND user_id = $2
264 AND deleted_at IS NULL
265 ",
266 course_id,
267 user_id,
268 )
269 .fetch_all(conn)
270 .await?;
271 Ok(res)
272}
273
274pub async fn get_all_by_user_id(
275 conn: &mut PgConnection,
276 user_id: Uuid,
277) -> ModelResult<Vec<CourseModuleCompletion>> {
278 let res = sqlx::query_as!(
279 CourseModuleCompletion,
280 "
281SELECT *
282FROM course_module_completions
283WHERE user_id = $1
284 AND deleted_at IS NULL
285 ",
286 user_id,
287 )
288 .fetch_all(conn)
289 .await?;
290 Ok(res)
291}
292
293pub async fn get_all_by_user_id_and_course_module_id(
294 conn: &mut PgConnection,
295 user_id: Uuid,
296 course_module_id: Uuid,
297) -> ModelResult<Vec<CourseModuleCompletion>> {
298 let res = sqlx::query_as!(
299 CourseModuleCompletion,
300 "
301SELECT *
302FROM course_module_completions
303WHERE user_id = $1
304 AND course_module_id = $2
305 AND deleted_at IS NULL
306 ",
307 user_id,
308 course_module_id,
309 )
310 .fetch_all(conn)
311 .await?;
312 Ok(res)
313}
314
315pub async fn get_all_by_course_module_and_user_ids(
316 conn: &mut PgConnection,
317 course_module_id: Uuid,
318 user_id: Uuid,
319) -> ModelResult<Vec<CourseModuleCompletion>> {
320 let res = sqlx::query_as!(
321 CourseModuleCompletion,
322 "
323SELECT *
324FROM course_module_completions
325WHERE course_module_id = $1
326 AND user_id = $2
327 AND deleted_at IS NULL
328 ",
329 course_module_id,
330 user_id,
331 )
332 .fetch_all(conn)
333 .await?;
334 Ok(res)
335}
336
337pub async fn get_latest_by_course_and_user_ids(
339 conn: &mut PgConnection,
340 course_module_id: Uuid,
341 user_id: Uuid,
342) -> ModelResult<CourseModuleCompletion> {
343 let res = sqlx::query_as!(
344 CourseModuleCompletion,
345 "
346SELECT *
347FROM course_module_completions
348WHERE course_module_id = $1
349 AND user_id = $2
350 AND deleted_at IS NULL
351ORDER BY created_at DESC
352LIMIT 1
353 ",
354 course_module_id,
355 user_id,
356 )
357 .fetch_one(conn)
358 .await?;
359 Ok(res)
360}
361
362pub async fn get_best_completion_by_user_and_course_module_id(
363 conn: &mut PgConnection,
364 user_id: Uuid,
365 course_module_id: Uuid,
366) -> ModelResult<Option<CourseModuleCompletion>> {
367 let completions = sqlx::query_as!(
368 CourseModuleCompletion,
369 r#"
370SELECT *
371FROM course_module_completions
372WHERE user_id = $1
373 AND course_module_id = $2
374 AND deleted_at IS NULL
375 "#,
376 user_id,
377 course_module_id,
378 )
379 .fetch_all(conn)
380 .await?;
381
382 let best_grade = completions
383 .into_iter()
384 .max_by(|completion_a, completion_b| {
385 let score_a = match completion_a.grade {
386 Some(grade) => grade as f32,
387 None => match completion_a.passed {
388 true => 0.5,
389 false => -1.0,
390 },
391 };
392
393 let score_b = match completion_b.grade {
394 Some(grade) => grade as f32,
395 None => match completion_b.passed {
396 true => 0.5,
397 false => -1.0,
398 },
399 };
400
401 score_a
402 .partial_cmp(&score_b)
403 .unwrap_or(std::cmp::Ordering::Equal)
404 });
405
406 Ok(best_grade)
407}
408
409pub fn select_best_completion(
411 completions: Vec<CourseModuleCompletion>,
412) -> Option<CourseModuleCompletion> {
413 completions.into_iter().max_by(|a, b| {
414 let score_a = match a.grade {
415 Some(grade) => grade as f32,
416 None => {
417 if a.passed {
418 0.5
419 } else {
420 -1.0
421 }
422 }
423 };
424 let score_b = match b.grade {
425 Some(grade) => grade as f32,
426 None => {
427 if b.passed {
428 0.5
429 } else {
430 -1.0
431 }
432 }
433 };
434 score_a
435 .partial_cmp(&score_b)
436 .unwrap_or(std::cmp::Ordering::Equal)
437 })
438}
439
440pub async fn get_count_of_distinct_completors_by_course_id(
442 conn: &mut PgConnection,
443 course_id: Uuid,
444) -> ModelResult<i64> {
445 let res = sqlx::query!(
446 "
447SELECT COUNT(DISTINCT user_id) as count
448FROM course_module_completions
449WHERE course_id = $1
450 AND deleted_at IS NULL
451",
452 course_id,
453 )
454 .fetch_one(conn)
455 .await?;
456 Ok(res.count.unwrap_or(0))
457}
458
459pub async fn get_automatic_completion_by_course_module_course_and_user_ids(
463 conn: &mut PgConnection,
464 course_module_id: Uuid,
465 course_id: Uuid,
466 user_id: Uuid,
467) -> ModelResult<CourseModuleCompletion> {
468 let res = sqlx::query_as!(
469 CourseModuleCompletion,
470 "
471SELECT *
472FROM course_module_completions
473WHERE course_module_id = $1
474 AND course_id = $2
475 AND user_id = $3
476 AND completion_granter_user_id IS NULL
477 AND deleted_at IS NULL
478 ",
479 course_module_id,
480 course_id,
481 user_id,
482 )
483 .fetch_one(conn)
484 .await?;
485 Ok(res)
486}
487
488pub async fn update_completion_registration_attempt_date(
489 conn: &mut PgConnection,
490 id: Uuid,
491 completion_registration_attempt_date: DateTime<Utc>,
492) -> ModelResult<bool> {
493 let res = sqlx::query!(
494 "
495UPDATE course_module_completions
496SET completion_registration_attempt_date = $1
497WHERE id = $2
498 AND deleted_at IS NULL
499 ",
500 Some(completion_registration_attempt_date),
501 id,
502 )
503 .execute(conn)
504 .await?;
505 Ok(res.rows_affected() > 0)
506}
507
508pub async fn update_prerequisite_modules_completed(
509 conn: &mut PgConnection,
510 id: Uuid,
511 prerequisite_modules_completed: bool,
512) -> ModelResult<bool> {
513 let res = sqlx::query!(
514 "
515UPDATE course_module_completions SET prerequisite_modules_completed = $1
516WHERE id = $2 AND deleted_at IS NULL
517 ",
518 prerequisite_modules_completed,
519 id
520 )
521 .execute(conn)
522 .await?;
523 Ok(res.rows_affected() > 0)
524}
525
526pub async fn update_passed_and_grade_status(
527 conn: &mut PgConnection,
528 course_id: Uuid,
529 user_id: Uuid,
530 passed: bool,
531 grade: i32,
532) -> ModelResult<bool> {
533 let res = sqlx::query!(
534 "
535UPDATE course_module_completions SET passed = $1, grade = $2
536WHERE user_id = $3 AND course_id = $4 AND deleted_at IS NULL
537 ",
538 passed,
539 grade,
540 user_id,
541 course_id
542 )
543 .execute(conn)
544 .await?;
545 Ok(res.rows_affected() > 0)
546}
547
548pub async fn update_needs_to_be_reviewed(
549 conn: &mut PgConnection,
550 id: Uuid,
551 needs_to_be_reviewed: bool,
552) -> ModelResult<bool> {
553 let res = sqlx::query!(
554 "
555UPDATE course_module_completions SET needs_to_be_reviewed = $1
556WHERE id = $2 AND deleted_at IS NULL
557 ",
558 needs_to_be_reviewed,
559 id
560 )
561 .execute(conn)
562 .await?;
563 Ok(res.rows_affected() > 0)
564}
565
566pub async fn user_has_completed_course_module(
569 conn: &mut PgConnection,
570 user_id: Uuid,
571 course_module_id: Uuid,
572) -> ModelResult<bool> {
573 let res = get_all_by_course_module_and_user_ids(conn, course_module_id, user_id).await?;
574 Ok(!res.is_empty())
575}
576
577#[derive(Clone, PartialEq, Deserialize, Serialize)]
579#[cfg_attr(feature = "ts_rs", derive(TS))]
580pub struct StudyRegistryCompletion {
581 pub completion_date: DateTime<Utc>,
587 pub completion_language: String,
589 pub completion_registration_attempt_date: Option<DateTime<Utc>>,
591 pub email: String,
595 pub grade: StudyRegistryGrade,
597 pub id: Uuid,
599 pub user_id: Uuid,
601 pub tier: Option<i32>,
604}
605
606impl From<CourseModuleCompletion> for StudyRegistryCompletion {
607 fn from(completion: CourseModuleCompletion) -> Self {
608 Self {
609 completion_date: completion.completion_date,
610 completion_language: completion.completion_language,
611 completion_registration_attempt_date: completion.completion_registration_attempt_date,
612 email: completion.email,
613 grade: StudyRegistryGrade::new(completion.passed, completion.grade),
614 id: completion.id,
615 user_id: completion.user_id,
616 tier: None,
617 }
618 }
619}
620
621#[derive(Clone, PartialEq, Deserialize, Serialize)]
660#[cfg_attr(feature = "ts_rs", derive(TS))]
661pub struct StudyRegistryGrade {
662 pub scale: String,
663 pub grade: String,
664}
665
666impl StudyRegistryGrade {
667 pub fn new(passed: bool, grade: Option<i32>) -> Self {
668 match grade {
669 Some(grade) => Self {
670 scale: "sis-0-5".to_string(),
671 grade: grade.to_string(),
672 },
673 None => Self {
674 scale: "sis-hyv-hyl".to_string(),
675 grade: if passed {
676 "1".to_string()
677 } else {
678 "0".to_string()
679 },
680 },
681 }
682 }
683}
684pub fn stream_by_course_module_id<'a>(
688 conn: &'a mut PgConnection,
689 course_module_ids: &'a [Uuid],
690 no_completions_registered_by_this_study_registry_registrar: &'a Option<StudyRegistryRegistrar>,
691) -> impl Stream<Item = sqlx::Result<StudyRegistryCompletion>> + Send + 'a {
692 let study_module_registrar_id = no_completions_registered_by_this_study_registry_registrar
694 .clone()
695 .map(|o| o.id)
696 .unwrap_or(Uuid::nil());
697
698 sqlx::query_as!(
699 CourseModuleCompletion,
700 r#"
701SELECT *
702FROM course_module_completions
703WHERE course_module_id = ANY($1)
704 AND prerequisite_modules_completed
705 AND eligible_for_ects IS TRUE
706 AND deleted_at IS NULL
707 AND id NOT IN (
708 SELECT course_module_completion_id
709 FROM course_module_completion_registered_to_study_registries
710 WHERE course_module_id = ANY($1)
711 AND study_registry_registrar_id = $2
712 AND deleted_at IS NULL
713 )
714 "#,
715 course_module_ids,
716 study_module_registrar_id,
717 )
718 .map(StudyRegistryCompletion::from)
719 .fetch(conn)
720}
721
722pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
723 sqlx::query!(
724 "
725
726UPDATE course_module_completions
727SET deleted_at = now()
728WHERE id = $1
729 ",
730 id,
731 )
732 .execute(conn)
733 .await?;
734 Ok(())
735}