Skip to main content

headless_lms_models/library/
progressing.rs

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