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