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    /// `None` only on a module registering through credit registration.
761    pub uh_course_code: Option<String>,
762    pub email: String,
763    pub ects_credits: Option<f32>,
764    pub enable_registering_completion_to_uh_open_university: bool,
765    pub enable_credit_registration_via_suotar: bool,
766    /// Whether this completion in particular goes through the push path. Decides which flow the
767    /// page shows: the module flag above only says the module takes part.
768    pub register_credits_via_suotar: bool,
769    /// What the student is told while the completion has no credit registration: `sending` when
770    /// the push path will create one, `not_registering` when it never will.
771    pub status_before_registration: StudentFacingCreditRegistrationStatus,
772    /// `Some` only when the student can generate a certificate for this module right now, which is
773    /// also the id `/generate-certificate` wants.
774    ///
775    /// Read off the same completion as the rest of this object, the one
776    /// [`course_module_completions::select_registration_completion`] picks. The course page's
777    /// congratulations card reads off the best one instead, so a student whose registration
778    /// completion is not their best can see a certificate there and none here.
779    pub certificate_configuration_id: Option<Uuid>,
780    /// Why the student said they need the credits rather than a certificate, if they have been
781    /// asked and answered. Advisory; it seeds the field when they come back to the page.
782    pub credit_justification: Option<String>,
783}
784
785/// The default certificate configuration a completion of this module alone earns.
786///
787/// `configurations` must come from
788/// [`crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course`],
789/// which has already dropped the configurations that require more than one module; a configuration
790/// merely naming this module among several is not one this module satisfies.
791fn default_certificate_configuration_id_for_module(
792    configurations: &[crate::certificate_configurations::CertificateConfigurationAndRequirements],
793    course_module_id: Uuid,
794) -> Option<Uuid> {
795    configurations
796        .iter()
797        .find(|configuration| {
798            configuration
799                .requirements
800                .course_module_ids
801                .contains(&course_module_id)
802        })
803        .map(|configuration| configuration.certificate_configuration.id)
804}
805
806pub async fn get_user_completion_information(
807    conn: &mut PgConnection,
808    user_id: Uuid,
809    course_module: &CourseModule,
810) -> ModelResult<UserCompletionInformation> {
811    let user = users::get_by_id(conn, user_id).await?;
812    let course = courses::get_course(conn, course_module.course_id).await?;
813    let course_module_completion =
814        course_module_completions::get_registration_completion_by_user_and_course_module_id(
815            conn,
816            user.id,
817            course_module.id,
818        )
819        .await?;
820    let credit_registration_config =
821        course_modules::get_credit_registration_config(conn, course_module.id).await?;
822    // The push path explains a missing course code on its own status page, so failing here would
823    // hide the error instead of showing it.
824    if course_module.uh_course_code.is_none()
825        && !course_module_completion.register_credits_via_suotar
826    {
827        return Err(ModelError::new(
828            ModelErrorType::InvalidRequest,
829            "Course module is missing uh_course_code.".to_string(),
830            None,
831        ));
832    }
833    // A completion still awaiting review is treated as if it were not there, so a flagged student
834    // cannot read their own flag off the certificate the page would otherwise offer them.
835    let certificate_configuration_id = if course_module.certification_enabled
836        && course_module_completion.passed
837        && !course_module_completion.needs_to_be_reviewed
838    {
839        let configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(
840            conn,
841            course_module.course_id,
842        )
843        .await?;
844        let configuration_id =
845            default_certificate_configuration_id_for_module(&configurations, course_module.id);
846        if configuration_id.is_none() {
847            debug!(
848                course_module_id = ?course_module.id,
849                "Certification is enabled for the module but no default certificate configuration names it, so the student is offered no certificate."
850            );
851        }
852        configuration_id
853    } else {
854        None
855    };
856    let credit_justification =
857        crate::completion_registration_credit_justifications::get_by_completion_id(
858            conn,
859            course_module_completion.id,
860        )
861        .await?
862        .map(|row| row.justification);
863    let status_before_registration =
864        if course_module_completions::get_credit_registration_expected_ids(
865            conn,
866            &[course_module_completion.id],
867        )
868        .await?
869        .is_empty()
870        {
871            StudentFacingCreditRegistrationStatus::NotRegistering
872        } else {
873            StudentFacingCreditRegistrationStatus::Sending
874        };
875    Ok(UserCompletionInformation {
876        course_module_completion_id: course_module_completion.id,
877        course_name: course.name.clone(),
878        course_module_name: course_module.name.clone(),
879        uh_course_code: course_module.uh_course_code.clone(),
880        ects_credits: course_module.ects_credits,
881        email: course_module_completion.email,
882        enable_registering_completion_to_uh_open_university: course_module
883            .enable_registering_completion_to_uh_open_university,
884        enable_credit_registration_via_suotar: credit_registration_config
885            .enable_credit_registration_via_suotar,
886        register_credits_via_suotar: course_module_completion.register_credits_via_suotar,
887        status_before_registration,
888        certificate_configuration_id,
889        credit_justification,
890    })
891}
892
893#[derive(Clone, PartialEq, Deserialize, Serialize, ToSchema)]
894
895pub struct UserModuleCompletionStatus {
896    pub completed: bool,
897    pub default: bool,
898    pub module_id: Uuid,
899    pub name: String,
900    pub order_number: i32,
901    pub prerequisite_modules_completed: bool,
902    pub grade: Option<i32>,
903    pub passed: Option<bool>,
904    pub enable_registering_completion_to_uh_open_university: bool,
905    pub enable_credit_registration_via_suotar: bool,
906    /// Whether the module's registration flow is the push path, decided by
907    /// [`course_module_completions::select_registration_completion`] rather than by the shown
908    /// completion. False when no completion is shown.
909    pub register_credits_via_suotar: bool,
910    pub certification_enabled: bool,
911    pub certificate_configuration_id: Option<Uuid>,
912}
913
914/// Gets course modules with user's completion status for the given instance.
915pub async fn get_user_module_completion_statuses_for_course(
916    conn: &mut PgConnection,
917    user_id: Uuid,
918    course_id: Uuid,
919) -> ModelResult<Vec<UserModuleCompletionStatus>> {
920    let course = courses::get_course(conn, course_id).await?;
921    let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
922
923    let course_module_completions_raw =
924        course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
925            .await?;
926
927    let completions_by_module: HashMap<Uuid, Vec<CourseModuleCompletion>> =
928        course_module_completions_raw
929            .into_iter()
930            .into_group_map_by(|c| c.course_module_id);
931    // A completion that still needs review (e.g. because the student was auto-flagged as a
932    // suspected cheater) is hidden from the student, as if it did not exist yet, so a flagged
933    // student cannot infer from the API that they are under suspicion.
934    let best_visible_completions: HashMap<Uuid, CourseModuleCompletion> = completions_by_module
935        .iter()
936        .filter_map(|(module_id, completions)| {
937            course_module_completions::select_best_completion(
938                completions.iter().filter(|c| !c.needs_to_be_reviewed),
939            )
940            .map(|best| (*module_id, best.clone()))
941        })
942        .collect();
943    let registers_via_suotar_module_ids: HashSet<Uuid> = completions_by_module
944        .into_iter()
945        .filter_map(|(module_id, completions)| {
946            course_module_completions::select_registration_completion(completions)
947                .filter(|c| c.register_credits_via_suotar)
948                .map(|_| module_id)
949        })
950        .collect();
951
952    let all_default_certificate_configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(conn, course_id).await?;
953
954    let credit_registration_enabled_module_ids: HashSet<Uuid> =
955        course_modules::get_credit_registration_enabled_ids_for_course(conn, course_id)
956            .await?
957            .into_iter()
958            .collect();
959
960    let course_module_completion_statuses = course_modules
961        .into_iter()
962        .map(|module| {
963            let completion = best_visible_completions.get(&module.id);
964            let passed = completion.map(|x| x.passed);
965            let certificate_configuration_id =
966                if module.certification_enabled && passed == Some(true) {
967                    default_certificate_configuration_id_for_module(
968                        &all_default_certificate_configurations,
969                        module.id,
970                    )
971                } else {
972                    None
973                };
974            UserModuleCompletionStatus {
975                completed: completion.is_some(),
976                default: module.is_default_module(),
977                module_id: module.id,
978                name: module.name.unwrap_or_else(|| course.name.clone()),
979                order_number: module.order_number,
980                passed,
981                grade: completion.and_then(|x| x.grade),
982                prerequisite_modules_completed: completion
983                    .is_some_and(|x| x.prerequisite_modules_completed),
984                enable_registering_completion_to_uh_open_university: module
985                    .enable_registering_completion_to_uh_open_university,
986                enable_credit_registration_via_suotar: credit_registration_enabled_module_ids
987                    .contains(&module.id),
988                register_credits_via_suotar: completion.is_some()
989                    && registers_via_suotar_module_ids.contains(&module.id),
990                certification_enabled: module.certification_enabled,
991                certificate_configuration_id,
992            }
993        })
994        .collect();
995    Ok(course_module_completion_statuses)
996}
997
998#[derive(Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)]
999
1000pub struct CompletionRegistrationLink {
1001    pub url: String,
1002}
1003
1004pub async fn get_completion_registration_link_and_save_attempt(
1005    conn: &mut PgConnection,
1006    user_id: Uuid,
1007    course_module: &CourseModule,
1008) -> ModelResult<CompletionRegistrationLink> {
1009    if !course_module.enable_registering_completion_to_uh_open_university {
1010        return Err(ModelError::new(
1011            ModelErrorType::InvalidRequest,
1012            "Completion registration is not enabled for this course module.".to_string(),
1013            None,
1014        ));
1015    }
1016    let user = users::get_by_id(conn, user_id).await?;
1017
1018    let course_module_completion =
1019        course_module_completions::get_registration_completion_by_user_and_course_module_id(
1020            conn,
1021            user.id,
1022            course_module.id,
1023        )
1024        .await?;
1025    // Same condition as the registration page's choice of flow.
1026    if course_module.enable_credit_registration_via_suotar
1027        && course_module_completion.register_credits_via_suotar
1028    {
1029        return Err(model_err!(
1030            InvalidRequest,
1031            "This completion is registered through the study registry.".to_string()
1032        ));
1033    }
1034    course_module_completions::update_completion_registration_attempt_date(
1035        conn,
1036        course_module_completion.id,
1037        Utc::now(),
1038    )
1039    .await?;
1040    let registration_link = if let Some(link_override) = course_module
1041        .completion_registration_link_override
1042        .as_deref()
1043        .map(str::trim)
1044        .filter(|link| !link.is_empty())
1045    {
1046        link_override.to_owned()
1047    } else {
1048        let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
1049            ModelError::new(
1050                ModelErrorType::PreconditionFailed,
1051                "Course module doesn't have an assossiated University of Helsinki course code."
1052                    .to_string(),
1053                None,
1054            )
1055        })?;
1056        open_university_registration_links::get_link_by_course_code(conn, &uh_course_code).await?
1057    };
1058
1059    Ok(CompletionRegistrationLink {
1060        url: registration_link,
1061    })
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066    use chrono::Duration;
1067    use user_exercise_states::{ReviewingStage, UserExerciseStateUpdate};
1068
1069    use super::*;
1070
1071    use crate::{
1072        exercises::{ActivityProgress, GradingProgress},
1073        suspected_cheaters::SuspectedCheaterStatus,
1074        test_helper::*,
1075    };
1076
1077    mod user_completion_information {
1078        use super::*;
1079        use crate::{
1080            certificate_configuration_to_requirements,
1081            certificate_configurations::{self, DatabaseCertificateConfiguration},
1082            file_uploads,
1083        };
1084
1085        const UH_COURSE_CODE: &str = "TKT00000";
1086
1087        /// A module the old registration page can actually be opened for: it needs a course code,
1088        /// and the certificate the detour offers needs certification switched on.
1089        async fn registrable_module(
1090            conn: &mut PgConnection,
1091            course_module_id: Uuid,
1092            certification_enabled: bool,
1093        ) -> CourseModule {
1094            course_modules::update_certification_enabled(
1095                conn,
1096                course_module_id,
1097                certification_enabled,
1098            )
1099            .await
1100            .unwrap();
1101            course_modules::update_uh_course_code(
1102                conn,
1103                course_module_id,
1104                Some(UH_COURSE_CODE.to_string()),
1105            )
1106            .await
1107            .unwrap()
1108        }
1109
1110        async fn complete(
1111            conn: &mut PgConnection,
1112            course_id: Uuid,
1113            course_module_id: Uuid,
1114            user_id: Uuid,
1115            passed: bool,
1116        ) -> CourseModuleCompletion {
1117            course_module_completions::insert(
1118                conn,
1119                PKeyPolicy::Generate,
1120                &NewCourseModuleCompletion {
1121                    course_id,
1122                    course_module_id,
1123                    user_id,
1124                    completion_date: Utc::now(),
1125                    completion_registration_attempt_date: None,
1126                    completion_language: "en-US".to_string(),
1127                    eligible_for_ects: true,
1128                    email: "student@example.com".to_string(),
1129                    grade: None,
1130                    passed,
1131                },
1132                CourseModuleCompletionGranter::Automatic,
1133            )
1134            .await
1135            .unwrap()
1136        }
1137
1138        /// A certificate configuration requiring exactly the given modules. One module makes it a
1139        /// default configuration; more than one makes it the kind no single module earns.
1140        async fn certificate_configuration_requiring(
1141            conn: &mut PgConnection,
1142            course_module_ids: &[Uuid],
1143        ) -> Uuid {
1144            let background_svg_file_upload_id = file_uploads::insert(
1145                conn,
1146                "background.svg",
1147                "certificates/background.svg",
1148                "image/svg+xml",
1149                None,
1150                None,
1151            )
1152            .await
1153            .unwrap();
1154            let configuration = certificate_configurations::insert(
1155                conn,
1156                &DatabaseCertificateConfiguration {
1157                    id: Uuid::new_v4(),
1158                    certificate_owner_name_y_pos: None,
1159                    certificate_owner_name_x_pos: None,
1160                    certificate_owner_name_font_size: None,
1161                    certificate_owner_name_text_color: None,
1162                    certificate_owner_name_text_anchor: None,
1163                    certificate_validate_url_y_pos: None,
1164                    certificate_validate_url_x_pos: None,
1165                    certificate_validate_url_font_size: None,
1166                    certificate_validate_url_text_color: None,
1167                    certificate_validate_url_text_anchor: None,
1168                    certificate_date_y_pos: None,
1169                    certificate_date_x_pos: None,
1170                    certificate_date_font_size: None,
1171                    certificate_date_text_color: None,
1172                    certificate_date_text_anchor: None,
1173                    certificate_locale: None,
1174                    paper_size: None,
1175                    background_svg_path: "certificates/background.svg".to_string(),
1176                    background_svg_file_upload_id,
1177                    overlay_svg_path: None,
1178                    overlay_svg_file_upload_id: None,
1179                    render_certificate_grade: false,
1180                    certificate_grade_y_pos: None,
1181                    certificate_grade_x_pos: None,
1182                    certificate_grade_font_size: None,
1183                    certificate_grade_text_color: None,
1184                    certificate_grade_text_anchor: None,
1185                },
1186            )
1187            .await
1188            .unwrap();
1189            for course_module_id in course_module_ids {
1190                certificate_configuration_to_requirements::insert(
1191                    conn,
1192                    configuration.id,
1193                    Some(*course_module_id),
1194                )
1195                .await
1196                .unwrap();
1197            }
1198            configuration.id
1199        }
1200
1201        #[tokio::test]
1202        async fn offers_the_certificate_a_passed_completion_has_earned() {
1203            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1204            let module = registrable_module(tx.as_mut(), course_module.id, true).await;
1205            complete(tx.as_mut(), course, module.id, user, true).await;
1206            let configuration_id =
1207                certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1208
1209            let information = get_user_completion_information(tx.as_mut(), user, &module)
1210                .await
1211                .unwrap();
1212
1213            assert_eq!(
1214                information.certificate_configuration_id,
1215                Some(configuration_id)
1216            );
1217        }
1218
1219        #[tokio::test]
1220        async fn offers_no_certificate_when_the_module_has_certification_off() {
1221            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1222            let module = registrable_module(tx.as_mut(), course_module.id, false).await;
1223            complete(tx.as_mut(), course, module.id, user, true).await;
1224            certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1225
1226            let information = get_user_completion_information(tx.as_mut(), user, &module)
1227                .await
1228                .unwrap();
1229
1230            assert_eq!(information.certificate_configuration_id, None);
1231        }
1232
1233        #[tokio::test]
1234        async fn offers_no_certificate_for_a_failed_completion() {
1235            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1236            let module = registrable_module(tx.as_mut(), course_module.id, true).await;
1237            complete(tx.as_mut(), course, module.id, user, false).await;
1238            certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1239
1240            let information = get_user_completion_information(tx.as_mut(), user, &module)
1241                .await
1242                .unwrap();
1243
1244            assert_eq!(information.certificate_configuration_id, None);
1245        }
1246
1247        #[tokio::test]
1248        async fn offers_no_certificate_while_the_completion_is_under_review() {
1249            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1250            let module = registrable_module(tx.as_mut(), course_module.id, true).await;
1251            let completion = complete(tx.as_mut(), course, module.id, user, true).await;
1252            certificate_configuration_requiring(tx.as_mut(), &[module.id]).await;
1253            course_module_completions::update_needs_to_be_reviewed(
1254                tx.as_mut(),
1255                completion.id,
1256                true,
1257            )
1258            .await
1259            .unwrap();
1260
1261            let information = get_user_completion_information(tx.as_mut(), user, &module)
1262                .await
1263                .unwrap();
1264
1265            assert_eq!(information.certificate_configuration_id, None);
1266        }
1267
1268        #[tokio::test]
1269        async fn offers_no_certificate_this_module_alone_does_not_earn() {
1270            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1271            let module = registrable_module(tx.as_mut(), course_module.id, true).await;
1272            let other_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1273                .await
1274                .unwrap();
1275            complete(tx.as_mut(), course, module.id, user, true).await;
1276            certificate_configuration_requiring(tx.as_mut(), &[module.id, other_module.id]).await;
1277
1278            let information = get_user_completion_information(tx.as_mut(), user, &module)
1279                .await
1280                .unwrap();
1281
1282            assert_eq!(information.certificate_configuration_id, None);
1283        }
1284
1285        #[tokio::test]
1286        async fn carries_back_the_answer_the_student_already_gave() {
1287            insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
1288            let module = registrable_module(tx.as_mut(), course_module.id, true).await;
1289            let completion = complete(tx.as_mut(), course, module.id, user, true).await;
1290            crate::completion_registration_credit_justifications::upsert(
1291                tx.as_mut(),
1292                completion.id,
1293                user,
1294                "My employer needs them in the registry.",
1295            )
1296            .await
1297            .unwrap();
1298
1299            let information = get_user_completion_information(tx.as_mut(), user, &module)
1300                .await
1301                .unwrap();
1302
1303            assert_eq!(
1304                information.credit_justification.as_deref(),
1305                Some("My employer needs them in the registry.")
1306            );
1307        }
1308    }
1309
1310    mod grant_automatic_completion_if_eligible {
1311        use super::*;
1312        use crate::{
1313            chapters::NewChapter,
1314            course_modules::{
1315                self, AutomaticCompletionRequirements, CompletionPolicy, NewCourseModule,
1316            },
1317            exercises::{self, ActivityProgress, GradingProgress},
1318            library::content_management,
1319            user_exercise_states::{self, ReviewingStage, UserExerciseStateUpdate},
1320        };
1321
1322        #[tokio::test]
1323        async fn grants_automatic_completion_but_no_prerequisite_for_default_module() {
1324            insert_data!(:tx);
1325            let (mut tx, user, course, _instance, default_module, _submodule_1, _submodule_2) =
1326                create_test_data(tx).await;
1327            update_automatic_completion_status_and_grant_if_eligible(
1328                tx.as_mut(),
1329                &default_module,
1330                user,
1331            )
1332            .await
1333            .unwrap();
1334            let statuses =
1335                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1336                    .await
1337                    .unwrap();
1338            let status = statuses
1339                .iter()
1340                .find(|x| x.module_id == default_module.id)
1341                .unwrap();
1342            assert!(status.completed);
1343            assert!(!status.prerequisite_modules_completed);
1344        }
1345
1346        #[tokio::test]
1347        async fn grants_automatic_completion_but_no_prerequisite_for_submodule() {
1348            insert_data!(:tx);
1349            let (mut tx, user, course, _instance, _default_module, submodule_1, _submodule_2) =
1350                create_test_data(tx).await;
1351            update_automatic_completion_status_and_grant_if_eligible(
1352                tx.as_mut(),
1353                &submodule_1,
1354                user,
1355            )
1356            .await
1357            .unwrap();
1358            let statuses =
1359                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1360                    .await
1361                    .unwrap();
1362            let status = statuses
1363                .iter()
1364                .find(|x| x.module_id == submodule_1.id)
1365                .unwrap();
1366            assert!(status.completed);
1367            assert!(!status.prerequisite_modules_completed);
1368        }
1369
1370        #[tokio::test]
1371        async fn grants_automatic_completion_for_eligible_submodule_when_completing_default_module()
1372        {
1373            insert_data!(:tx);
1374            let (mut tx, user, course, _instance, default_module, submodule_1, submodule_2) =
1375                create_test_data(tx).await;
1376            update_automatic_completion_status_and_grant_if_eligible(
1377                tx.as_mut(),
1378                &default_module,
1379                user,
1380            )
1381            .await
1382            .unwrap();
1383            update_automatic_completion_status_and_grant_if_eligible(
1384                tx.as_mut(),
1385                &submodule_1,
1386                user,
1387            )
1388            .await
1389            .unwrap();
1390            update_automatic_completion_status_and_grant_if_eligible(
1391                tx.as_mut(),
1392                &submodule_2,
1393                user,
1394            )
1395            .await
1396            .unwrap();
1397            let statuses =
1398                get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1399                    .await
1400                    .unwrap();
1401            statuses.iter().for_each(|x| {
1402                assert!(x.completed);
1403                assert!(x.prerequisite_modules_completed);
1404            });
1405        }
1406
1407        async fn create_test_data(
1408            mut tx: Tx<'_>,
1409        ) -> (
1410            Tx<'_>,
1411            Uuid,
1412            Uuid,
1413            Uuid,
1414            CourseModule,
1415            CourseModule,
1416            CourseModule,
1417        ) {
1418            insert_data!(tx: tx; :user, :org, :course, :instance, :course_module, :chapter, :page, :exercise);
1419            // These tests complete modules instantly, which would trip suspected-cheater detection
1420            // (on by default) and hide the completion. Detection is exercised by its own tests, so
1421            // disable it here to test automatic-completion granting in isolation.
1422            courses::set_cheater_detection_enabled(tx.as_mut(), course, false)
1423                .await
1424                .unwrap();
1425            let automatic_completion_policy =
1426                CompletionPolicy::Automatic(AutomaticCompletionRequirements {
1427                    course_module_id: course_module.id,
1428                    number_of_exercises_attempted_treshold: Some(0),
1429                    number_of_points_treshold: Some(0),
1430                    requires_exam: false,
1431                });
1432            courses::update_course_base_module_completion_count_requirement(tx.as_mut(), course, 1)
1433                .await
1434                .unwrap();
1435            let course_module_2 = course_modules::insert(
1436                tx.as_mut(),
1437                PKeyPolicy::Generate,
1438                &NewCourseModule::new(course, Some("Module 2".to_string()), 1),
1439            )
1440            .await
1441            .unwrap();
1442            let (chapter_2, page2) = content_management::create_new_chapter(
1443                tx.as_mut(),
1444                PKeyPolicy::Generate,
1445                &NewChapter {
1446                    name: "chapter 2".to_string(),
1447                    color: None,
1448                    course_id: course,
1449                    chapter_number: 2,
1450                    front_page_id: None,
1451                    opens_at: None,
1452                    deadline: None,
1453                    course_module_id: Some(course_module_2.id),
1454                },
1455                user,
1456                |_, _, _| unimplemented!(),
1457                |_| unimplemented!(),
1458            )
1459            .await
1460            .unwrap();
1461
1462            let exercise_2 = exercises::insert(
1463                tx.as_mut(),
1464                PKeyPolicy::Generate,
1465                course,
1466                "",
1467                page2.id,
1468                chapter_2.id,
1469                0,
1470            )
1471            .await
1472            .unwrap();
1473            let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
1474                tx.as_mut(),
1475                user,
1476                exercise,
1477                Some(course),
1478                None,
1479            )
1480            .await
1481            .unwrap();
1482            user_exercise_states::update(
1483                tx.as_mut(),
1484                UserExerciseStateUpdate {
1485                    id: user_exercise_state.id,
1486                    score_given: Some(0.0),
1487                    activity_progress: ActivityProgress::Completed,
1488                    reviewing_stage: ReviewingStage::NotStarted,
1489                    grading_progress: GradingProgress::FullyGraded,
1490                },
1491            )
1492            .await
1493            .unwrap();
1494            let user_exercise_state_2 = user_exercise_states::get_or_create_user_exercise_state(
1495                tx.as_mut(),
1496                user,
1497                exercise_2,
1498                Some(course),
1499                None,
1500            )
1501            .await
1502            .unwrap();
1503            user_exercise_states::update(
1504                tx.as_mut(),
1505                UserExerciseStateUpdate {
1506                    id: user_exercise_state_2.id,
1507                    score_given: Some(0.0),
1508                    activity_progress: ActivityProgress::Completed,
1509                    reviewing_stage: ReviewingStage::NotStarted,
1510                    grading_progress: GradingProgress::FullyGraded,
1511                },
1512            )
1513            .await
1514            .unwrap();
1515            let default_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1516                .await
1517                .unwrap();
1518            let default_module = course_modules::update_automatic_completion_status(
1519                tx.as_mut(),
1520                default_module.id,
1521                &automatic_completion_policy,
1522            )
1523            .await
1524            .unwrap();
1525            let course_module = course_modules::update_automatic_completion_status(
1526                tx.as_mut(),
1527                course_module.id,
1528                &automatic_completion_policy,
1529            )
1530            .await
1531            .unwrap();
1532            let course_module_2 = course_modules::update_automatic_completion_status(
1533                tx.as_mut(),
1534                course_module_2.id,
1535                &automatic_completion_policy,
1536            )
1537            .await
1538            .unwrap();
1539            (
1540                tx,
1541                user,
1542                course,
1543                instance.id,
1544                default_module,
1545                course_module,
1546                course_module_2,
1547            )
1548        }
1549    }
1550
1551    #[tokio::test]
1552    async fn tags_suspected_cheater() {
1553        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1554
1555        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1556            .await
1557            .unwrap();
1558        let state = user_exercise_states::get_or_create_user_exercise_state(
1559            tx.as_mut(),
1560            user,
1561            exercise,
1562            Some(course),
1563            None,
1564        )
1565        .await
1566        .unwrap();
1567        user_exercise_states::update(
1568            tx.as_mut(),
1569            UserExerciseStateUpdate {
1570                id: state.id,
1571                score_given: Some(10.0),
1572                activity_progress: ActivityProgress::Completed,
1573                reviewing_stage: ReviewingStage::NotStarted,
1574                grading_progress: GradingProgress::FullyGraded,
1575            },
1576        )
1577        .await
1578        .unwrap();
1579
1580        let completion = course_module_completions::insert(
1581            tx.as_mut(),
1582            PKeyPolicy::Generate,
1583            &NewCourseModuleCompletion {
1584                course_id: course,
1585                course_module_id: course_module.id,
1586                user_id: user,
1587                completion_date: Utc::now() + Duration::days(1),
1588                completion_registration_attempt_date: None,
1589                completion_language: "en-US".to_string(),
1590                eligible_for_ects: false,
1591                email: "email".to_string(),
1592                grade: None,
1593                passed: true,
1594            },
1595            CourseModuleCompletionGranter::Automatic,
1596        )
1597        .await
1598        .unwrap();
1599        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1600            tx.as_mut(),
1601            course_module.id,
1602            259200,
1603        )
1604        .await
1605        .unwrap();
1606        check_and_insert_suspected_cheaters(
1607            tx.as_mut(),
1608            user,
1609            course,
1610            thresholds.duration_seconds,
1611            completion,
1612        )
1613        .await
1614        .unwrap();
1615
1616        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1617            tx.as_mut(),
1618            course,
1619            SuspectedCheaterStatus::Flagged,
1620        )
1621        .await
1622        .unwrap();
1623        assert_eq!(cheaters.len(), 1);
1624        assert_eq!(cheaters[0].user_id, user);
1625    }
1626
1627    #[tokio::test]
1628    async fn tagging_suspected_cheater_is_idempotent() {
1629        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1630
1631        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1632            .await
1633            .unwrap();
1634        let state = user_exercise_states::get_or_create_user_exercise_state(
1635            tx.as_mut(),
1636            user,
1637            exercise,
1638            Some(course),
1639            None,
1640        )
1641        .await
1642        .unwrap();
1643        user_exercise_states::update(
1644            tx.as_mut(),
1645            UserExerciseStateUpdate {
1646                id: state.id,
1647                score_given: Some(10.0),
1648                activity_progress: ActivityProgress::Completed,
1649                reviewing_stage: ReviewingStage::NotStarted,
1650                grading_progress: GradingProgress::FullyGraded,
1651            },
1652        )
1653        .await
1654        .unwrap();
1655
1656        let completion = course_module_completions::insert(
1657            tx.as_mut(),
1658            PKeyPolicy::Generate,
1659            &NewCourseModuleCompletion {
1660                course_id: course,
1661                course_module_id: course_module.id,
1662                user_id: user,
1663                completion_date: Utc::now() + Duration::days(1),
1664                completion_registration_attempt_date: None,
1665                completion_language: "en-US".to_string(),
1666                eligible_for_ects: false,
1667                email: "email".to_string(),
1668                grade: None,
1669                passed: true,
1670            },
1671            CourseModuleCompletionGranter::Automatic,
1672        )
1673        .await
1674        .unwrap();
1675        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1676            tx.as_mut(),
1677            course_module.id,
1678            259200,
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.clone(),
1688        )
1689        .await
1690        .unwrap();
1691        check_and_insert_suspected_cheaters(
1692            tx.as_mut(),
1693            user,
1694            course,
1695            thresholds.duration_seconds,
1696            completion,
1697        )
1698        .await
1699        .unwrap();
1700
1701        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1702            tx.as_mut(),
1703            course,
1704            SuspectedCheaterStatus::Flagged,
1705        )
1706        .await
1707        .unwrap();
1708        assert_eq!(cheaters.len(), 1);
1709        let completion_needing_review =
1710            course_module_completions::get_latest_by_course_and_user_ids(
1711                tx.as_mut(),
1712                course_module.id,
1713                user,
1714            )
1715            .await
1716            .unwrap();
1717        assert!(completion_needing_review.needs_to_be_reviewed);
1718
1719        suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1720            .await
1721            .unwrap();
1722        let archived_completion = course_module_completions::get_latest_by_course_and_user_ids(
1723            tx.as_mut(),
1724            course_module.id,
1725            user,
1726        )
1727        .await
1728        .unwrap();
1729        assert!(!archived_completion.needs_to_be_reviewed);
1730        check_and_insert_suspected_cheaters(
1731            tx.as_mut(),
1732            user,
1733            course,
1734            thresholds.duration_seconds,
1735            archived_completion,
1736        )
1737        .await
1738        .unwrap();
1739
1740        let visible_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1741            tx.as_mut(),
1742            course,
1743            SuspectedCheaterStatus::Flagged,
1744        )
1745        .await
1746        .unwrap();
1747        let archived_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1748            tx.as_mut(),
1749            course,
1750            SuspectedCheaterStatus::Dismissed,
1751        )
1752        .await
1753        .unwrap();
1754        assert!(visible_cheaters.is_empty());
1755        assert_eq!(archived_cheaters.len(), 1);
1756        let rechecked_completion = course_module_completions::get_latest_by_course_and_user_ids(
1757            tx.as_mut(),
1758            course_module.id,
1759            user,
1760        )
1761        .await
1762        .unwrap();
1763        assert!(!rechecked_completion.needs_to_be_reviewed);
1764    }
1765
1766    #[tokio::test]
1767    async fn confirming_then_dismissing_restores_grade() {
1768        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1769
1770        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1771            .await
1772            .unwrap();
1773        let state = user_exercise_states::get_or_create_user_exercise_state(
1774            tx.as_mut(),
1775            user,
1776            exercise,
1777            Some(course),
1778            None,
1779        )
1780        .await
1781        .unwrap();
1782        user_exercise_states::update(
1783            tx.as_mut(),
1784            UserExerciseStateUpdate {
1785                id: state.id,
1786                score_given: Some(10.0),
1787                activity_progress: ActivityProgress::Completed,
1788                reviewing_stage: ReviewingStage::NotStarted,
1789                grading_progress: GradingProgress::FullyGraded,
1790            },
1791        )
1792        .await
1793        .unwrap();
1794
1795        // A graded, passing completion so we can prove the exact grade is restored, not just pass/fail.
1796        let completion = course_module_completions::insert(
1797            tx.as_mut(),
1798            PKeyPolicy::Generate,
1799            &NewCourseModuleCompletion {
1800                course_id: course,
1801                course_module_id: course_module.id,
1802                user_id: user,
1803                completion_date: Utc::now() + Duration::days(1),
1804                completion_registration_attempt_date: None,
1805                completion_language: "en-US".to_string(),
1806                eligible_for_ects: false,
1807                email: "email".to_string(),
1808                grade: Some(4),
1809                passed: true,
1810            },
1811            CourseModuleCompletionGranter::Automatic,
1812        )
1813        .await
1814        .unwrap();
1815        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1816            tx.as_mut(),
1817            course_module.id,
1818            259200,
1819        )
1820        .await
1821        .unwrap();
1822        check_and_insert_suspected_cheaters(
1823            tx.as_mut(),
1824            user,
1825            course,
1826            thresholds.duration_seconds,
1827            completion,
1828        )
1829        .await
1830        .unwrap();
1831
1832        // Confirm: the student is failed and the previous grade is snapshotted.
1833        suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
1834            .await
1835            .unwrap();
1836        let failed = course_module_completions::get_latest_by_course_and_user_ids(
1837            tx.as_mut(),
1838            course_module.id,
1839            user,
1840        )
1841        .await
1842        .unwrap();
1843        assert!(!failed.passed);
1844        assert_eq!(failed.grade, Some(0));
1845        let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1846            tx.as_mut(),
1847            course,
1848            SuspectedCheaterStatus::ConfirmedCheating,
1849        )
1850        .await
1851        .unwrap();
1852        assert_eq!(confirmed.len(), 1);
1853
1854        // Dismiss: the confirmation is undone and the exact previous grade is restored.
1855        suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1856            .await
1857            .unwrap();
1858        let restored = course_module_completions::get_latest_by_course_and_user_ids(
1859            tx.as_mut(),
1860            course_module.id,
1861            user,
1862        )
1863        .await
1864        .unwrap();
1865        assert!(restored.passed);
1866        assert_eq!(restored.grade, Some(4));
1867        assert!(!restored.needs_to_be_reviewed);
1868        let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1869            tx.as_mut(),
1870            course,
1871            SuspectedCheaterStatus::Dismissed,
1872        )
1873        .await
1874        .unwrap();
1875        assert_eq!(dismissed.len(), 1);
1876    }
1877
1878    #[tokio::test]
1879    async fn manual_completion_prevents_flagging() {
1880        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module);
1881
1882        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1883            .await
1884            .unwrap();
1885
1886        // A teacher has manually granted a completion for this student.
1887        let teacher = users::insert(
1888            tx.as_mut(),
1889            PKeyPolicy::Generate,
1890            "teacher-vouching@example.com",
1891            Some("Teacher"),
1892            Some("McVouch"),
1893        )
1894        .await
1895        .unwrap();
1896        course_module_completions::insert(
1897            tx.as_mut(),
1898            PKeyPolicy::Generate,
1899            &NewCourseModuleCompletion {
1900                course_id: course,
1901                course_module_id: course_module.id,
1902                user_id: user,
1903                completion_date: Utc::now(),
1904                completion_registration_attempt_date: None,
1905                completion_language: "en-US".to_string(),
1906                eligible_for_ects: false,
1907                email: "email".to_string(),
1908                grade: Some(5),
1909                passed: true,
1910            },
1911            CourseModuleCompletionGranter::User(teacher),
1912        )
1913        .await
1914        .unwrap();
1915
1916        // An automatic completion well inside the threshold would normally flag the student.
1917        let automatic_completion = course_module_completions::insert(
1918            tx.as_mut(),
1919            PKeyPolicy::Generate,
1920            &NewCourseModuleCompletion {
1921                course_id: course,
1922                course_module_id: course_module.id,
1923                user_id: user,
1924                completion_date: Utc::now() + Duration::days(1),
1925                completion_registration_attempt_date: None,
1926                completion_language: "en-US".to_string(),
1927                eligible_for_ects: false,
1928                email: "email".to_string(),
1929                grade: None,
1930                passed: true,
1931            },
1932            CourseModuleCompletionGranter::Automatic,
1933        )
1934        .await
1935        .unwrap();
1936        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1937            tx.as_mut(),
1938            course_module.id,
1939            259200,
1940        )
1941        .await
1942        .unwrap();
1943        check_and_insert_suspected_cheaters(
1944            tx.as_mut(),
1945            user,
1946            course,
1947            thresholds.duration_seconds,
1948            automatic_completion,
1949        )
1950        .await
1951        .unwrap();
1952
1953        // The teacher's manual completion exempts the student, so no suspicion is created.
1954        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1955            tx.as_mut(),
1956            course,
1957            SuspectedCheaterStatus::Flagged,
1958        )
1959        .await
1960        .unwrap();
1961        assert!(cheaters.is_empty());
1962    }
1963
1964    #[tokio::test]
1965    async fn adding_manual_completion_dismisses_confirmed_suspicion_and_restores_grade() {
1966        insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1967
1968        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1969            .await
1970            .unwrap();
1971        let state = user_exercise_states::get_or_create_user_exercise_state(
1972            tx.as_mut(),
1973            user,
1974            exercise,
1975            Some(course),
1976            None,
1977        )
1978        .await
1979        .unwrap();
1980        user_exercise_states::update(
1981            tx.as_mut(),
1982            UserExerciseStateUpdate {
1983                id: state.id,
1984                score_given: Some(10.0),
1985                activity_progress: ActivityProgress::Completed,
1986                reviewing_stage: ReviewingStage::NotStarted,
1987                grading_progress: GradingProgress::FullyGraded,
1988            },
1989        )
1990        .await
1991        .unwrap();
1992
1993        // Flag the student via a fast automatic completion, then confirm cheating (fails the grade).
1994        let completion = course_module_completions::insert(
1995            tx.as_mut(),
1996            PKeyPolicy::Generate,
1997            &NewCourseModuleCompletion {
1998                course_id: course,
1999                course_module_id: course_module.id,
2000                user_id: user,
2001                completion_date: Utc::now() + Duration::days(1),
2002                completion_registration_attempt_date: None,
2003                completion_language: "en-US".to_string(),
2004                eligible_for_ects: false,
2005                email: "email".to_string(),
2006                grade: Some(4),
2007                passed: true,
2008            },
2009            CourseModuleCompletionGranter::Automatic,
2010        )
2011        .await
2012        .unwrap();
2013        let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
2014            tx.as_mut(),
2015            course_module.id,
2016            259200,
2017        )
2018        .await
2019        .unwrap();
2020        check_and_insert_suspected_cheaters(
2021            tx.as_mut(),
2022            user,
2023            course,
2024            thresholds.duration_seconds,
2025            completion,
2026        )
2027        .await
2028        .unwrap();
2029        suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
2030            .await
2031            .unwrap();
2032
2033        // A teacher manually adds a completion for the same student. `skip_duplicate_completions` is
2034        // false so the completion is inserted even though the module is already completed.
2035        let teacher = users::insert(
2036            tx.as_mut(),
2037            PKeyPolicy::Generate,
2038            "teacher-vouching@example.com",
2039            Some("Teacher"),
2040            Some("McVouch"),
2041        )
2042        .await
2043        .unwrap();
2044        add_manual_completions(
2045            tx.as_mut(),
2046            teacher,
2047            &instance,
2048            &TeacherManualCompletionRequest {
2049                course_module_id: course_module.id,
2050                new_completions: vec![TeacherManualCompletion {
2051                    user_id: user,
2052                    grade: Some(5),
2053                    passed: true,
2054                    completion_date: None,
2055                }],
2056                skip_duplicate_completions: false,
2057            },
2058        )
2059        .await
2060        .unwrap();
2061
2062        // The suspicion is dismissed and the confirmed-cheating grade failure is undone.
2063        let flagged = suspected_cheaters::get_all_suspected_cheaters_in_course(
2064            tx.as_mut(),
2065            course,
2066            SuspectedCheaterStatus::Flagged,
2067        )
2068        .await
2069        .unwrap();
2070        let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
2071            tx.as_mut(),
2072            course,
2073            SuspectedCheaterStatus::ConfirmedCheating,
2074        )
2075        .await
2076        .unwrap();
2077        let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
2078            tx.as_mut(),
2079            course,
2080            SuspectedCheaterStatus::Dismissed,
2081        )
2082        .await
2083        .unwrap();
2084        assert!(flagged.is_empty());
2085        assert!(confirmed.is_empty());
2086        assert_eq!(dismissed.len(), 1);
2087
2088        // The automatic completion's failed grade is restored and its review flag is cleared.
2089        let restored =
2090            course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
2091                tx.as_mut(),
2092                course_module.id,
2093                course,
2094                user,
2095            )
2096            .await
2097            .unwrap();
2098        assert!(restored.passed);
2099        assert_eq!(restored.grade, Some(4));
2100        assert!(!restored.needs_to_be_reviewed);
2101    }
2102
2103    #[tokio::test]
2104    async fn doesnt_tag_suspected_cheater() {
2105        insert_data!(:tx, user:user, :org, :course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
2106
2107        crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
2108            .await
2109            .unwrap();
2110        let state = user_exercise_states::get_or_create_user_exercise_state(
2111            tx.as_mut(),
2112            user,
2113            exercise,
2114            Some(course),
2115            None,
2116        )
2117        .await
2118        .unwrap();
2119        user_exercise_states::update(
2120            tx.as_mut(),
2121            UserExerciseStateUpdate {
2122                id: state.id,
2123                score_given: Some(9.0),
2124                activity_progress: ActivityProgress::Completed,
2125                reviewing_stage: ReviewingStage::NotStarted,
2126                grading_progress: GradingProgress::FullyGraded,
2127            },
2128        )
2129        .await
2130        .unwrap();
2131
2132        course_module_completions::insert(
2133            tx.as_mut(),
2134            PKeyPolicy::Generate,
2135            &NewCourseModuleCompletion {
2136                course_id: course,
2137                course_module_id: course_module.id,
2138                user_id: user,
2139                completion_date: Utc::now() + Duration::days(3),
2140                completion_registration_attempt_date: None,
2141                completion_language: "en-US".to_string(),
2142                eligible_for_ects: false,
2143                email: "email".to_string(),
2144                grade: Some(9),
2145                passed: true,
2146            },
2147            CourseModuleCompletionGranter::Automatic,
2148        )
2149        .await
2150        .unwrap();
2151        suspected_cheaters::insert_thresholds_by_module_id(tx.as_mut(), course_module.id, 172800)
2152            .await
2153            .unwrap();
2154        update_automatic_completion_status_and_grant_if_eligible(tx.as_mut(), &course_module, user)
2155            .await
2156            .unwrap();
2157
2158        let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
2159            tx.as_mut(),
2160            course,
2161            SuspectedCheaterStatus::Flagged,
2162        )
2163        .await
2164        .unwrap();
2165        assert!(cheaters.is_empty());
2166    }
2167
2168    // TODO: New automatic completion tests?
2169}