headless_lms_models/library/
progressing.rs

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