headless_lms_models/library/
progressing.rs

1use chrono::{DateTime, Utc};
2use itertools::Itertools;
3use std::collections::HashMap;
4
5use crate::{
6    course_exams,
7    course_instance_enrollments::{self, NewCourseInstanceEnrollment},
8    course_instances::{self, CourseInstance},
9    course_module_completions::{
10        self, CourseModuleCompletion, CourseModuleCompletionGranter,
11        CourseModuleCompletionWithRegistrationInfo, NewCourseModuleCompletion,
12    },
13    course_modules::{self, AutomaticCompletionRequirements, CompletionPolicy, CourseModule},
14    courses, exams, open_university_registration_links,
15    prelude::*,
16    suspected_cheaters::{self, Threshold},
17    user_course_settings,
18    user_details::UserDetail,
19    user_exercise_states,
20    users::{self, User},
21};
22
23/// Checks whether the course module can be completed automatically and creates an entry for completion
24/// if the user meets the criteria. Also re-checks module completion prerequisites if the module is
25/// completed.
26pub async fn update_automatic_completion_status_and_grant_if_eligible(
27    conn: &mut PgConnection,
28    course_module: &CourseModule,
29    user_id: Uuid,
30) -> ModelResult<()> {
31    let mut tx = conn.begin().await?;
32    let completion =
33        create_automatic_course_module_completion_if_eligible(&mut tx, course_module, user_id)
34            .await?;
35    if let Some(completion) = completion {
36        let course = courses::get_course(&mut tx, course_module.course_id).await?;
37        let submodule_completions_required = course
38            .base_module_completion_requires_n_submodule_completions
39            .try_into()?;
40        update_module_completion_prerequisite_statuses_for_user(
41            &mut tx,
42            user_id,
43            course_module.course_id,
44            submodule_completions_required,
45        )
46        .await?;
47
48        if let Some(thresholds) =
49            suspected_cheaters::get_thresholds_by_module_id(&mut tx, completion.course_module_id)
50                .await?
51        {
52            check_and_insert_suspected_cheaters(
53                &mut tx,
54                user_id,
55                course.id,
56                &thresholds,
57                completion,
58            )
59            .await?;
60        }
61    }
62    tx.commit().await?;
63    Ok(())
64}
65
66pub async fn check_and_insert_suspected_cheaters(
67    conn: &mut PgConnection,
68    user_id: Uuid,
69    course_id: Uuid,
70    thresholds: &Threshold,
71    completion: CourseModuleCompletion,
72) -> ModelResult<()> {
73    let total_points = user_exercise_states::get_user_total_course_points(conn, user_id, course_id)
74        .await?
75        .unwrap_or(0.0);
76
77    let completed_module = course_modules::get_by_id(conn, completion.course_module_id).await?;
78    let is_default_module = completed_module.is_default_module();
79
80    let student_duration_seconds = if is_default_module {
81        course_instances::get_student_duration(conn, completion.user_id, course_id)
82            .await?
83            .unwrap_or(0)
84    } else {
85        let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
86        let default_completion = course_module_completions::get_all_by_course_module_and_user_ids(
87            conn,
88            default_module.id,
89            completion.user_id,
90        )
91        .await?
92        .into_iter()
93        .max_by_key(|c| c.completion_date);
94
95        if let Some(default_completion) = default_completion {
96            let duration =
97                (completion.completion_date - default_completion.completion_date).num_seconds();
98            duration.max(0)
99        } else {
100            // No default completion exists yet, fall back to calculating duration from enrollment time.
101            course_instances::get_student_duration(conn, completion.user_id, course_id)
102                .await?
103                .unwrap_or(0)
104        }
105    };
106
107    if (student_duration_seconds as i32) < thresholds.duration_seconds {
108        suspected_cheaters::insert(
109            conn,
110            completion.user_id,
111            course_id,
112            Some(student_duration_seconds as i32),
113            total_points as i32,
114        )
115        .await?;
116
117        course_module_completions::update_needs_to_be_reviewed(conn, completion.id, true).await?;
118    }
119
120    Ok(())
121}
122
123/// Creates completion for the user if eligible and previous one doesn't exist. Returns an Option containing
124/// the completion if one exists after calling this function.
125#[instrument(skip(conn))]
126async fn create_automatic_course_module_completion_if_eligible(
127    conn: &mut PgConnection,
128    course_module: &CourseModule,
129    user_id: Uuid,
130) -> ModelResult<Option<CourseModuleCompletion>> {
131    let existing_completion =
132        course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
133            conn,
134            course_module.id,
135            course_module.course_id,
136            user_id,
137        )
138        .await
139        .optional()?;
140    if let Some(existing_completion) = existing_completion {
141        // If user already has a completion, do not attempt to create a new one.
142        Ok(Some(existing_completion))
143    } else {
144        let eligible =
145            user_is_eligible_for_automatic_completion(conn, course_module, user_id).await?;
146        if eligible {
147            let course = courses::get_course(conn, course_module.course_id).await?;
148            let user = users::get_by_id(conn, user_id).await?;
149            if user.deleted_at.is_some() {
150                warn!("Cannot create a completion for a deleted user");
151                return Ok(None);
152            }
153            let user_details =
154                crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
155            let completion = course_module_completions::insert(
156                conn,
157                PKeyPolicy::Generate,
158                &NewCourseModuleCompletion {
159                    course_id: course_module.course_id,
160                    course_module_id: course_module.id,
161                    user_id,
162                    completion_date: Utc::now(),
163                    completion_registration_attempt_date: None,
164                    completion_language: course.language_code,
165                    eligible_for_ects: true,
166                    email: user_details.email,
167                    grade: None,
168                    passed: true,
169                },
170                CourseModuleCompletionGranter::Automatic,
171            )
172            .await?;
173            info!("Created a completion");
174            Ok(Some(completion))
175        } else {
176            // Can't grant automatic completion; no-op.
177            Ok(None)
178        }
179    }
180}
181
182#[instrument(skip(conn))]
183async fn user_is_eligible_for_automatic_completion(
184    conn: &mut PgConnection,
185    course_module: &CourseModule,
186    user_id: Uuid,
187) -> ModelResult<bool> {
188    match &course_module.completion_policy {
189        CompletionPolicy::Automatic(requirements) => {
190            let eligible = user_passes_automatic_completion_exercise_tresholds(
191                conn,
192                user_id,
193                requirements,
194                course_module.course_id,
195            )
196            .await?;
197            if eligible {
198                if requirements.requires_exam {
199                    info!("To complete this module automatically, the user must pass an exam.");
200                    user_has_passed_exam_for_the_course_based_on_points(
201                        conn,
202                        user_id,
203                        course_module.course_id,
204                    )
205                    .await
206                } else {
207                    Ok(true)
208                }
209            } else {
210                Ok(false)
211            }
212        }
213        CompletionPolicy::Manual => Ok(false),
214    }
215}
216
217/// Checks whether the student can partake in an exam.
218///
219/// The result of this process depends on the configuration for the exam. If the exam is not linked
220/// to any course, the user will always be able to take it by default. Otherwise the student
221/// progress in their current selected instances is compared against any of the linked courses, and
222/// checked whether any pass the exercise completion tresholds. Finally, if none of the courses have
223/// automatic completion configuration, the exam is once again allowed to be taken by default.
224#[instrument(skip(conn))]
225pub async fn user_can_take_exam(
226    conn: &mut PgConnection,
227    exam_id: Uuid,
228    user_id: Uuid,
229) -> ModelResult<bool> {
230    let course_ids = course_exams::get_course_ids_by_exam_id(conn, exam_id).await?;
231    let settings = user_course_settings::get_all_by_user_and_multiple_current_courses(
232        conn,
233        &course_ids,
234        user_id,
235    )
236    .await?;
237    // User can take the exam by default if course_ids is an empty array.
238    let mut can_take_exam = true;
239    for course_id in course_ids {
240        let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
241        if let CompletionPolicy::Automatic(requirements) = &default_module.completion_policy {
242            if let Some(s) = settings.iter().find(|x| x.current_course_id == course_id) {
243                let eligible = user_passes_automatic_completion_exercise_tresholds(
244                    conn,
245                    s.user_id,
246                    requirements,
247                    s.current_course_id,
248                )
249                .await?;
250                if eligible {
251                    // Only one current instance needs to pass the tresholds.
252                    can_take_exam = true;
253                    break;
254                }
255            }
256            // If there is at least one associated course with requirements, make sure that the user
257            // passes one of them.
258            can_take_exam = false;
259        }
260    }
261    Ok(can_take_exam)
262}
263
264/// Returns true if there is at least one exam associated with the course, that has ended and the
265/// user has received enough points from it.
266async fn user_has_passed_exam_for_the_course_based_on_points(
267    conn: &mut PgConnection,
268    user_id: Uuid,
269    course_id: Uuid,
270) -> ModelResult<bool> {
271    let now = Utc::now();
272    let exam_ids = course_exams::get_exam_ids_by_course_id(conn, course_id).await?;
273    for exam_id in exam_ids {
274        let exam = exams::get(conn, exam_id).await?;
275        // A minimum points threshold of 0 indicates that the "Related courses can be completed automatically" option has not been enabled by the teacher. If you wish to remove this condition, please first store this information in a separate column in the exams table.
276        if exam.minimum_points_treshold == 0 || exam.grade_manually {
277            continue;
278        }
279        if exam.ended_at_or(now, false) {
280            let points =
281                user_exercise_states::get_user_total_exam_points(conn, user_id, exam_id).await?;
282            if let Some(points) = points {
283                if points >= exam.minimum_points_treshold as f32 {
284                    return Ok(true);
285                }
286            }
287        }
288    }
289    Ok(false)
290}
291
292async fn user_passes_automatic_completion_exercise_tresholds(
293    conn: &mut PgConnection,
294    user_id: Uuid,
295    requirements: &AutomaticCompletionRequirements,
296    course_id: Uuid,
297) -> ModelResult<bool> {
298    let user_metrics = user_exercise_states::get_single_module_metrics(
299        conn,
300        course_id,
301        requirements.course_module_id,
302        user_id,
303    )
304    .await?;
305    let attempted_exercises: i32 = user_metrics.attempted_exercises.unwrap_or(0) as i32;
306    let exercise_points = user_metrics.score_given.unwrap_or(0.0) as i32;
307    let eligible = requirements.passes_exercise_tresholds(attempted_exercises, exercise_points);
308    Ok(eligible)
309}
310
311/// Fetches all course module completions for the given user on the given course and updates the
312/// prerequisite module completion statuses for any completions that are missing them.
313#[instrument(skip(conn))]
314async fn update_module_completion_prerequisite_statuses_for_user(
315    conn: &mut PgConnection,
316    user_id: Uuid,
317    course_id: Uuid,
318    base_module_completion_requires_n_submodule_completions: u32,
319) -> ModelResult<()> {
320    let default_course_module = course_modules::get_default_by_course_id(conn, course_id).await?;
321    let course_module_completions =
322        course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
323            .await?;
324    let default_module_is_completed = course_module_completions
325        .iter()
326        .any(|x| x.course_module_id == default_course_module.id);
327    let submodule_completions = course_module_completions
328        .iter()
329        .filter(|x| x.course_module_id != default_course_module.id)
330        .unique_by(|x| x.course_module_id)
331        .count();
332    let enough_submodule_completions = submodule_completions
333        >= base_module_completion_requires_n_submodule_completions.try_into()?;
334    let completions_needing_processing: Vec<_> = course_module_completions
335        .into_iter()
336        .filter(|x| !x.prerequisite_modules_completed)
337        .collect();
338    for completion in completions_needing_processing {
339        if completion.course_module_id == default_course_module.id {
340            if enough_submodule_completions {
341                course_module_completions::update_prerequisite_modules_completed(
342                    conn,
343                    completion.id,
344                    true,
345                )
346                .await?;
347            }
348        } else if default_module_is_completed {
349            course_module_completions::update_prerequisite_modules_completed(
350                conn,
351                completion.id,
352                true,
353            )
354            .await?;
355        }
356    }
357    Ok(())
358}
359
360/// Goes through all user on a course and grants completions where eligible.
361#[instrument(skip(conn))]
362pub async fn process_all_course_completions(
363    conn: &mut PgConnection,
364    course_id: Uuid,
365) -> ModelResult<()> {
366    info!("Reprocessing course completions");
367    let course = courses::get_course(conn, course_id).await?;
368    let submodule_completions_required = course
369        .base_module_completion_requires_n_submodule_completions
370        .try_into()?;
371    let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
372    // If user has an user exercise state, they might have returned an exercise so we need to check whether they have completed modules.
373    let users =
374        crate::users::get_all_user_ids_with_user_exercise_states_on_course(conn, course_id).await?;
375    info!(users = ?users.len(), course_modules = ?course_modules.len(), ?submodule_completions_required, "Completion reprocessing info");
376    for course_module in course_modules.iter() {
377        info!(?course_module, "Course module information");
378    }
379    let mut tx = conn.begin().await?;
380    for user_id in users {
381        let mut num_completions = 0;
382        for course_module in course_modules.iter() {
383            let completion = create_automatic_course_module_completion_if_eligible(
384                &mut tx,
385                course_module,
386                user_id,
387            )
388            .await?;
389            if completion.is_some() {
390                num_completions += 1;
391            }
392        }
393        if num_completions > 0 {
394            update_module_completion_prerequisite_statuses_for_user(
395                &mut tx,
396                user_id,
397                course_id,
398                submodule_completions_required,
399            )
400            .await?;
401        }
402    }
403    tx.commit().await?;
404    info!("Reprocessing course module completions complete");
405    Ok(())
406}
407
408#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
409#[cfg_attr(feature = "ts_rs", derive(TS))]
410pub struct CourseInstanceCompletionSummary {
411    pub course_modules: Vec<CourseModule>,
412    pub users_with_course_module_completions: Vec<UserWithModuleCompletions>,
413}
414
415#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
416#[cfg_attr(feature = "ts_rs", derive(TS))]
417pub struct UserWithModuleCompletions {
418    pub completed_modules: Vec<CourseModuleCompletionWithRegistrationInfo>,
419    pub email: String,
420    pub first_name: Option<String>,
421    pub last_name: Option<String>,
422    pub user_id: Uuid,
423}
424
425#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
426#[cfg_attr(feature = "ts_rs", derive(TS))]
427pub struct UserCourseModuleCompletion {
428    pub course_module_id: Uuid,
429    pub grade: Option<i32>,
430    pub passed: bool,
431}
432
433impl From<CourseModuleCompletion> for UserCourseModuleCompletion {
434    fn from(course_module_completion: CourseModuleCompletion) -> Self {
435        Self {
436            course_module_id: course_module_completion.course_module_id,
437            grade: course_module_completion.grade,
438            passed: course_module_completion.passed,
439        }
440    }
441}
442
443impl UserWithModuleCompletions {
444    fn from_user_and_details(user: User, user_details: UserDetail) -> Self {
445        Self {
446            user_id: user.id,
447            first_name: user_details.first_name,
448            last_name: user_details.last_name,
449            email: user_details.email,
450            completed_modules: vec![],
451        }
452    }
453}
454
455pub async fn get_course_instance_completion_summary(
456    conn: &mut PgConnection,
457    course_instance: &CourseInstance,
458) -> ModelResult<CourseInstanceCompletionSummary> {
459    let course_modules = course_modules::get_by_course_id(conn, course_instance.course_id).await?;
460    let users_with_course_module_completions_list =
461        users::get_users_by_course_instance_enrollment(conn, course_instance.id).await?;
462    let user_id_to_details_map = crate::user_details::get_users_details_by_user_id_map(
463        conn,
464        &users_with_course_module_completions_list,
465    )
466    .await?;
467    let mut users_with_course_module_completions: HashMap<Uuid, UserWithModuleCompletions> =
468        users_with_course_module_completions_list
469            .into_iter()
470            .filter_map(|o| {
471                let details = user_id_to_details_map.get(&o.id);
472                details.map(|details| (o, details))
473            })
474            .map(|u| {
475                (
476                    u.0.id,
477                    UserWithModuleCompletions::from_user_and_details(u.0, u.1.clone()),
478                )
479            })
480            .collect();
481    let completions =
482        course_module_completions::get_all_with_registration_information_by_course_instance_id(
483            conn,
484            course_instance.id,
485            course_instance.course_id,
486        )
487        .await?;
488    completions.into_iter().for_each(|x| {
489        let user_with_completions = users_with_course_module_completions.get_mut(&x.user_id);
490        if let Some(completion) = user_with_completions {
491            completion.completed_modules.push(x);
492        }
493    });
494    Ok(CourseInstanceCompletionSummary {
495        course_modules,
496        users_with_course_module_completions: users_with_course_module_completions
497            .into_values()
498            .collect(),
499    })
500}
501
502#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
503#[cfg_attr(feature = "ts_rs", derive(TS))]
504pub struct TeacherManualCompletionRequest {
505    pub course_module_id: Uuid,
506    pub new_completions: Vec<TeacherManualCompletion>,
507    pub skip_duplicate_completions: bool,
508}
509
510#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
511#[cfg_attr(feature = "ts_rs", derive(TS))]
512pub struct TeacherManualCompletion {
513    pub user_id: Uuid,
514    pub grade: Option<i32>,
515    pub passed: bool,
516    pub completion_date: Option<DateTime<Utc>>,
517}
518
519pub async fn add_manual_completions(
520    conn: &mut PgConnection,
521    completion_giver_user_id: Uuid,
522    course_instance: &CourseInstance,
523    manual_completion_request: &TeacherManualCompletionRequest,
524) -> ModelResult<()> {
525    let course_module =
526        course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
527    if course_module.course_id != course_instance.course_id {
528        return Err(ModelError::new(
529            ModelErrorType::PreconditionFailed,
530            "Course module not part of the course.".to_string(),
531            None,
532        ));
533    }
534    let course = courses::get_course(conn, course_instance.course_id).await?;
535    let mut tx = conn.begin().await?;
536    for completion in manual_completion_request.new_completions.iter() {
537        let completion_receiver = users::get_by_id(&mut tx, completion.user_id).await?;
538        let completion_receiver_user_details =
539            crate::user_details::get_user_details_by_user_id(&mut tx, completion_receiver.id)
540                .await?;
541        let module_completed = course_module_completions::user_has_completed_course_module(
542            &mut tx,
543            completion.user_id,
544            manual_completion_request.course_module_id,
545        )
546        .await?;
547        if !module_completed || !manual_completion_request.skip_duplicate_completions {
548            course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
549                &mut tx,
550                NewCourseInstanceEnrollment {
551                    user_id: completion_receiver.id,
552                    course_id: course.id,
553                    course_instance_id: course_instance.id,
554                },
555            )
556            .await?;
557
558            if completion.grade.is_some()
559                && (completion.grade > Some(5) || completion.grade < Some(0))
560            {
561                return Err(ModelError::new(
562                    ModelErrorType::PreconditionFailed,
563                    "Invalid grade".to_string(),
564                    None,
565                ));
566            }
567            course_module_completions::insert(
568                &mut tx,
569                PKeyPolicy::Generate,
570                &NewCourseModuleCompletion {
571                    course_id: course_instance.course_id,
572                    course_module_id: manual_completion_request.course_module_id,
573                    user_id: completion.user_id,
574                    completion_date: completion.completion_date.unwrap_or_else(Utc::now),
575                    completion_registration_attempt_date: None,
576                    completion_language: course.language_code.clone(),
577                    eligible_for_ects: true,
578                    email: completion_receiver_user_details.email,
579                    grade: completion.grade,
580                    passed: if completion.grade == Some(0) {
581                        false
582                    } else {
583                        completion.passed
584                    },
585                },
586                CourseModuleCompletionGranter::User(completion_giver_user_id),
587            )
588            .await?;
589
590            // User may not have enrolled to the course at all, or they may have enrolled to a different instance. By inserting the enrollment
591            crate::course_instance_enrollments::insert_enrollment_and_set_as_current(
592                &mut tx,
593                NewCourseInstanceEnrollment {
594                    user_id: completion_receiver.id,
595                    course_id: course.id,
596                    course_instance_id: course_instance.id,
597                },
598            )
599            .await?;
600
601            update_module_completion_prerequisite_statuses_for_user(
602                &mut tx,
603                completion_receiver.id,
604                course.id,
605                course
606                    .base_module_completion_requires_n_submodule_completions
607                    .try_into()?,
608            )
609            .await?;
610        }
611    }
612    tx.commit().await?;
613    Ok(())
614}
615
616#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
617#[cfg_attr(feature = "ts_rs", derive(TS))]
618pub struct ManualCompletionPreview {
619    pub already_completed_users: Vec<ManualCompletionPreviewUser>,
620    pub first_time_completing_users: Vec<ManualCompletionPreviewUser>,
621    pub non_enrolled_users: Vec<ManualCompletionPreviewUser>,
622}
623
624#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
625#[cfg_attr(feature = "ts_rs", derive(TS))]
626pub struct ManualCompletionPreviewUser {
627    pub user_id: Uuid,
628    pub first_name: Option<String>,
629    pub last_name: Option<String>,
630    pub grade: Option<i32>,
631    pub passed: bool,
632    pub previous_best_grade: Option<i32>,
633}
634
635/// Gets a preview of changes that will occur to completions with the given manual completion data.
636pub async fn get_manual_completion_result_preview(
637    conn: &mut PgConnection,
638    course_instance: &CourseInstance,
639    manual_completion_request: &TeacherManualCompletionRequest,
640) -> ModelResult<ManualCompletionPreview> {
641    let course_module =
642        course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
643    if course_module.course_id != course_instance.course_id {
644        return Err(ModelError::new(
645            ModelErrorType::PreconditionFailed,
646            "Course module not part of the course.".to_string(),
647            None,
648        ));
649    }
650    let mut already_completed_users = vec![];
651    let mut first_time_completing_users = vec![];
652    let mut non_enrolled_users = vec![];
653    for completion in manual_completion_request.new_completions.iter() {
654        let user = users::get_by_id(conn, completion.user_id).await?;
655        let user_details = crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
656        let user = ManualCompletionPreviewUser {
657            user_id: user.id,
658            first_name: user_details.first_name,
659            last_name: user_details.last_name,
660            grade: completion.grade,
661            passed: completion.passed,
662            previous_best_grade: None,
663        };
664        let enrollment = course_instance_enrollments::get_by_user_and_course_instance_id(
665            conn,
666            completion.user_id,
667            course_instance.id,
668        )
669        .await
670        .optional()?;
671        if enrollment.is_none() {
672            non_enrolled_users.push(user.clone());
673        }
674        let module_completed = course_module_completions::user_has_completed_course_module(
675            conn,
676            completion.user_id,
677            manual_completion_request.course_module_id,
678        )
679        .await?;
680        if module_completed {
681            already_completed_users.push(user);
682        } else {
683            first_time_completing_users.push(user);
684        }
685    }
686    Ok(ManualCompletionPreview {
687        already_completed_users,
688        first_time_completing_users,
689        non_enrolled_users,
690    })
691}
692
693#[derive(Clone, PartialEq, Deserialize, Serialize)]
694#[cfg_attr(feature = "ts_rs", derive(TS))]
695pub struct UserCompletionInformation {
696    pub course_module_completion_id: Uuid,
697    pub course_name: String,
698    pub uh_course_code: String,
699    pub email: String,
700    pub ects_credits: Option<f32>,
701    pub enable_registering_completion_to_uh_open_university: bool,
702}
703
704pub async fn get_user_completion_information(
705    conn: &mut PgConnection,
706    user_id: Uuid,
707    course_module: &CourseModule,
708) -> ModelResult<UserCompletionInformation> {
709    let user = users::get_by_id(conn, user_id).await?;
710    let course = courses::get_course(conn, course_module.course_id).await?;
711    let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
712        conn,
713        course_module.id,
714        user.id,
715    )
716    .await?;
717    // Course code is required only so that fetching the link later works.
718    let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
719        ModelError::new(
720            ModelErrorType::InvalidRequest,
721            "Course module is missing uh_course_code.".to_string(),
722            None,
723        )
724    })?;
725    Ok(UserCompletionInformation {
726        course_module_completion_id: course_module_completion.id,
727        course_name: course_module
728            .name
729            .clone()
730            .unwrap_or_else(|| course.name.clone()),
731        uh_course_code,
732        ects_credits: course_module.ects_credits,
733        email: course_module_completion.email,
734        enable_registering_completion_to_uh_open_university: course_module
735            .enable_registering_completion_to_uh_open_university,
736    })
737}
738
739#[derive(Clone, PartialEq, Deserialize, Serialize)]
740#[cfg_attr(feature = "ts_rs", derive(TS))]
741pub struct UserModuleCompletionStatus {
742    pub completed: bool,
743    pub default: bool,
744    pub module_id: Uuid,
745    pub name: String,
746    pub order_number: i32,
747    pub prerequisite_modules_completed: bool,
748    pub grade: Option<i32>,
749    pub passed: Option<bool>,
750    pub enable_registering_completion_to_uh_open_university: bool,
751    pub certification_enabled: bool,
752    pub certificate_configuration_id: Option<Uuid>,
753    pub needs_to_be_reviewed: bool,
754}
755
756/// Gets course modules with user's completion status for the given instance.
757pub async fn get_user_module_completion_statuses_for_course(
758    conn: &mut PgConnection,
759    user_id: Uuid,
760    course_id: Uuid,
761) -> ModelResult<Vec<UserModuleCompletionStatus>> {
762    let course = courses::get_course(conn, course_id).await?;
763    let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
764
765    let course_module_completions_raw =
766        course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
767            .await?;
768
769    let course_module_completions: HashMap<Uuid, CourseModuleCompletion> =
770        course_module_completions_raw
771            .into_iter()
772            .sorted_by_key(|c| c.course_module_id)
773            .chunk_by(|c| c.course_module_id)
774            .into_iter()
775            .filter_map(|(module_id, group)| {
776                crate::course_module_completions::select_best_completion(group.collect())
777                    .map(|best| (module_id, best))
778            })
779            .collect();
780
781    let all_default_certificate_configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(conn, course_id).await?;
782
783    let course_module_completion_statuses = course_modules
784        .into_iter()
785        .map(|module| {
786            let mut certificate_configuration_id = None;
787
788            let completion = course_module_completions.get(&module.id);
789            let passed = completion.map(|x| x.passed);
790            if module.certification_enabled && passed == Some(true) {
791                // If passed, show the user the default certificate configuration id so that they can generate their certificate.
792                let default_certificate_configuration = all_default_certificate_configurations
793                    .iter()
794                    .find(|x| x.requirements.course_module_ids.contains(&module.id));
795                if let Some(default_certificate_configuration) = default_certificate_configuration {
796                    certificate_configuration_id = Some(
797                        default_certificate_configuration
798                            .certificate_configuration
799                            .id,
800                    );
801                }
802            }
803            UserModuleCompletionStatus {
804                completed: completion.is_some(),
805                default: module.is_default_module(),
806                module_id: module.id,
807                name: module.name.unwrap_or_else(|| course.name.clone()),
808                order_number: module.order_number,
809                passed,
810                grade: completion.and_then(|x| x.grade),
811                prerequisite_modules_completed: completion
812                    .is_some_and(|x| x.prerequisite_modules_completed),
813                enable_registering_completion_to_uh_open_university: module
814                    .enable_registering_completion_to_uh_open_university,
815                certification_enabled: module.certification_enabled,
816                certificate_configuration_id,
817                needs_to_be_reviewed: completion.is_some_and(|x| x.needs_to_be_reviewed),
818            }
819        })
820        .collect();
821    Ok(course_module_completion_statuses)
822}
823
824#[derive(Clone, PartialEq, Deserialize, Serialize)]
825#[cfg_attr(feature = "ts_rs", derive(TS))]
826pub struct CompletionRegistrationLink {
827    pub url: String,
828}
829
830pub async fn get_completion_registration_link_and_save_attempt(
831    conn: &mut PgConnection,
832    user_id: Uuid,
833    course_module: &CourseModule,
834) -> ModelResult<CompletionRegistrationLink> {
835    if !course_module.enable_registering_completion_to_uh_open_university {
836        return Err(ModelError::new(
837            ModelErrorType::InvalidRequest,
838            "Completion registration is not enabled for this course module.".to_string(),
839            None,
840        ));
841    }
842    let user = users::get_by_id(conn, user_id).await?;
843
844    let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
845        conn,
846        course_module.id,
847        user.id,
848    )
849    .await?;
850    course_module_completions::update_completion_registration_attempt_date(
851        conn,
852        course_module_completion.id,
853        Utc::now(),
854    )
855    .await?;
856    let registration_link = if let Some(link_override) =
857        course_module.completion_registration_link_override.as_ref()
858    {
859        link_override.clone()
860    } else {
861        let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
862            ModelError::new(
863                ModelErrorType::PreconditionFailed,
864                "Course module doesn't have an assossiated University of Helsinki course code."
865                    .to_string(),
866                None,
867            )
868        })?;
869        open_university_registration_links::get_link_by_course_code(conn, &uh_course_code).await?
870    };
871
872    Ok(CompletionRegistrationLink {
873        url: registration_link,
874    })
875}
876
877#[cfg(test)]
878mod tests {
879    use chrono::Duration;
880    use user_exercise_states::{ReviewingStage, UserExerciseStateUpdate};
881
882    use super::*;
883
884    use crate::{
885        exercises::{ActivityProgress, GradingProgress},
886        test_helper::*,
887    };
888
889    mod grant_automatic_completion_if_eligible {
890        use super::*;
891        use crate::{
892            chapters::NewChapter,
893            course_modules::{
894                self, AutomaticCompletionRequirements, CompletionPolicy, NewCourseModule,
895            },
896            exercises::{self, ActivityProgress, GradingProgress},
897            library::content_management,
898            user_exercise_states::{self, ReviewingStage, UserExerciseStateUpdate},
899        };
900
901        #[tokio::test]
902        async fn grants_automatic_completion_but_no_prerequisite_for_default_module() {
903            insert_data!(:tx);
904            let (mut tx, user, course, _instance, default_module, _submodule_1, _submodule_2) =
905                create_test_data(tx).await;
906            update_automatic_completion_status_and_grant_if_eligible(
907                tx.as_mut(),
908                &default_module,
909                user,
910            )
911            .await
912            .unwrap();
913            let statuses =
914                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
915                    .await
916                    .unwrap();
917            let status = statuses
918                .iter()
919                .find(|x| x.module_id == default_module.id)
920                .unwrap();
921            assert!(status.completed);
922            assert!(!status.prerequisite_modules_completed);
923        }
924
925        #[tokio::test]
926        async fn grants_automatic_completion_but_no_prerequisite_for_submodule() {
927            insert_data!(:tx);
928            let (mut tx, user, course, _instance, _default_module, submodule_1, _submodule_2) =
929                create_test_data(tx).await;
930            update_automatic_completion_status_and_grant_if_eligible(
931                tx.as_mut(),
932                &submodule_1,
933                user,
934            )
935            .await
936            .unwrap();
937            let statuses =
938                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
939                    .await
940                    .unwrap();
941            let status = statuses
942                .iter()
943                .find(|x| x.module_id == submodule_1.id)
944                .unwrap();
945            assert!(status.completed);
946            assert!(!status.prerequisite_modules_completed);
947        }
948
949        #[tokio::test]
950        async fn grants_automatic_completion_for_eligible_submodule_when_completing_default_module()
951        {
952            insert_data!(:tx);
953            let (mut tx, user, course, _instance, default_module, submodule_1, submodule_2) =
954                create_test_data(tx).await;
955            update_automatic_completion_status_and_grant_if_eligible(
956                tx.as_mut(),
957                &default_module,
958                user,
959            )
960            .await
961            .unwrap();
962            update_automatic_completion_status_and_grant_if_eligible(
963                tx.as_mut(),
964                &submodule_1,
965                user,
966            )
967            .await
968            .unwrap();
969            update_automatic_completion_status_and_grant_if_eligible(
970                tx.as_mut(),
971                &submodule_2,
972                user,
973            )
974            .await
975            .unwrap();
976            let statuses =
977                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
978                    .await
979                    .unwrap();
980            statuses.iter().for_each(|x| {
981                assert!(x.completed);
982                assert!(x.prerequisite_modules_completed);
983            });
984        }
985
986        async fn create_test_data(
987            mut tx: Tx<'_>,
988        ) -> (
989            Tx<'_>,
990            Uuid,
991            Uuid,
992            Uuid,
993            CourseModule,
994            CourseModule,
995            CourseModule,
996        ) {
997            insert_data!(tx: tx; :user, :org, :course, :instance, :course_module, :chapter, :page, :exercise);
998            let automatic_completion_policy =
999                CompletionPolicy::Automatic(AutomaticCompletionRequirements {
1000                    course_module_id: course_module.id,
1001                    number_of_exercises_attempted_treshold: Some(0),
1002                    number_of_points_treshold: Some(0),
1003                    requires_exam: false,
1004                });
1005            courses::update_course_base_module_completion_count_requirement(tx.as_mut(), course, 1)
1006                .await
1007                .unwrap();
1008            let course_module_2 = course_modules::insert(
1009                tx.as_mut(),
1010                PKeyPolicy::Generate,
1011                &NewCourseModule::new(course, Some("Module 2".to_string()), 1),
1012            )
1013            .await
1014            .unwrap();
1015            let (chapter_2, page2) = content_management::create_new_chapter(
1016                tx.as_mut(),
1017                PKeyPolicy::Generate,
1018                &NewChapter {
1019                    name: "chapter 2".to_string(),
1020                    color: None,
1021                    course_id: course,
1022                    chapter_number: 2,
1023                    front_page_id: None,
1024                    opens_at: None,
1025                    deadline: None,
1026                    course_module_id: Some(course_module_2.id),
1027                },
1028                user,
1029                |_, _, _| unimplemented!(),
1030                |_| unimplemented!(),
1031            )
1032            .await
1033            .unwrap();
1034
1035            let exercise_2 = exercises::insert(
1036                tx.as_mut(),
1037                PKeyPolicy::Generate,
1038                course,
1039                "",
1040                page2.id,
1041                chapter_2.id,
1042                0,
1043            )
1044            .await
1045            .unwrap();
1046            let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
1047                tx.as_mut(),
1048                user,
1049                exercise,
1050                Some(course),
1051                None,
1052            )
1053            .await
1054            .unwrap();
1055            user_exercise_states::update(
1056                tx.as_mut(),
1057                UserExerciseStateUpdate {
1058                    id: user_exercise_state.id,
1059                    score_given: Some(0.0),
1060                    activity_progress: ActivityProgress::Completed,
1061                    reviewing_stage: ReviewingStage::NotStarted,
1062                    grading_progress: GradingProgress::FullyGraded,
1063                },
1064            )
1065            .await
1066            .unwrap();
1067            let user_exercise_state_2 = user_exercise_states::get_or_create_user_exercise_state(
1068                tx.as_mut(),
1069                user,
1070                exercise_2,
1071                Some(course),
1072                None,
1073            )
1074            .await
1075            .unwrap();
1076            user_exercise_states::update(
1077                tx.as_mut(),
1078                UserExerciseStateUpdate {
1079                    id: user_exercise_state_2.id,
1080                    score_given: Some(0.0),
1081                    activity_progress: ActivityProgress::Completed,
1082                    reviewing_stage: ReviewingStage::NotStarted,
1083                    grading_progress: GradingProgress::FullyGraded,
1084                },
1085            )
1086            .await
1087            .unwrap();
1088            let default_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1089                .await
1090                .unwrap();
1091            let default_module = course_modules::update_automatic_completion_status(
1092                tx.as_mut(),
1093                default_module.id,
1094                &automatic_completion_policy,
1095            )
1096            .await
1097            .unwrap();
1098            let course_module = course_modules::update_automatic_completion_status(
1099                tx.as_mut(),
1100                course_module.id,
1101                &automatic_completion_policy,
1102            )
1103            .await
1104            .unwrap();
1105            let course_module_2 = course_modules::update_automatic_completion_status(
1106                tx.as_mut(),
1107                course_module_2.id,
1108                &automatic_completion_policy,
1109            )
1110            .await
1111            .unwrap();
1112            (
1113                tx,
1114                user,
1115                course,
1116                instance.id,
1117                default_module,
1118                course_module,
1119                course_module_2,
1120            )
1121        }
1122    }
1123
1124    #[tokio::test]
1125    async fn tags_suspected_cheater() {
1126        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1127
1128        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1129            .await
1130            .unwrap();
1131        let state = user_exercise_states::get_or_create_user_exercise_state(
1132            tx.as_mut(),
1133            user,
1134            exercise,
1135            Some(course),
1136            None,
1137        )
1138        .await
1139        .unwrap();
1140        user_exercise_states::update(
1141            tx.as_mut(),
1142            UserExerciseStateUpdate {
1143                id: state.id,
1144                score_given: Some(10.0),
1145                activity_progress: ActivityProgress::Completed,
1146                reviewing_stage: ReviewingStage::NotStarted,
1147                grading_progress: GradingProgress::FullyGraded,
1148            },
1149        )
1150        .await
1151        .unwrap();
1152
1153        let completion = course_module_completions::insert(
1154            tx.as_mut(),
1155            PKeyPolicy::Generate,
1156            &NewCourseModuleCompletion {
1157                course_id: course,
1158                course_module_id: course_module.id,
1159                user_id: user,
1160                completion_date: Utc::now() + Duration::days(1),
1161                completion_registration_attempt_date: None,
1162                completion_language: "en-US".to_string(),
1163                eligible_for_ects: false,
1164                email: "email".to_string(),
1165                grade: None,
1166                passed: true,
1167            },
1168            CourseModuleCompletionGranter::Automatic,
1169        )
1170        .await
1171        .unwrap();
1172        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1173            tx.as_mut(),
1174            course_module.id,
1175            259200,
1176        )
1177        .await
1178        .unwrap();
1179        check_and_insert_suspected_cheaters(tx.as_mut(), user, course, &thresholds, completion)
1180            .await
1181            .unwrap();
1182
1183        let cheaters =
1184            suspected_cheaters::get_all_suspected_cheaters_in_course(tx.as_mut(), course, false)
1185                .await
1186                .unwrap();
1187        assert_eq!(cheaters.len(), 1);
1188        assert_eq!(cheaters[0].user_id, user);
1189    }
1190
1191    #[tokio::test]
1192    async fn doesnt_tag_suspected_cheater() {
1193        insert_data!(:tx, user:user, :org, :course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1194
1195        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1196            .await
1197            .unwrap();
1198        let state = user_exercise_states::get_or_create_user_exercise_state(
1199            tx.as_mut(),
1200            user,
1201            exercise,
1202            Some(course),
1203            None,
1204        )
1205        .await
1206        .unwrap();
1207        user_exercise_states::update(
1208            tx.as_mut(),
1209            UserExerciseStateUpdate {
1210                id: state.id,
1211                score_given: Some(9.0),
1212                activity_progress: ActivityProgress::Completed,
1213                reviewing_stage: ReviewingStage::NotStarted,
1214                grading_progress: GradingProgress::FullyGraded,
1215            },
1216        )
1217        .await
1218        .unwrap();
1219
1220        course_module_completions::insert(
1221            tx.as_mut(),
1222            PKeyPolicy::Generate,
1223            &NewCourseModuleCompletion {
1224                course_id: course,
1225                course_module_id: course_module.id,
1226                user_id: user,
1227                completion_date: Utc::now() + Duration::days(3),
1228                completion_registration_attempt_date: None,
1229                completion_language: "en-US".to_string(),
1230                eligible_for_ects: false,
1231                email: "email".to_string(),
1232                grade: Some(9),
1233                passed: true,
1234            },
1235            CourseModuleCompletionGranter::Automatic,
1236        )
1237        .await
1238        .unwrap();
1239        suspected_cheaters::insert_thresholds_by_module_id(tx.as_mut(), course_module.id, 172800)
1240            .await
1241            .unwrap();
1242        update_automatic_completion_status_and_grant_if_eligible(tx.as_mut(), &course_module, user)
1243            .await
1244            .unwrap();
1245
1246        let cheaters =
1247            suspected_cheaters::get_all_suspected_cheaters_in_course(tx.as_mut(), course, false)
1248                .await
1249                .unwrap();
1250        assert!(cheaters.is_empty());
1251    }
1252
1253    // TODO: New automatic completion tests?
1254}