Skip to main content

headless_lms_models/library/
progressing.rs

1use chrono::{DateTime, Utc};
2use itertools::Itertools;
3use std::collections::{HashMap, HashSet};
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,
16    library::credit_registration::StudentFacingCreditRegistrationStatus,
17    open_university_registration_links,
18    prelude::*,
19    suspected_cheaters, user_course_settings,
20    user_details::UserDetail,
21    user_exercise_states,
22    users::{self, User},
23};
24
25/// Checks whether the course module can be completed automatically and creates an entry for completion
26/// if the user meets the criteria. Also re-checks module completion prerequisites if the module is
27/// completed.
28pub async fn update_automatic_completion_status_and_grant_if_eligible(
29    conn: &mut PgConnection,
30    course_module: &CourseModule,
31    user_id: Uuid,
32) -> ModelResult<()> {
33    let mut tx = conn.begin().await?;
34    let completion =
35        create_automatic_course_module_completion_if_eligible(&mut tx, course_module, user_id)
36            .await?;
37    if let Some(completion) = completion {
38        let course = courses::get_course(&mut tx, course_module.course_id).await?;
39        let submodule_completions_required = course
40            .base_module_completion_requires_n_submodule_completions
41            .try_into()?;
42        update_module_completion_prerequisite_statuses_for_user(
43            &mut tx,
44            user_id,
45            course_module.course_id,
46            submodule_completions_required,
47        )
48        .await?;
49
50        if course.cheater_detection_enabled {
51            // Detection is on by default. When the course has no explicit threshold configured,
52            // fall back to the default (3 hours).
53            let threshold_seconds = suspected_cheaters::get_thresholds_by_module_id(
54                &mut tx,
55                completion.course_module_id,
56            )
57            .await?
58            .map(|t| t.duration_seconds)
59            .unwrap_or(suspected_cheaters::DEFAULT_CHEATER_THRESHOLD_SECONDS);
60            // A threshold of 0 (or less) is the documented per-module off-switch for the duration
61            // check. Only an explicitly configured 0 disables it -- the default fallback above is
62            // always positive.
63            if threshold_seconds > 0 {
64                check_and_insert_suspected_cheaters(
65                    &mut tx,
66                    user_id,
67                    course.id,
68                    threshold_seconds,
69                    completion,
70                )
71                .await?;
72            }
73        }
74    }
75    tx.commit().await?;
76    Ok(())
77}
78
79pub async fn check_and_insert_suspected_cheaters(
80    conn: &mut PgConnection,
81    user_id: Uuid,
82    course_id: Uuid,
83    threshold_seconds: i32,
84    completion: CourseModuleCompletion,
85) -> ModelResult<()> {
86    // A teacher-granted (manual) completion means the teacher has vouched for the student, so they
87    // are not subject to automatic cheating suspicion for this course.
88    if course_module_completions::user_has_manual_completion_in_course(conn, user_id, course_id)
89        .await?
90    {
91        return Ok(());
92    }
93
94    let total_points = user_exercise_states::get_user_total_course_points(conn, user_id, course_id)
95        .await?
96        .unwrap_or(0.0);
97
98    let completed_module = course_modules::get_by_id(conn, completion.course_module_id).await?;
99    let is_default_module = completed_module.is_default_module();
100
101    let student_duration_seconds = if is_default_module {
102        course_instances::get_student_duration(conn, completion.user_id, course_id)
103            .await?
104            .unwrap_or(0)
105    } else {
106        let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
107        let default_completion = course_module_completions::get_all_by_course_module_and_user_ids(
108            conn,
109            default_module.id,
110            completion.user_id,
111        )
112        .await?
113        .into_iter()
114        .max_by_key(|c| c.completion_date);
115
116        if let Some(default_completion) = default_completion {
117            let duration =
118                (completion.completion_date - default_completion.completion_date).num_seconds();
119            duration.max(0)
120        } else {
121            // No default completion exists yet, fall back to calculating duration from enrollment time.
122            course_instances::get_student_duration(conn, completion.user_id, course_id)
123                .await?
124                .unwrap_or(0)
125        }
126    };
127
128    if (student_duration_seconds as i32) < threshold_seconds {
129        let suspicion_is_active = suspected_cheaters::insert(
130            conn,
131            completion.user_id,
132            course_id,
133            completion.course_module_id,
134            Some(student_duration_seconds as i32),
135            total_points as i32,
136        )
137        .await?;
138
139        if suspicion_is_active {
140            course_module_completions::update_needs_to_be_reviewed(conn, completion.id, true)
141                .await?;
142        }
143    }
144
145    Ok(())
146}
147
148/// Creates completion for the user if eligible and previous one doesn't exist. Returns an Option containing
149/// the completion if one exists after calling this function.
150#[instrument(skip(conn))]
151async fn create_automatic_course_module_completion_if_eligible(
152    conn: &mut PgConnection,
153    course_module: &CourseModule,
154    user_id: Uuid,
155) -> ModelResult<Option<CourseModuleCompletion>> {
156    let existing_completion =
157        course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
158            conn,
159            course_module.id,
160            course_module.course_id,
161            user_id,
162        )
163        .await
164        .optional()?;
165    if let Some(existing_completion) = existing_completion {
166        // If user already has a completion, do not attempt to create a new one.
167        Ok(Some(existing_completion))
168    } else {
169        let eligible =
170            user_is_eligible_for_automatic_completion(conn, course_module, user_id).await?;
171        if eligible {
172            let course = courses::get_course(conn, course_module.course_id).await?;
173            let user = users::get_by_id(conn, user_id).await?;
174            if user.deleted_at.is_some() {
175                warn!("Cannot create a completion for a deleted user");
176                return Ok(None);
177            }
178            let user_details =
179                crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
180            let completion = course_module_completions::insert(
181                conn,
182                PKeyPolicy::Generate,
183                &NewCourseModuleCompletion {
184                    course_id: course_module.course_id,
185                    course_module_id: course_module.id,
186                    user_id,
187                    completion_date: Utc::now(),
188                    completion_registration_attempt_date: None,
189                    completion_language: course.language_code,
190                    eligible_for_ects: true,
191                    email: user_details.email,
192                    grade: None,
193                    passed: true,
194                },
195                CourseModuleCompletionGranter::Automatic,
196            )
197            .await?;
198            info!("Created a completion");
199            Ok(Some(completion))
200        } else {
201            // Can't grant automatic completion; no-op.
202            Ok(None)
203        }
204    }
205}
206
207#[instrument(skip(conn))]
208async fn user_is_eligible_for_automatic_completion(
209    conn: &mut PgConnection,
210    course_module: &CourseModule,
211    user_id: Uuid,
212) -> ModelResult<bool> {
213    match &course_module.completion_policy {
214        CompletionPolicy::Automatic(requirements) => {
215            let eligible = user_passes_automatic_completion_exercise_tresholds(
216                conn,
217                user_id,
218                requirements,
219                course_module.course_id,
220            )
221            .await?;
222            if eligible {
223                // Do not grant an automatic completion while the user still has answers in this
224                // module waiting for manual grading; a teacher must review them first.
225                if user_exercise_states::has_pending_manual_reviews_in_module(
226                    conn,
227                    user_id,
228                    course_module.course_id,
229                    requirements.course_module_id,
230                )
231                .await?
232                {
233                    info!(
234                        "The user has answers pending manual review in this module; not granting an automatic completion yet."
235                    );
236                    return Ok(false);
237                }
238                if requirements.requires_exam {
239                    info!("To complete this module automatically, the user must pass an exam.");
240                    user_has_passed_exam_for_the_course_based_on_points(
241                        conn,
242                        user_id,
243                        course_module.course_id,
244                    )
245                    .await
246                } else {
247                    Ok(true)
248                }
249            } else {
250                Ok(false)
251            }
252        }
253        CompletionPolicy::Manual => Ok(false),
254    }
255}
256
257/// Checks whether the student can partake in an exam.
258///
259/// The result of this process depends on the configuration for the exam. If the exam is not linked
260/// to any course, the user will always be able to take it by default. Otherwise the student
261/// progress in their current selected instances is compared against any of the linked courses, and
262/// checked whether any pass the exercise completion tresholds. Finally, if none of the courses have
263/// automatic completion configuration, the exam is once again allowed to be taken by default.
264#[instrument(skip(conn))]
265pub async fn user_can_take_exam(
266    conn: &mut PgConnection,
267    exam_id: Uuid,
268    user_id: Uuid,
269) -> ModelResult<bool> {
270    let course_ids = course_exams::get_course_ids_by_exam_id(conn, exam_id).await?;
271    let settings = user_course_settings::get_all_by_user_and_multiple_current_courses(
272        conn,
273        &course_ids,
274        user_id,
275    )
276    .await?;
277    // User can take the exam by default if course_ids is an empty array.
278    let mut can_take_exam = true;
279    for course_id in course_ids {
280        let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
281        if let CompletionPolicy::Automatic(requirements) = &default_module.completion_policy {
282            if let Some(s) = settings.iter().find(|x| x.current_course_id == course_id) {
283                let eligible = user_passes_automatic_completion_exercise_tresholds(
284                    conn,
285                    s.user_id,
286                    requirements,
287                    s.current_course_id,
288                )
289                .await?;
290                if eligible {
291                    // Only one current instance needs to pass the tresholds.
292                    can_take_exam = true;
293                    break;
294                }
295            }
296            // If there is at least one associated course with requirements, make sure that the user
297            // passes one of them.
298            can_take_exam = false;
299        }
300    }
301    Ok(can_take_exam)
302}
303
304/// Returns true if there is at least one exam associated with the course, that has ended and the
305/// user has received enough points from it.
306pub async fn user_has_passed_exam_for_the_course_based_on_points(
307    conn: &mut PgConnection,
308    user_id: Uuid,
309    course_id: Uuid,
310) -> ModelResult<bool> {
311    let now = Utc::now();
312    let exam_ids = course_exams::get_exam_ids_by_course_id(conn, course_id).await?;
313    for exam_id in exam_ids {
314        let exam = exams::get(conn, exam_id).await?;
315        // 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.
316        if exam.minimum_points_treshold == 0 || exam.grade_manually {
317            continue;
318        }
319        if exam.ended_at_or(now, false) {
320            let points =
321                user_exercise_states::get_user_total_exam_points(conn, user_id, exam_id).await?;
322            if let Some(points) = points
323                && points >= exam.minimum_points_treshold as f32
324            {
325                return Ok(true);
326            }
327        }
328    }
329    Ok(false)
330}
331
332async fn user_passes_automatic_completion_exercise_tresholds(
333    conn: &mut PgConnection,
334    user_id: Uuid,
335    requirements: &AutomaticCompletionRequirements,
336    course_id: Uuid,
337) -> ModelResult<bool> {
338    let user_metrics = user_exercise_states::get_single_module_metrics(
339        conn,
340        course_id,
341        requirements.course_module_id,
342        user_id,
343    )
344    .await?;
345    let attempted_exercises: i32 = user_metrics.attempted_exercises.unwrap_or(0) as i32;
346    let exercise_points = user_metrics.score_given.unwrap_or(0.0) as i32;
347    let eligible = requirements.passes_exercise_tresholds(attempted_exercises, exercise_points);
348    Ok(eligible)
349}
350
351/// Fetches all course module completions for the given user on the given course and updates the
352/// prerequisite module completion statuses for any completions that are missing them.
353#[instrument(skip(conn))]
354async fn update_module_completion_prerequisite_statuses_for_user(
355    conn: &mut PgConnection,
356    user_id: Uuid,
357    course_id: Uuid,
358    base_module_completion_requires_n_submodule_completions: u32,
359) -> ModelResult<()> {
360    let default_course_module = course_modules::get_default_by_course_id(conn, course_id).await?;
361    let course_module_completions =
362        course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
363            .await?;
364    let default_module_is_completed = course_module_completions
365        .iter()
366        .any(|x| x.course_module_id == default_course_module.id);
367    let submodule_completions = course_module_completions
368        .iter()
369        .filter(|x| x.course_module_id != default_course_module.id)
370        .unique_by(|x| x.course_module_id)
371        .count();
372    let enough_submodule_completions = submodule_completions
373        >= base_module_completion_requires_n_submodule_completions.try_into()?;
374    let completions_needing_processing: Vec<_> = course_module_completions
375        .into_iter()
376        .filter(|x| !x.prerequisite_modules_completed)
377        .collect();
378    for completion in completions_needing_processing {
379        if completion.course_module_id == default_course_module.id {
380            if enough_submodule_completions {
381                course_module_completions::update_prerequisite_modules_completed(
382                    conn,
383                    completion.id,
384                    true,
385                )
386                .await?;
387            }
388        } else if default_module_is_completed {
389            course_module_completions::update_prerequisite_modules_completed(
390                conn,
391                completion.id,
392                true,
393            )
394            .await?;
395        }
396    }
397    Ok(())
398}
399
400/// Goes through all user on a course and grants completions where eligible.
401#[instrument(skip(conn))]
402pub async fn process_all_course_completions(
403    conn: &mut PgConnection,
404    course_id: Uuid,
405) -> ModelResult<()> {
406    info!("Reprocessing course completions");
407    let course = courses::get_course(conn, course_id).await?;
408    let submodule_completions_required = course
409        .base_module_completion_requires_n_submodule_completions
410        .try_into()?;
411    let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
412    // If user has an user exercise state, they might have returned an exercise so we need to check whether they have completed modules.
413    let users =
414        crate::users::get_all_user_ids_with_user_exercise_states_on_course(conn, course_id).await?;
415    info!(users = ?users.len(), course_modules = ?course_modules.len(), ?submodule_completions_required, "Completion reprocessing info");
416    for course_module in course_modules.iter() {
417        info!(?course_module, "Course module information");
418    }
419    let mut tx = conn.begin().await?;
420    for user_id in users {
421        let mut num_completions = 0;
422        for course_module in course_modules.iter() {
423            let completion = create_automatic_course_module_completion_if_eligible(
424                &mut tx,
425                course_module,
426                user_id,
427            )
428            .await?;
429            if completion.is_some() {
430                num_completions += 1;
431            }
432        }
433        if num_completions > 0 {
434            update_module_completion_prerequisite_statuses_for_user(
435                &mut tx,
436                user_id,
437                course_id,
438                submodule_completions_required,
439            )
440            .await?;
441        }
442    }
443    tx.commit().await?;
444    info!("Reprocessing course module completions complete");
445    Ok(())
446}
447
448#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
449
450pub struct CourseInstanceCompletionSummary {
451    pub course_modules: Vec<CourseModule>,
452    pub users_with_course_module_completions: Vec<UserWithModuleCompletions>,
453}
454
455#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
456
457pub struct UserWithModuleCompletions {
458    pub completed_modules: Vec<CourseModuleCompletionWithRegistrationInfo>,
459    pub email: String,
460    pub first_name: Option<String>,
461    pub last_name: Option<String>,
462    pub user_id: Uuid,
463}
464
465#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
466
467pub struct UserCourseModuleCompletion {
468    pub course_module_id: Uuid,
469    pub grade: Option<i32>,
470    pub passed: bool,
471}
472
473impl From<CourseModuleCompletion> for UserCourseModuleCompletion {
474    fn from(course_module_completion: CourseModuleCompletion) -> Self {
475        Self {
476            course_module_id: course_module_completion.course_module_id,
477            grade: course_module_completion.grade,
478            passed: course_module_completion.passed,
479        }
480    }
481}
482
483impl UserWithModuleCompletions {
484    fn from_user_and_details(user: User, user_details: UserDetail) -> Self {
485        Self {
486            user_id: user.id,
487            first_name: user_details.first_name,
488            last_name: user_details.last_name,
489            email: user_details.email,
490            completed_modules: vec![],
491        }
492    }
493}
494
495pub async fn get_course_instance_completion_summary(
496    conn: &mut PgConnection,
497    course_instance: &CourseInstance,
498) -> ModelResult<CourseInstanceCompletionSummary> {
499    let course_modules = course_modules::get_by_course_id(conn, course_instance.course_id).await?;
500    let users_with_course_module_completions_list =
501        users::get_users_by_course_instance_enrollment(conn, course_instance.id).await?;
502    let user_id_to_details_map = crate::user_details::get_users_details_by_user_id_map(
503        conn,
504        &users_with_course_module_completions_list,
505    )
506    .await?;
507    let mut users_with_course_module_completions: HashMap<Uuid, UserWithModuleCompletions> =
508        users_with_course_module_completions_list
509            .into_iter()
510            .filter_map(|o| {
511                let details = user_id_to_details_map.get(&o.id);
512                details.map(|details| (o, details))
513            })
514            .map(|u| {
515                (
516                    u.0.id,
517                    UserWithModuleCompletions::from_user_and_details(u.0, u.1.clone()),
518                )
519            })
520            .collect();
521    let completions =
522        course_module_completions::get_all_with_registration_information_by_course_instance_id(
523            conn,
524            course_instance.id,
525            course_instance.course_id,
526        )
527        .await?;
528    completions.into_iter().for_each(|x| {
529        let user_with_completions = users_with_course_module_completions.get_mut(&x.user_id);
530        if let Some(completion) = user_with_completions {
531            completion.completed_modules.push(x);
532        }
533    });
534    Ok(CourseInstanceCompletionSummary {
535        course_modules,
536        users_with_course_module_completions: users_with_course_module_completions
537            .into_values()
538            .collect(),
539    })
540}
541
542#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
543
544pub struct TeacherManualCompletionRequest {
545    pub course_module_id: Uuid,
546    pub new_completions: Vec<TeacherManualCompletion>,
547    pub skip_duplicate_completions: bool,
548}
549
550#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
551
552pub struct TeacherManualCompletion {
553    pub user_id: Uuid,
554    pub grade: Option<i32>,
555    pub passed: bool,
556    pub completion_date: Option<DateTime<Utc>>,
557}
558
559pub async fn add_manual_completions(
560    conn: &mut PgConnection,
561    completion_giver_user_id: Uuid,
562    course_instance: &CourseInstance,
563    manual_completion_request: &TeacherManualCompletionRequest,
564) -> ModelResult<()> {
565    let course_module =
566        course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
567    if course_module.course_id != course_instance.course_id {
568        return Err(ModelError::new(
569            ModelErrorType::PreconditionFailed,
570            "Course module not part of the course.".to_string(),
571            None,
572        ));
573    }
574    let course = courses::get_course(conn, course_instance.course_id).await?;
575    let mut tx = conn.begin().await?;
576    for completion in manual_completion_request.new_completions.iter() {
577        let completion_receiver = users::get_by_id(&mut tx, completion.user_id).await?;
578        let completion_receiver_user_details =
579            crate::user_details::get_user_details_by_user_id(&mut tx, completion_receiver.id)
580                .await?;
581        let module_completed = course_module_completions::user_has_completed_course_module(
582            &mut tx,
583            completion.user_id,
584            manual_completion_request.course_module_id,
585        )
586        .await?;
587        if !module_completed || !manual_completion_request.skip_duplicate_completions {
588            course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
589                &mut tx,
590                NewCourseInstanceEnrollment {
591                    user_id: completion_receiver.id,
592                    course_id: course.id,
593                    course_instance_id: course_instance.id,
594                },
595            )
596            .await?;
597
598            if completion.grade.is_some()
599                && (completion.grade > Some(5) || completion.grade < Some(0))
600            {
601                return Err(ModelError::new(
602                    ModelErrorType::PreconditionFailed,
603                    "Invalid grade".to_string(),
604                    None,
605                ));
606            }
607            course_module_completions::insert(
608                &mut tx,
609                PKeyPolicy::Generate,
610                &NewCourseModuleCompletion {
611                    course_id: course_instance.course_id,
612                    course_module_id: manual_completion_request.course_module_id,
613                    user_id: completion.user_id,
614                    completion_date: completion.completion_date.unwrap_or_else(Utc::now),
615                    completion_registration_attempt_date: None,
616                    completion_language: course.language_code.clone(),
617                    eligible_for_ects: true,
618                    email: completion_receiver_user_details.email,
619                    grade: completion.grade,
620                    passed: if completion.grade == Some(0) {
621                        false
622                    } else {
623                        completion.passed
624                    },
625                },
626                CourseModuleCompletionGranter::User(completion_giver_user_id),
627            )
628            .await?;
629
630            // User may not have enrolled to the course at all, or they may have enrolled to a different instance. By inserting the enrollment
631            crate::course_instance_enrollments::insert_enrollment_and_set_as_current(
632                &mut tx,
633                NewCourseInstanceEnrollment {
634                    user_id: completion_receiver.id,
635                    course_id: course.id,
636                    course_instance_id: course_instance.id,
637                },
638            )
639            .await?;
640
641            update_module_completion_prerequisite_statuses_for_user(
642                &mut tx,
643                completion_receiver.id,
644                course.id,
645                course
646                    .base_module_completion_requires_n_submodule_completions
647                    .try_into()?,
648            )
649            .await?;
650        }
651
652        // Adding a manual completion vouches for the student, so any cheating suspicion for this
653        // course (flagged or confirmed) is cleared and any grade a confirmation had failed is
654        // restored. This runs regardless of `skip_duplicate_completions` (a teacher vouching for an
655        // already-completed student should still clear the flag) and regardless of the completion's
656        // grade (recording any manual completion defers the decision to the teacher). The existence
657        // check is needed because `dismiss_...` errors when no suspicion row exists.
658        if suspected_cheaters::get_by_user_id_and_course_id(&mut tx, completion.user_id, course.id)
659            .await
660            .optional()?
661            .is_some()
662        {
663            suspected_cheaters::dismiss_by_user_id_and_course_id(
664                &mut tx,
665                completion.user_id,
666                course.id,
667            )
668            .await?;
669        }
670    }
671    tx.commit().await?;
672    Ok(())
673}
674
675#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
676
677pub struct ManualCompletionPreview {
678    pub already_completed_users: Vec<ManualCompletionPreviewUser>,
679    pub first_time_completing_users: Vec<ManualCompletionPreviewUser>,
680    pub non_enrolled_users: Vec<ManualCompletionPreviewUser>,
681}
682
683#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
684
685pub struct ManualCompletionPreviewUser {
686    pub user_id: Uuid,
687    pub first_name: Option<String>,
688    pub last_name: Option<String>,
689    pub grade: Option<i32>,
690    pub passed: bool,
691    pub previous_best_grade: Option<i32>,
692}
693
694/// Gets a preview of changes that will occur to completions with the given manual completion data.
695pub async fn get_manual_completion_result_preview(
696    conn: &mut PgConnection,
697    course_instance: &CourseInstance,
698    manual_completion_request: &TeacherManualCompletionRequest,
699) -> ModelResult<ManualCompletionPreview> {
700    let course_module =
701        course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
702    if course_module.course_id != course_instance.course_id {
703        return Err(ModelError::new(
704            ModelErrorType::PreconditionFailed,
705            "Course module not part of the course.".to_string(),
706            None,
707        ));
708    }
709    let mut already_completed_users = vec![];
710    let mut first_time_completing_users = vec![];
711    let mut non_enrolled_users = vec![];
712    for completion in manual_completion_request.new_completions.iter() {
713        let user = users::get_by_id(conn, completion.user_id).await?;
714        let user_details = crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
715        let user = ManualCompletionPreviewUser {
716            user_id: user.id,
717            first_name: user_details.first_name,
718            last_name: user_details.last_name,
719            grade: completion.grade,
720            passed: completion.passed,
721            previous_best_grade: None,
722        };
723        let enrollment = course_instance_enrollments::get_by_user_and_course_instance_id(
724            conn,
725            completion.user_id,
726            course_instance.id,
727        )
728        .await
729        .optional()?;
730        if enrollment.is_none() {
731            non_enrolled_users.push(user.clone());
732        }
733        let module_completed = course_module_completions::user_has_completed_course_module(
734            conn,
735            completion.user_id,
736            manual_completion_request.course_module_id,
737        )
738        .await?;
739        if module_completed {
740            already_completed_users.push(user);
741        } else {
742            first_time_completing_users.push(user);
743        }
744    }
745    Ok(ManualCompletionPreview {
746        already_completed_users,
747        first_time_completing_users,
748        non_enrolled_users,
749    })
750}
751
752#[derive(Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)]
753
754pub struct UserCompletionInformation {
755    pub course_module_completion_id: Uuid,
756    /// The course's own name, never the module's; the module is named by `course_module_name`.
757    pub course_name: String,
758    /// The module's own name, `None` on a course's default module.
759    pub course_module_name: Option<String>,
760    pub uh_course_code: Option<String>,
761    /// The module's open university enrolment page: the same link the credit registration carries,
762    /// here so the page has it before the registration exists.
763    pub enrolment_link: Option<String>,
764    pub email: String,
765    pub ects_credits: Option<f32>,
766    /// Whether the module has a course code, without which its completions are never registered.
767    pub registers_credits: bool,
768    /// Whether this completion goes through the push path, which decides the flow the page shows.
769    pub register_credits_via_suotar: bool,
770    /// What the student is told while the completion has no credit registration: `sending` when
771    /// the push path will create one, `not_registering` when it never will.
772    pub status_before_registration: StudentFacingCreditRegistrationStatus,
773    /// `Some` only when the student can generate a certificate for this module right now, which is
774    /// also the id `/generate-certificate` wants.
775    ///
776    /// Read off the same completion as the rest of this object, the one
777    /// [`course_module_completions::select_registration_completion`] picks. The course page's
778    /// congratulations card reads off the best one instead, so a student whose registration
779    /// completion is not their best can see a certificate there and none here.
780    pub certificate_configuration_id: Option<Uuid>,
781    /// Why the student said they need the credits rather than a certificate, if they have been
782    /// asked and answered. Advisory; it seeds the field when they come back to the page.
783    pub credit_justification: Option<String>,
784}
785
786/// The default certificate configuration a completion of this module alone earns.
787///
788/// `configurations` must come from
789/// [`crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course`],
790/// which has already dropped the configurations that require more than one module; a configuration
791/// merely naming this module among several is not one this module satisfies.
792fn default_certificate_configuration_id_for_module(
793    configurations: &[crate::certificate_configurations::CertificateConfigurationAndRequirements],
794    course_module_id: Uuid,
795) -> Option<Uuid> {
796    configurations
797        .iter()
798        .find(|configuration| {
799            configuration
800                .requirements
801                .course_module_ids
802                .contains(&course_module_id)
803        })
804        .map(|configuration| configuration.certificate_configuration.id)
805}
806
807pub async fn get_user_completion_information(
808    conn: &mut PgConnection,
809    user_id: Uuid,
810    course_module: &CourseModule,
811) -> ModelResult<UserCompletionInformation> {
812    let user = users::get_by_id(conn, user_id).await?;
813    let course = courses::get_course(conn, course_module.course_id).await?;
814    let course_module_completion =
815        course_module_completions::get_registration_completion_by_user_and_course_module_id(
816            conn,
817            user.id,
818            course_module.id,
819        )
820        .await?;
821    // A completion still awaiting review is treated as if it were not there, so a flagged student
822    // cannot read their own flag off the certificate the page would otherwise offer them.
823    let certificate_configuration_id = if course_module.certification_enabled
824        && course_module_completion.passed
825        && !course_module_completion.needs_to_be_reviewed
826    {
827        let configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(
828            conn,
829            course_module.course_id,
830        )
831        .await?;
832        let configuration_id =
833            default_certificate_configuration_id_for_module(&configurations, course_module.id);
834        if configuration_id.is_none() {
835            debug!(
836                course_module_id = ?course_module.id,
837                "Certification is enabled for the module but no default certificate configuration names it, so the student is offered no certificate."
838            );
839        }
840        configuration_id
841    } else {
842        None
843    };
844    let credit_justification =
845        crate::completion_registration_credit_justifications::get_by_completion_id(
846            conn,
847            course_module_completion.id,
848        )
849        .await?
850        .map(|row| row.justification);
851    let status_before_registration =
852        if course_module_completions::get_credit_registration_expected_ids(
853            conn,
854            &[course_module_completion.id],
855        )
856        .await?
857        .is_empty()
858        {
859            StudentFacingCreditRegistrationStatus::NotRegistering
860        } else {
861            StudentFacingCreditRegistrationStatus::Sending
862        };
863    Ok(UserCompletionInformation {
864        course_module_completion_id: course_module_completion.id,
865        course_name: course.name.clone(),
866        course_module_name: course_module.name.clone(),
867        uh_course_code: course_module.uh_course_code.clone(),
868        enrolment_link: course_module
869            .completion_registration_link_override
870            .as_deref()
871            .map(str::trim)
872            .filter(|link| !link.is_empty())
873            .map(str::to_owned),
874        ects_credits: course_module.ects_credits,
875        email: course_module_completion.email,
876        registers_credits: course_modules::registers_credits(
877            course_module.uh_course_code.as_deref(),
878            course_module.completions_can_be_registered_to_sisu,
879        ),
880        register_credits_via_suotar: course_module_completion.register_credits_via_suotar,
881        status_before_registration,
882        certificate_configuration_id,
883        credit_justification,
884    })
885}
886
887#[derive(Clone, PartialEq, Deserialize, Serialize, ToSchema)]
888
889pub struct UserModuleCompletionStatus {
890    pub completed: bool,
891    pub default: bool,
892    pub module_id: Uuid,
893    pub name: String,
894    pub order_number: i32,
895    pub prerequisite_modules_completed: bool,
896    pub grade: Option<i32>,
897    pub passed: Option<bool>,
898    /// Whether the module has a course code, without which its completions are never registered.
899    pub registers_credits: bool,
900    /// Whether the module's registration flow is the push path, decided by
901    /// [`course_module_completions::select_registration_completion`] rather than by the shown
902    /// completion. False when no completion is shown.
903    pub register_credits_via_suotar: bool,
904    pub certification_enabled: bool,
905    pub certificate_configuration_id: Option<Uuid>,
906}
907
908/// Gets course modules with user's completion status for the given instance.
909pub async fn get_user_module_completion_statuses_for_course(
910    conn: &mut PgConnection,
911    user_id: Uuid,
912    course_id: Uuid,
913) -> ModelResult<Vec<UserModuleCompletionStatus>> {
914    let course = courses::get_course(conn, course_id).await?;
915    let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
916
917    let course_module_completions_raw =
918        course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
919            .await?;
920
921    let completions_by_module: HashMap<Uuid, Vec<CourseModuleCompletion>> =
922        course_module_completions_raw
923            .into_iter()
924            .into_group_map_by(|c| c.course_module_id);
925    // A completion that still needs review (e.g. because the student was auto-flagged as a
926    // suspected cheater) is hidden from the student, as if it did not exist yet, so a flagged
927    // student cannot infer from the API that they are under suspicion.
928    let best_visible_completions: HashMap<Uuid, CourseModuleCompletion> = completions_by_module
929        .iter()
930        .filter_map(|(module_id, completions)| {
931            course_module_completions::select_best_completion(
932                completions.iter().filter(|c| !c.needs_to_be_reviewed),
933            )
934            .map(|best| (*module_id, best.clone()))
935        })
936        .collect();
937    let registers_via_suotar_module_ids: HashSet<Uuid> = completions_by_module
938        .into_iter()
939        .filter_map(|(module_id, completions)| {
940            course_module_completions::select_registration_completion(completions)
941                .filter(|c| c.register_credits_via_suotar)
942                .map(|_| module_id)
943        })
944        .collect();
945
946    let all_default_certificate_configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(conn, course_id).await?;
947
948    let course_module_completion_statuses = course_modules
949        .into_iter()
950        .map(|module| {
951            let completion = best_visible_completions.get(&module.id);
952            let passed = completion.map(|x| x.passed);
953            let certificate_configuration_id =
954                if module.certification_enabled && passed == Some(true) {
955                    default_certificate_configuration_id_for_module(
956                        &all_default_certificate_configurations,
957                        module.id,
958                    )
959                } else {
960                    None
961                };
962            UserModuleCompletionStatus {
963                completed: completion.is_some(),
964                default: module.is_default_module(),
965                module_id: module.id,
966                name: module.name.unwrap_or_else(|| course.name.clone()),
967                order_number: module.order_number,
968                passed,
969                grade: completion.and_then(|x| x.grade),
970                prerequisite_modules_completed: completion
971                    .is_some_and(|x| x.prerequisite_modules_completed),
972                registers_credits: course_modules::registers_credits(
973                    module.uh_course_code.as_deref(),
974                    module.completions_can_be_registered_to_sisu,
975                ),
976                register_credits_via_suotar: completion.is_some()
977                    && registers_via_suotar_module_ids.contains(&module.id),
978                certification_enabled: module.certification_enabled,
979                certificate_configuration_id,
980            }
981        })
982        .collect();
983    Ok(course_module_completion_statuses)
984}
985
986#[derive(Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)]
987
988pub struct CompletionRegistrationLink {
989    pub url: String,
990}
991
992pub async fn get_completion_registration_link_and_save_attempt(
993    conn: &mut PgConnection,
994    user_id: Uuid,
995    course_module: &CourseModule,
996) -> ModelResult<CompletionRegistrationLink> {
997    if !course_modules::registers_credits(
998        course_module.uh_course_code.as_deref(),
999        course_module.completions_can_be_registered_to_sisu,
1000    ) {
1001        return Err(ModelError::new(
1002            ModelErrorType::InvalidRequest,
1003            "Completion registration is not enabled for this course module.".to_string(),
1004            None,
1005        ));
1006    }
1007    let user = users::get_by_id(conn, user_id).await?;
1008
1009    let course_module_completion =
1010        course_module_completions::get_registration_completion_by_user_and_course_module_id(
1011            conn,
1012            user.id,
1013            course_module.id,
1014        )
1015        .await?;
1016    // Same condition as the registration page's choice of flow.
1017    if course_module_completion.register_credits_via_suotar {
1018        return Err(model_err!(
1019            InvalidRequest,
1020            "This completion is registered through the study registry.".to_string()
1021        ));
1022    }
1023    course_module_completions::update_completion_registration_attempt_date(
1024        conn,
1025        course_module_completion.id,
1026        Utc::now(),
1027    )
1028    .await?;
1029    let registration_link = if let Some(link_override) = course_module
1030        .completion_registration_link_override
1031        .as_deref()
1032        .map(str::trim)
1033        .filter(|link| !link.is_empty())
1034    {
1035        link_override.to_owned()
1036    } else {
1037        let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
1038            ModelError::new(
1039                ModelErrorType::PreconditionFailed,
1040                "Course module doesn't have an assossiated University of Helsinki course code."
1041                    .to_string(),
1042                None,
1043            )
1044        })?;
1045        open_university_registration_links::get_link_by_course_code(conn, &uh_course_code).await?
1046    };
1047
1048    Ok(CompletionRegistrationLink {
1049        url: registration_link,
1050    })
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use chrono::Duration;
1056    use user_exercise_states::{ReviewingStage, UserExerciseStateUpdate};
1057
1058    use super::*;
1059
1060    use crate::{
1061        exercises::{ActivityProgress, GradingProgress},
1062        suspected_cheaters::SuspectedCheaterStatus,
1063        test_helper::*,
1064    };
1065
1066    mod user_completion_information {
1067        use super::*;
1068        use crate::{
1069            certificate_configuration_to_requirements,
1070            certificate_configurations::{self, DatabaseCertificateConfiguration},
1071            file_uploads,
1072        };
1073
1074        const UH_COURSE_CODE: &str = "TKT00000";
1075
1076        /// A module the old registration page can actually be opened for: it needs a course code,
1077        /// and the certificate the detour offers needs certification switched on.
1078        async fn registrable_module(
1079            conn: &mut PgConnection,
1080            course_module_id: Uuid,
1081            certification_enabled: bool,
1082            user_id: Uuid,
1083        ) -> CourseModule {
1084            course_modules::update_certification_enabled(
1085                conn,
1086                course_module_id,
1087                certification_enabled,
1088                user_id,
1089            )
1090            .await
1091            .unwrap();
1092            course_modules::update_uh_course_code(
1093                conn,
1094                course_module_id,
1095                Some(UH_COURSE_CODE.to_string()),
1096            )
1097            .await
1098            .unwrap()
1099        }
1100
1101        async fn complete(
1102            conn: &mut PgConnection,
1103            course_id: Uuid,
1104            course_module_id: Uuid,
1105            user_id: Uuid,
1106            passed: bool,
1107        ) -> CourseModuleCompletion {
1108            course_module_completions::insert(
1109                conn,
1110                PKeyPolicy::Generate,
1111                &NewCourseModuleCompletion {
1112                    course_id,
1113                    course_module_id,
1114                    user_id,
1115                    completion_date: Utc::now(),
1116                    completion_registration_attempt_date: None,
1117                    completion_language: "en-US".to_string(),
1118                    eligible_for_ects: true,
1119                    email: "student@example.com".to_string(),
1120                    grade: None,
1121                    passed,
1122                },
1123                CourseModuleCompletionGranter::Automatic,
1124            )
1125            .await
1126            .unwrap()
1127        }
1128
1129        /// A certificate configuration requiring exactly the given modules. One module makes it a
1130        /// default configuration; more than one makes it the kind no single module earns.
1131        async fn certificate_configuration_requiring(
1132            conn: &mut PgConnection,
1133            course_module_ids: &[Uuid],
1134        ) -> Uuid {
1135            let background_svg_file_upload_id = file_uploads::insert(
1136                conn,
1137                "background.svg",
1138                "certificates/background.svg",
1139                "image/svg+xml",
1140                None,
1141                None,
1142            )
1143            .await
1144            .unwrap();
1145            let configuration = certificate_configurations::insert(
1146                conn,
1147                &DatabaseCertificateConfiguration {
1148                    id: Uuid::new_v4(),
1149                    certificate_owner_name_y_pos: None,
1150                    certificate_owner_name_x_pos: None,
1151                    certificate_owner_name_font_size: None,
1152                    certificate_owner_name_text_color: None,
1153                    certificate_owner_name_text_anchor: None,
1154                    certificate_validate_url_y_pos: None,
1155                    certificate_validate_url_x_pos: None,
1156                    certificate_validate_url_font_size: None,
1157                    certificate_validate_url_text_color: None,
1158                    certificate_validate_url_text_anchor: None,
1159                    certificate_date_y_pos: None,
1160                    certificate_date_x_pos: None,
1161                    certificate_date_font_size: None,
1162                    certificate_date_text_color: None,
1163                    certificate_date_text_anchor: None,
1164                    certificate_locale: None,
1165                    paper_size: None,
1166                    background_svg_path: "certificates/background.svg".to_string(),
1167                    background_svg_file_upload_id,
1168                    overlay_svg_path: None,
1169                    overlay_svg_file_upload_id: None,
1170                    render_certificate_grade: false,
1171                    certificate_grade_y_pos: None,
1172                    certificate_grade_x_pos: None,
1173                    certificate_grade_font_size: None,
1174                    certificate_grade_text_color: None,
1175                    certificate_grade_text_anchor: None,
1176                },
1177            )
1178            .await
1179            .unwrap();
1180            for course_module_id in course_module_ids {
1181                certificate_configuration_to_requirements::insert(
1182                    conn,
1183                    configuration.id,
1184                    Some(*course_module_id),
1185                )
1186                .await
1187                .unwrap();
1188            }
1189            configuration.id
1190        }
1191
1192        #[tokio::test]
1193        async fn offers_the_certificate_a_passed_completion_has_earned() {
1194            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1195            let module = registrable_module(tx.as_mut(), course_module.id, true, user).await;
1196            complete(tx.as_mut(), course, module.id, user, true).await;
1197            let configuration_id =
1198                certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1199
1200            let information = get_user_completion_information(tx.as_mut(), user, &module)
1201                .await
1202                .unwrap();
1203
1204            assert_eq!(
1205                information.certificate_configuration_id,
1206                Some(configuration_id)
1207            );
1208        }
1209
1210        #[tokio::test]
1211        async fn offers_no_certificate_when_the_module_has_certification_off() {
1212            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1213            let module = registrable_module(tx.as_mut(), course_module.id, false, user).await;
1214            complete(tx.as_mut(), course, module.id, user, true).await;
1215            certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1216
1217            let information = get_user_completion_information(tx.as_mut(), user, &module)
1218                .await
1219                .unwrap();
1220
1221            assert_eq!(information.certificate_configuration_id, None);
1222        }
1223
1224        #[tokio::test]
1225        async fn offers_no_certificate_for_a_failed_completion() {
1226            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1227            let module = registrable_module(tx.as_mut(), course_module.id, true, user).await;
1228            complete(tx.as_mut(), course, module.id, user, false).await;
1229            certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1230
1231            let information = get_user_completion_information(tx.as_mut(), user, &module)
1232                .await
1233                .unwrap();
1234
1235            assert_eq!(information.certificate_configuration_id, None);
1236        }
1237
1238        #[tokio::test]
1239        async fn offers_no_certificate_while_the_completion_is_under_review() {
1240            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1241            let module = registrable_module(tx.as_mut(), course_module.id, true, user).await;
1242            let completion = complete(tx.as_mut(), course, module.id, user, true).await;
1243            certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1244            course_module_completions::update_needs_to_be_reviewed(
1245                tx.as_mut(),
1246                completion.id,
1247                true,
1248            )
1249            .await
1250            .unwrap();
1251
1252            let information = get_user_completion_information(tx.as_mut(), user, &module)
1253                .await
1254                .unwrap();
1255
1256            assert_eq!(information.certificate_configuration_id, None);
1257        }
1258
1259        #[tokio::test]
1260        async fn offers_no_certificate_this_module_alone_does_not_earn() {
1261            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1262            let module = registrable_module(tx.as_mut(), course_module.id, true, user).await;
1263            let other_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1264                .await
1265                .unwrap();
1266            complete(tx.as_mut(), course, module.id, user, true).await;
1267            certificate_configuration_requiring(tx.as_mut(), &[module.id, other_module.id]).await;
1268
1269            let information = get_user_completion_information(tx.as_mut(), user, &module)
1270                .await
1271                .unwrap();
1272
1273            assert_eq!(information.certificate_configuration_id, None);
1274        }
1275
1276        #[tokio::test]
1277        async fn carries_back_the_answer_the_student_already_gave() {
1278            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1279            let module = registrable_module(tx.as_mut(), course_module.id, true, user).await;
1280            let completion = complete(tx.as_mut(), course, module.id, user, true).await;
1281            crate::completion_registration_credit_justifications::upsert(
1282                tx.as_mut(),
1283                completion.id,
1284                user,
1285                "My employer needs them in the registry.",
1286            )
1287            .await
1288            .unwrap();
1289
1290            let information = get_user_completion_information(tx.as_mut(), user, &module)
1291                .await
1292                .unwrap();
1293
1294            assert_eq!(
1295                information.credit_justification.as_deref(),
1296                Some("My employer needs them in the registry.")
1297            );
1298        }
1299    }
1300
1301    mod grant_automatic_completion_if_eligible {
1302        use super::*;
1303        use crate::{
1304            chapters::NewChapter,
1305            course_modules::{
1306                self, AutomaticCompletionRequirements, CompletionPolicy, NewCourseModule,
1307            },
1308            exercises::{self, ActivityProgress, GradingProgress},
1309            library::content_management,
1310            user_exercise_states::{self, ReviewingStage, UserExerciseStateUpdate},
1311        };
1312
1313        #[tokio::test]
1314        async fn grants_automatic_completion_but_no_prerequisite_for_default_module() {
1315            insert_data!(:tx);
1316            let (mut tx, user, course, _instance, default_module, _submodule_1, _submodule_2) =
1317                create_test_data(tx).await;
1318            update_automatic_completion_status_and_grant_if_eligible(
1319                tx.as_mut(),
1320                &default_module,
1321                user,
1322            )
1323            .await
1324            .unwrap();
1325            let statuses =
1326                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1327                    .await
1328                    .unwrap();
1329            let status = statuses
1330                .iter()
1331                .find(|x| x.module_id == default_module.id)
1332                .unwrap();
1333            assert!(status.completed);
1334            assert!(!status.prerequisite_modules_completed);
1335        }
1336
1337        #[tokio::test]
1338        async fn grants_automatic_completion_but_no_prerequisite_for_submodule() {
1339            insert_data!(:tx);
1340            let (mut tx, user, course, _instance, _default_module, submodule_1, _submodule_2) =
1341                create_test_data(tx).await;
1342            update_automatic_completion_status_and_grant_if_eligible(
1343                tx.as_mut(),
1344                &submodule_1,
1345                user,
1346            )
1347            .await
1348            .unwrap();
1349            let statuses =
1350                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1351                    .await
1352                    .unwrap();
1353            let status = statuses
1354                .iter()
1355                .find(|x| x.module_id == submodule_1.id)
1356                .unwrap();
1357            assert!(status.completed);
1358            assert!(!status.prerequisite_modules_completed);
1359        }
1360
1361        #[tokio::test]
1362        async fn grants_automatic_completion_for_eligible_submodule_when_completing_default_module()
1363        {
1364            insert_data!(:tx);
1365            let (mut tx, user, course, _instance, default_module, submodule_1, submodule_2) =
1366                create_test_data(tx).await;
1367            update_automatic_completion_status_and_grant_if_eligible(
1368                tx.as_mut(),
1369                &default_module,
1370                user,
1371            )
1372            .await
1373            .unwrap();
1374            update_automatic_completion_status_and_grant_if_eligible(
1375                tx.as_mut(),
1376                &submodule_1,
1377                user,
1378            )
1379            .await
1380            .unwrap();
1381            update_automatic_completion_status_and_grant_if_eligible(
1382                tx.as_mut(),
1383                &submodule_2,
1384                user,
1385            )
1386            .await
1387            .unwrap();
1388            let statuses =
1389                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1390                    .await
1391                    .unwrap();
1392            statuses.iter().for_each(|x| {
1393                assert!(x.completed);
1394                assert!(x.prerequisite_modules_completed);
1395            });
1396        }
1397
1398        async fn create_test_data(
1399            mut tx: Tx<'_>,
1400        ) -> (
1401            Tx<'_>,
1402            Uuid,
1403            Uuid,
1404            Uuid,
1405            CourseModule,
1406            CourseModule,
1407            CourseModule,
1408        ) {
1409            insert_data!(tx: tx; :user, :org, :course, :instance, :course_module, :chapter, :page, :exercise);
1410            // These tests complete modules instantly, which would trip suspected-cheater detection
1411            // (on by default) and hide the completion. Detection is exercised by its own tests, so
1412            // disable it here to test automatic-completion granting in isolation.
1413            courses::set_cheater_detection_enabled(tx.as_mut(), course, false)
1414                .await
1415                .unwrap();
1416            let automatic_completion_policy =
1417                CompletionPolicy::Automatic(AutomaticCompletionRequirements {
1418                    course_module_id: course_module.id,
1419                    number_of_exercises_attempted_treshold: Some(0),
1420                    number_of_points_treshold: Some(0),
1421                    requires_exam: false,
1422                });
1423            courses::update_course_base_module_completion_count_requirement(tx.as_mut(), course, 1)
1424                .await
1425                .unwrap();
1426            let course_module_2 = course_modules::insert(
1427                tx.as_mut(),
1428                PKeyPolicy::Generate,
1429                &NewCourseModule::new(course, Some("Module 2".to_string()), 1),
1430            )
1431            .await
1432            .unwrap();
1433            let (chapter_2, page2) = content_management::create_new_chapter(
1434                tx.as_mut(),
1435                PKeyPolicy::Generate,
1436                &NewChapter {
1437                    name: "chapter 2".to_string(),
1438                    color: None,
1439                    course_id: course,
1440                    chapter_number: 2,
1441                    front_page_id: None,
1442                    opens_at: None,
1443                    deadline: None,
1444                    course_module_id: Some(course_module_2.id),
1445                },
1446                user,
1447                |_, _, _| unimplemented!(),
1448                |_| unimplemented!(),
1449            )
1450            .await
1451            .unwrap();
1452
1453            let exercise_2 = exercises::insert(
1454                tx.as_mut(),
1455                PKeyPolicy::Generate,
1456                course,
1457                "",
1458                page2.id,
1459                chapter_2.id,
1460                0,
1461            )
1462            .await
1463            .unwrap();
1464            let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
1465                tx.as_mut(),
1466                user,
1467                exercise,
1468                Some(course),
1469                None,
1470            )
1471            .await
1472            .unwrap();
1473            user_exercise_states::update(
1474                tx.as_mut(),
1475                UserExerciseStateUpdate {
1476                    id: user_exercise_state.id,
1477                    score_given: Some(0.0),
1478                    activity_progress: ActivityProgress::Completed,
1479                    reviewing_stage: ReviewingStage::NotStarted,
1480                    grading_progress: GradingProgress::FullyGraded,
1481                },
1482            )
1483            .await
1484            .unwrap();
1485            let user_exercise_state_2 = user_exercise_states::get_or_create_user_exercise_state(
1486                tx.as_mut(),
1487                user,
1488                exercise_2,
1489                Some(course),
1490                None,
1491            )
1492            .await
1493            .unwrap();
1494            user_exercise_states::update(
1495                tx.as_mut(),
1496                UserExerciseStateUpdate {
1497                    id: user_exercise_state_2.id,
1498                    score_given: Some(0.0),
1499                    activity_progress: ActivityProgress::Completed,
1500                    reviewing_stage: ReviewingStage::NotStarted,
1501                    grading_progress: GradingProgress::FullyGraded,
1502                },
1503            )
1504            .await
1505            .unwrap();
1506            let default_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1507                .await
1508                .unwrap();
1509            let default_module = course_modules::update_automatic_completion_status(
1510                tx.as_mut(),
1511                default_module.id,
1512                &automatic_completion_policy,
1513            )
1514            .await
1515            .unwrap();
1516            let course_module = course_modules::update_automatic_completion_status(
1517                tx.as_mut(),
1518                course_module.id,
1519                &automatic_completion_policy,
1520            )
1521            .await
1522            .unwrap();
1523            let course_module_2 = course_modules::update_automatic_completion_status(
1524                tx.as_mut(),
1525                course_module_2.id,
1526                &automatic_completion_policy,
1527            )
1528            .await
1529            .unwrap();
1530            (
1531                tx,
1532                user,
1533                course,
1534                instance.id,
1535                default_module,
1536                course_module,
1537                course_module_2,
1538            )
1539        }
1540    }
1541
1542    #[tokio::test]
1543    async fn tags_suspected_cheater() {
1544        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1545
1546        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1547            .await
1548            .unwrap();
1549        let state = user_exercise_states::get_or_create_user_exercise_state(
1550            tx.as_mut(),
1551            user,
1552            exercise,
1553            Some(course),
1554            None,
1555        )
1556        .await
1557        .unwrap();
1558        user_exercise_states::update(
1559            tx.as_mut(),
1560            UserExerciseStateUpdate {
1561                id: state.id,
1562                score_given: Some(10.0),
1563                activity_progress: ActivityProgress::Completed,
1564                reviewing_stage: ReviewingStage::NotStarted,
1565                grading_progress: GradingProgress::FullyGraded,
1566            },
1567        )
1568        .await
1569        .unwrap();
1570
1571        let completion = course_module_completions::insert(
1572            tx.as_mut(),
1573            PKeyPolicy::Generate,
1574            &NewCourseModuleCompletion {
1575                course_id: course,
1576                course_module_id: course_module.id,
1577                user_id: user,
1578                completion_date: Utc::now() + Duration::days(1),
1579                completion_registration_attempt_date: None,
1580                completion_language: "en-US".to_string(),
1581                eligible_for_ects: false,
1582                email: "email".to_string(),
1583                grade: None,
1584                passed: true,
1585            },
1586            CourseModuleCompletionGranter::Automatic,
1587        )
1588        .await
1589        .unwrap();
1590        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1591            tx.as_mut(),
1592            course_module.id,
1593            259200,
1594        )
1595        .await
1596        .unwrap();
1597        check_and_insert_suspected_cheaters(
1598            tx.as_mut(),
1599            user,
1600            course,
1601            thresholds.duration_seconds,
1602            completion,
1603        )
1604        .await
1605        .unwrap();
1606
1607        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1608            tx.as_mut(),
1609            course,
1610            SuspectedCheaterStatus::Flagged,
1611        )
1612        .await
1613        .unwrap();
1614        assert_eq!(cheaters.len(), 1);
1615        assert_eq!(cheaters[0].user_id, user);
1616    }
1617
1618    #[tokio::test]
1619    async fn tagging_suspected_cheater_is_idempotent() {
1620        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1621
1622        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1623            .await
1624            .unwrap();
1625        let state = user_exercise_states::get_or_create_user_exercise_state(
1626            tx.as_mut(),
1627            user,
1628            exercise,
1629            Some(course),
1630            None,
1631        )
1632        .await
1633        .unwrap();
1634        user_exercise_states::update(
1635            tx.as_mut(),
1636            UserExerciseStateUpdate {
1637                id: state.id,
1638                score_given: Some(10.0),
1639                activity_progress: ActivityProgress::Completed,
1640                reviewing_stage: ReviewingStage::NotStarted,
1641                grading_progress: GradingProgress::FullyGraded,
1642            },
1643        )
1644        .await
1645        .unwrap();
1646
1647        let completion = course_module_completions::insert(
1648            tx.as_mut(),
1649            PKeyPolicy::Generate,
1650            &NewCourseModuleCompletion {
1651                course_id: course,
1652                course_module_id: course_module.id,
1653                user_id: user,
1654                completion_date: Utc::now() + Duration::days(1),
1655                completion_registration_attempt_date: None,
1656                completion_language: "en-US".to_string(),
1657                eligible_for_ects: false,
1658                email: "email".to_string(),
1659                grade: None,
1660                passed: true,
1661            },
1662            CourseModuleCompletionGranter::Automatic,
1663        )
1664        .await
1665        .unwrap();
1666        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1667            tx.as_mut(),
1668            course_module.id,
1669            259200,
1670        )
1671        .await
1672        .unwrap();
1673        check_and_insert_suspected_cheaters(
1674            tx.as_mut(),
1675            user,
1676            course,
1677            thresholds.duration_seconds,
1678            completion.clone(),
1679        )
1680        .await
1681        .unwrap();
1682        check_and_insert_suspected_cheaters(
1683            tx.as_mut(),
1684            user,
1685            course,
1686            thresholds.duration_seconds,
1687            completion,
1688        )
1689        .await
1690        .unwrap();
1691
1692        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1693            tx.as_mut(),
1694            course,
1695            SuspectedCheaterStatus::Flagged,
1696        )
1697        .await
1698        .unwrap();
1699        assert_eq!(cheaters.len(), 1);
1700        let completion_needing_review =
1701            course_module_completions::get_latest_by_course_and_user_ids(
1702                tx.as_mut(),
1703                course_module.id,
1704                user,
1705            )
1706            .await
1707            .unwrap();
1708        assert!(completion_needing_review.needs_to_be_reviewed);
1709
1710        suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1711            .await
1712            .unwrap();
1713        let archived_completion = course_module_completions::get_latest_by_course_and_user_ids(
1714            tx.as_mut(),
1715            course_module.id,
1716            user,
1717        )
1718        .await
1719        .unwrap();
1720        assert!(!archived_completion.needs_to_be_reviewed);
1721        check_and_insert_suspected_cheaters(
1722            tx.as_mut(),
1723            user,
1724            course,
1725            thresholds.duration_seconds,
1726            archived_completion,
1727        )
1728        .await
1729        .unwrap();
1730
1731        let visible_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1732            tx.as_mut(),
1733            course,
1734            SuspectedCheaterStatus::Flagged,
1735        )
1736        .await
1737        .unwrap();
1738        let archived_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1739            tx.as_mut(),
1740            course,
1741            SuspectedCheaterStatus::Dismissed,
1742        )
1743        .await
1744        .unwrap();
1745        assert!(visible_cheaters.is_empty());
1746        assert_eq!(archived_cheaters.len(), 1);
1747        let rechecked_completion = course_module_completions::get_latest_by_course_and_user_ids(
1748            tx.as_mut(),
1749            course_module.id,
1750            user,
1751        )
1752        .await
1753        .unwrap();
1754        assert!(!rechecked_completion.needs_to_be_reviewed);
1755    }
1756
1757    #[tokio::test]
1758    async fn confirming_then_dismissing_restores_grade() {
1759        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1760
1761        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1762            .await
1763            .unwrap();
1764        let state = user_exercise_states::get_or_create_user_exercise_state(
1765            tx.as_mut(),
1766            user,
1767            exercise,
1768            Some(course),
1769            None,
1770        )
1771        .await
1772        .unwrap();
1773        user_exercise_states::update(
1774            tx.as_mut(),
1775            UserExerciseStateUpdate {
1776                id: state.id,
1777                score_given: Some(10.0),
1778                activity_progress: ActivityProgress::Completed,
1779                reviewing_stage: ReviewingStage::NotStarted,
1780                grading_progress: GradingProgress::FullyGraded,
1781            },
1782        )
1783        .await
1784        .unwrap();
1785
1786        // A graded, passing completion so we can prove the exact grade is restored, not just pass/fail.
1787        let completion = course_module_completions::insert(
1788            tx.as_mut(),
1789            PKeyPolicy::Generate,
1790            &NewCourseModuleCompletion {
1791                course_id: course,
1792                course_module_id: course_module.id,
1793                user_id: user,
1794                completion_date: Utc::now() + Duration::days(1),
1795                completion_registration_attempt_date: None,
1796                completion_language: "en-US".to_string(),
1797                eligible_for_ects: false,
1798                email: "email".to_string(),
1799                grade: Some(4),
1800                passed: true,
1801            },
1802            CourseModuleCompletionGranter::Automatic,
1803        )
1804        .await
1805        .unwrap();
1806        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1807            tx.as_mut(),
1808            course_module.id,
1809            259200,
1810        )
1811        .await
1812        .unwrap();
1813        check_and_insert_suspected_cheaters(
1814            tx.as_mut(),
1815            user,
1816            course,
1817            thresholds.duration_seconds,
1818            completion,
1819        )
1820        .await
1821        .unwrap();
1822
1823        // Confirm: the student is failed and the previous grade is snapshotted.
1824        suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
1825            .await
1826            .unwrap();
1827        let failed = course_module_completions::get_latest_by_course_and_user_ids(
1828            tx.as_mut(),
1829            course_module.id,
1830            user,
1831        )
1832        .await
1833        .unwrap();
1834        assert!(!failed.passed);
1835        assert_eq!(failed.grade, Some(0));
1836        let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1837            tx.as_mut(),
1838            course,
1839            SuspectedCheaterStatus::ConfirmedCheating,
1840        )
1841        .await
1842        .unwrap();
1843        assert_eq!(confirmed.len(), 1);
1844
1845        // Dismiss: the confirmation is undone and the exact previous grade is restored.
1846        suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1847            .await
1848            .unwrap();
1849        let restored = course_module_completions::get_latest_by_course_and_user_ids(
1850            tx.as_mut(),
1851            course_module.id,
1852            user,
1853        )
1854        .await
1855        .unwrap();
1856        assert!(restored.passed);
1857        assert_eq!(restored.grade, Some(4));
1858        assert!(!restored.needs_to_be_reviewed);
1859        let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1860            tx.as_mut(),
1861            course,
1862            SuspectedCheaterStatus::Dismissed,
1863        )
1864        .await
1865        .unwrap();
1866        assert_eq!(dismissed.len(), 1);
1867    }
1868
1869    #[tokio::test]
1870    async fn manual_completion_prevents_flagging() {
1871        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module);
1872
1873        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1874            .await
1875            .unwrap();
1876
1877        // A teacher has manually granted a completion for this student.
1878        let teacher = users::insert(
1879            tx.as_mut(),
1880            PKeyPolicy::Generate,
1881            "teacher-vouching@example.com",
1882            Some("Teacher"),
1883            Some("McVouch"),
1884        )
1885        .await
1886        .unwrap();
1887        course_module_completions::insert(
1888            tx.as_mut(),
1889            PKeyPolicy::Generate,
1890            &NewCourseModuleCompletion {
1891                course_id: course,
1892                course_module_id: course_module.id,
1893                user_id: user,
1894                completion_date: Utc::now(),
1895                completion_registration_attempt_date: None,
1896                completion_language: "en-US".to_string(),
1897                eligible_for_ects: false,
1898                email: "email".to_string(),
1899                grade: Some(5),
1900                passed: true,
1901            },
1902            CourseModuleCompletionGranter::User(teacher),
1903        )
1904        .await
1905        .unwrap();
1906
1907        // An automatic completion well inside the threshold would normally flag the student.
1908        let automatic_completion = course_module_completions::insert(
1909            tx.as_mut(),
1910            PKeyPolicy::Generate,
1911            &NewCourseModuleCompletion {
1912                course_id: course,
1913                course_module_id: course_module.id,
1914                user_id: user,
1915                completion_date: Utc::now() + Duration::days(1),
1916                completion_registration_attempt_date: None,
1917                completion_language: "en-US".to_string(),
1918                eligible_for_ects: false,
1919                email: "email".to_string(),
1920                grade: None,
1921                passed: true,
1922            },
1923            CourseModuleCompletionGranter::Automatic,
1924        )
1925        .await
1926        .unwrap();
1927        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1928            tx.as_mut(),
1929            course_module.id,
1930            259200,
1931        )
1932        .await
1933        .unwrap();
1934        check_and_insert_suspected_cheaters(
1935            tx.as_mut(),
1936            user,
1937            course,
1938            thresholds.duration_seconds,
1939            automatic_completion,
1940        )
1941        .await
1942        .unwrap();
1943
1944        // The teacher's manual completion exempts the student, so no suspicion is created.
1945        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1946            tx.as_mut(),
1947            course,
1948            SuspectedCheaterStatus::Flagged,
1949        )
1950        .await
1951        .unwrap();
1952        assert!(cheaters.is_empty());
1953    }
1954
1955    #[tokio::test]
1956    async fn adding_manual_completion_dismisses_confirmed_suspicion_and_restores_grade() {
1957        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1958
1959        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1960            .await
1961            .unwrap();
1962        let state = user_exercise_states::get_or_create_user_exercise_state(
1963            tx.as_mut(),
1964            user,
1965            exercise,
1966            Some(course),
1967            None,
1968        )
1969        .await
1970        .unwrap();
1971        user_exercise_states::update(
1972            tx.as_mut(),
1973            UserExerciseStateUpdate {
1974                id: state.id,
1975                score_given: Some(10.0),
1976                activity_progress: ActivityProgress::Completed,
1977                reviewing_stage: ReviewingStage::NotStarted,
1978                grading_progress: GradingProgress::FullyGraded,
1979            },
1980        )
1981        .await
1982        .unwrap();
1983
1984        // Flag the student via a fast automatic completion, then confirm cheating (fails the grade).
1985        let completion = course_module_completions::insert(
1986            tx.as_mut(),
1987            PKeyPolicy::Generate,
1988            &NewCourseModuleCompletion {
1989                course_id: course,
1990                course_module_id: course_module.id,
1991                user_id: user,
1992                completion_date: Utc::now() + Duration::days(1),
1993                completion_registration_attempt_date: None,
1994                completion_language: "en-US".to_string(),
1995                eligible_for_ects: false,
1996                email: "email".to_string(),
1997                grade: Some(4),
1998                passed: true,
1999            },
2000            CourseModuleCompletionGranter::Automatic,
2001        )
2002        .await
2003        .unwrap();
2004        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
2005            tx.as_mut(),
2006            course_module.id,
2007            259200,
2008        )
2009        .await
2010        .unwrap();
2011        check_and_insert_suspected_cheaters(
2012            tx.as_mut(),
2013            user,
2014            course,
2015            thresholds.duration_seconds,
2016            completion,
2017        )
2018        .await
2019        .unwrap();
2020        suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
2021            .await
2022            .unwrap();
2023
2024        // A teacher manually adds a completion for the same student. `skip_duplicate_completions` is
2025        // false so the completion is inserted even though the module is already completed.
2026        let teacher = users::insert(
2027            tx.as_mut(),
2028            PKeyPolicy::Generate,
2029            "teacher-vouching@example.com",
2030            Some("Teacher"),
2031            Some("McVouch"),
2032        )
2033        .await
2034        .unwrap();
2035        add_manual_completions(
2036            tx.as_mut(),
2037            teacher,
2038            &instance,
2039            &TeacherManualCompletionRequest {
2040                course_module_id: course_module.id,
2041                new_completions: vec![TeacherManualCompletion {
2042                    user_id: user,
2043                    grade: Some(5),
2044                    passed: true,
2045                    completion_date: None,
2046                }],
2047                skip_duplicate_completions: false,
2048            },
2049        )
2050        .await
2051        .unwrap();
2052
2053        // The suspicion is dismissed and the confirmed-cheating grade failure is undone.
2054        let flagged = suspected_cheaters::get_all_suspected_cheaters_in_course(
2055            tx.as_mut(),
2056            course,
2057            SuspectedCheaterStatus::Flagged,
2058        )
2059        .await
2060        .unwrap();
2061        let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
2062            tx.as_mut(),
2063            course,
2064            SuspectedCheaterStatus::ConfirmedCheating,
2065        )
2066        .await
2067        .unwrap();
2068        let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
2069            tx.as_mut(),
2070            course,
2071            SuspectedCheaterStatus::Dismissed,
2072        )
2073        .await
2074        .unwrap();
2075        assert!(flagged.is_empty());
2076        assert!(confirmed.is_empty());
2077        assert_eq!(dismissed.len(), 1);
2078
2079        // The automatic completion's failed grade is restored and its review flag is cleared.
2080        let restored =
2081            course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
2082                tx.as_mut(),
2083                course_module.id,
2084                course,
2085                user,
2086            )
2087            .await
2088            .unwrap();
2089        assert!(restored.passed);
2090        assert_eq!(restored.grade, Some(4));
2091        assert!(!restored.needs_to_be_reviewed);
2092    }
2093
2094    #[tokio::test]
2095    async fn doesnt_tag_suspected_cheater() {
2096        insert_data!(:tx, user:user, :org, :course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
2097
2098        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
2099            .await
2100            .unwrap();
2101        let state = user_exercise_states::get_or_create_user_exercise_state(
2102            tx.as_mut(),
2103            user,
2104            exercise,
2105            Some(course),
2106            None,
2107        )
2108        .await
2109        .unwrap();
2110        user_exercise_states::update(
2111            tx.as_mut(),
2112            UserExerciseStateUpdate {
2113                id: state.id,
2114                score_given: Some(9.0),
2115                activity_progress: ActivityProgress::Completed,
2116                reviewing_stage: ReviewingStage::NotStarted,
2117                grading_progress: GradingProgress::FullyGraded,
2118            },
2119        )
2120        .await
2121        .unwrap();
2122
2123        course_module_completions::insert(
2124            tx.as_mut(),
2125            PKeyPolicy::Generate,
2126            &NewCourseModuleCompletion {
2127                course_id: course,
2128                course_module_id: course_module.id,
2129                user_id: user,
2130                completion_date: Utc::now() + Duration::days(3),
2131                completion_registration_attempt_date: None,
2132                completion_language: "en-US".to_string(),
2133                eligible_for_ects: false,
2134                email: "email".to_string(),
2135                grade: Some(9),
2136                passed: true,
2137            },
2138            CourseModuleCompletionGranter::Automatic,
2139        )
2140        .await
2141        .unwrap();
2142        suspected_cheaters::insert_thresholds_by_module_id(tx.as_mut(), course_module.id, 172800)
2143            .await
2144            .unwrap();
2145        update_automatic_completion_status_and_grant_if_eligible(tx.as_mut(), &course_module, user)
2146            .await
2147            .unwrap();
2148
2149        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
2150            tx.as_mut(),
2151            course,
2152            SuspectedCheaterStatus::Flagged,
2153        )
2154        .await
2155        .unwrap();
2156        assert!(cheaters.is_empty());
2157    }
2158
2159    // TODO: New automatic completion tests?
2160}