headless_lms_models/library/
progressing.rs

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