1use chrono::{DateTime, Utc};
2use itertools::Itertools;
3use std::collections::HashMap;
4use utoipa::ToSchema;
5
6use crate::{
7 course_exams,
8 course_instance_enrollments::{self, NewCourseInstanceEnrollment},
9 course_instances::{self, CourseInstance},
10 course_module_completions::{
11 self, CourseModuleCompletion, CourseModuleCompletionGranter,
12 CourseModuleCompletionWithRegistrationInfo, NewCourseModuleCompletion,
13 },
14 course_modules::{self, AutomaticCompletionRequirements, CompletionPolicy, CourseModule},
15 courses, exams, open_university_registration_links,
16 prelude::*,
17 suspected_cheaters, user_course_settings,
18 user_details::UserDetail,
19 user_exercise_states,
20 users::{self, User},
21};
22
23pub async fn update_automatic_completion_status_and_grant_if_eligible(
27 conn: &mut PgConnection,
28 course_module: &CourseModule,
29 user_id: Uuid,
30) -> ModelResult<()> {
31 let mut tx = conn.begin().await?;
32 let completion =
33 create_automatic_course_module_completion_if_eligible(&mut tx, course_module, user_id)
34 .await?;
35 if let Some(completion) = completion {
36 let course = courses::get_course(&mut tx, course_module.course_id).await?;
37 let submodule_completions_required = course
38 .base_module_completion_requires_n_submodule_completions
39 .try_into()?;
40 update_module_completion_prerequisite_statuses_for_user(
41 &mut tx,
42 user_id,
43 course_module.course_id,
44 submodule_completions_required,
45 )
46 .await?;
47
48 if course.cheater_detection_enabled {
49 let threshold_seconds = suspected_cheaters::get_thresholds_by_module_id(
52 &mut tx,
53 completion.course_module_id,
54 )
55 .await?
56 .map(|t| t.duration_seconds)
57 .unwrap_or(suspected_cheaters::DEFAULT_CHEATER_THRESHOLD_SECONDS);
58 if threshold_seconds > 0 {
62 check_and_insert_suspected_cheaters(
63 &mut tx,
64 user_id,
65 course.id,
66 threshold_seconds,
67 completion,
68 )
69 .await?;
70 }
71 }
72 }
73 tx.commit().await?;
74 Ok(())
75}
76
77pub async fn check_and_insert_suspected_cheaters(
78 conn: &mut PgConnection,
79 user_id: Uuid,
80 course_id: Uuid,
81 threshold_seconds: i32,
82 completion: CourseModuleCompletion,
83) -> ModelResult<()> {
84 if course_module_completions::user_has_manual_completion_in_course(conn, user_id, course_id)
87 .await?
88 {
89 return Ok(());
90 }
91
92 let total_points = user_exercise_states::get_user_total_course_points(conn, user_id, course_id)
93 .await?
94 .unwrap_or(0.0);
95
96 let completed_module = course_modules::get_by_id(conn, completion.course_module_id).await?;
97 let is_default_module = completed_module.is_default_module();
98
99 let student_duration_seconds = if is_default_module {
100 course_instances::get_student_duration(conn, completion.user_id, course_id)
101 .await?
102 .unwrap_or(0)
103 } else {
104 let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
105 let default_completion = course_module_completions::get_all_by_course_module_and_user_ids(
106 conn,
107 default_module.id,
108 completion.user_id,
109 )
110 .await?
111 .into_iter()
112 .max_by_key(|c| c.completion_date);
113
114 if let Some(default_completion) = default_completion {
115 let duration =
116 (completion.completion_date - default_completion.completion_date).num_seconds();
117 duration.max(0)
118 } else {
119 course_instances::get_student_duration(conn, completion.user_id, course_id)
121 .await?
122 .unwrap_or(0)
123 }
124 };
125
126 if (student_duration_seconds as i32) < threshold_seconds {
127 let suspicion_is_active = suspected_cheaters::insert(
128 conn,
129 completion.user_id,
130 course_id,
131 completion.course_module_id,
132 Some(student_duration_seconds as i32),
133 total_points as i32,
134 )
135 .await?;
136
137 if suspicion_is_active {
138 course_module_completions::update_needs_to_be_reviewed(conn, completion.id, true)
139 .await?;
140 }
141 }
142
143 Ok(())
144}
145
146#[instrument(skip(conn))]
149async fn create_automatic_course_module_completion_if_eligible(
150 conn: &mut PgConnection,
151 course_module: &CourseModule,
152 user_id: Uuid,
153) -> ModelResult<Option<CourseModuleCompletion>> {
154 let existing_completion =
155 course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
156 conn,
157 course_module.id,
158 course_module.course_id,
159 user_id,
160 )
161 .await
162 .optional()?;
163 if let Some(existing_completion) = existing_completion {
164 Ok(Some(existing_completion))
166 } else {
167 let eligible =
168 user_is_eligible_for_automatic_completion(conn, course_module, user_id).await?;
169 if eligible {
170 let course = courses::get_course(conn, course_module.course_id).await?;
171 let user = users::get_by_id(conn, user_id).await?;
172 if user.deleted_at.is_some() {
173 warn!("Cannot create a completion for a deleted user");
174 return Ok(None);
175 }
176 let user_details =
177 crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
178 let completion = course_module_completions::insert(
179 conn,
180 PKeyPolicy::Generate,
181 &NewCourseModuleCompletion {
182 course_id: course_module.course_id,
183 course_module_id: course_module.id,
184 user_id,
185 completion_date: Utc::now(),
186 completion_registration_attempt_date: None,
187 completion_language: course.language_code,
188 eligible_for_ects: true,
189 email: user_details.email,
190 grade: None,
191 passed: true,
192 },
193 CourseModuleCompletionGranter::Automatic,
194 )
195 .await?;
196 info!("Created a completion");
197 Ok(Some(completion))
198 } else {
199 Ok(None)
201 }
202 }
203}
204
205#[instrument(skip(conn))]
206async fn user_is_eligible_for_automatic_completion(
207 conn: &mut PgConnection,
208 course_module: &CourseModule,
209 user_id: Uuid,
210) -> ModelResult<bool> {
211 match &course_module.completion_policy {
212 CompletionPolicy::Automatic(requirements) => {
213 let eligible = user_passes_automatic_completion_exercise_tresholds(
214 conn,
215 user_id,
216 requirements,
217 course_module.course_id,
218 )
219 .await?;
220 if eligible {
221 if user_exercise_states::has_pending_manual_reviews_in_module(
224 conn,
225 user_id,
226 course_module.course_id,
227 requirements.course_module_id,
228 )
229 .await?
230 {
231 info!(
232 "The user has answers pending manual review in this module; not granting an automatic completion yet."
233 );
234 return Ok(false);
235 }
236 if requirements.requires_exam {
237 info!("To complete this module automatically, the user must pass an exam.");
238 user_has_passed_exam_for_the_course_based_on_points(
239 conn,
240 user_id,
241 course_module.course_id,
242 )
243 .await
244 } else {
245 Ok(true)
246 }
247 } else {
248 Ok(false)
249 }
250 }
251 CompletionPolicy::Manual => Ok(false),
252 }
253}
254
255#[instrument(skip(conn))]
263pub async fn user_can_take_exam(
264 conn: &mut PgConnection,
265 exam_id: Uuid,
266 user_id: Uuid,
267) -> ModelResult<bool> {
268 let course_ids = course_exams::get_course_ids_by_exam_id(conn, exam_id).await?;
269 let settings = user_course_settings::get_all_by_user_and_multiple_current_courses(
270 conn,
271 &course_ids,
272 user_id,
273 )
274 .await?;
275 let mut can_take_exam = true;
277 for course_id in course_ids {
278 let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
279 if let CompletionPolicy::Automatic(requirements) = &default_module.completion_policy {
280 if let Some(s) = settings.iter().find(|x| x.current_course_id == course_id) {
281 let eligible = user_passes_automatic_completion_exercise_tresholds(
282 conn,
283 s.user_id,
284 requirements,
285 s.current_course_id,
286 )
287 .await?;
288 if eligible {
289 can_take_exam = true;
291 break;
292 }
293 }
294 can_take_exam = false;
297 }
298 }
299 Ok(can_take_exam)
300}
301
302async fn user_has_passed_exam_for_the_course_based_on_points(
305 conn: &mut PgConnection,
306 user_id: Uuid,
307 course_id: Uuid,
308) -> ModelResult<bool> {
309 let now = Utc::now();
310 let exam_ids = course_exams::get_exam_ids_by_course_id(conn, course_id).await?;
311 for exam_id in exam_ids {
312 let exam = exams::get(conn, exam_id).await?;
313 if exam.minimum_points_treshold == 0 || exam.grade_manually {
315 continue;
316 }
317 if exam.ended_at_or(now, false) {
318 let points =
319 user_exercise_states::get_user_total_exam_points(conn, user_id, exam_id).await?;
320 if let Some(points) = points
321 && points >= exam.minimum_points_treshold as f32
322 {
323 return Ok(true);
324 }
325 }
326 }
327 Ok(false)
328}
329
330async fn user_passes_automatic_completion_exercise_tresholds(
331 conn: &mut PgConnection,
332 user_id: Uuid,
333 requirements: &AutomaticCompletionRequirements,
334 course_id: Uuid,
335) -> ModelResult<bool> {
336 let user_metrics = user_exercise_states::get_single_module_metrics(
337 conn,
338 course_id,
339 requirements.course_module_id,
340 user_id,
341 )
342 .await?;
343 let attempted_exercises: i32 = user_metrics.attempted_exercises.unwrap_or(0) as i32;
344 let exercise_points = user_metrics.score_given.unwrap_or(0.0) as i32;
345 let eligible = requirements.passes_exercise_tresholds(attempted_exercises, exercise_points);
346 Ok(eligible)
347}
348
349#[instrument(skip(conn))]
352async fn update_module_completion_prerequisite_statuses_for_user(
353 conn: &mut PgConnection,
354 user_id: Uuid,
355 course_id: Uuid,
356 base_module_completion_requires_n_submodule_completions: u32,
357) -> ModelResult<()> {
358 let default_course_module = course_modules::get_default_by_course_id(conn, course_id).await?;
359 let course_module_completions =
360 course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
361 .await?;
362 let default_module_is_completed = course_module_completions
363 .iter()
364 .any(|x| x.course_module_id == default_course_module.id);
365 let submodule_completions = course_module_completions
366 .iter()
367 .filter(|x| x.course_module_id != default_course_module.id)
368 .unique_by(|x| x.course_module_id)
369 .count();
370 let enough_submodule_completions = submodule_completions
371 >= base_module_completion_requires_n_submodule_completions.try_into()?;
372 let completions_needing_processing: Vec<_> = course_module_completions
373 .into_iter()
374 .filter(|x| !x.prerequisite_modules_completed)
375 .collect();
376 for completion in completions_needing_processing {
377 if completion.course_module_id == default_course_module.id {
378 if enough_submodule_completions {
379 course_module_completions::update_prerequisite_modules_completed(
380 conn,
381 completion.id,
382 true,
383 )
384 .await?;
385 }
386 } else if default_module_is_completed {
387 course_module_completions::update_prerequisite_modules_completed(
388 conn,
389 completion.id,
390 true,
391 )
392 .await?;
393 }
394 }
395 Ok(())
396}
397
398#[instrument(skip(conn))]
400pub async fn process_all_course_completions(
401 conn: &mut PgConnection,
402 course_id: Uuid,
403) -> ModelResult<()> {
404 info!("Reprocessing course completions");
405 let course = courses::get_course(conn, course_id).await?;
406 let submodule_completions_required = course
407 .base_module_completion_requires_n_submodule_completions
408 .try_into()?;
409 let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
410 let users =
412 crate::users::get_all_user_ids_with_user_exercise_states_on_course(conn, course_id).await?;
413 info!(users = ?users.len(), course_modules = ?course_modules.len(), ?submodule_completions_required, "Completion reprocessing info");
414 for course_module in course_modules.iter() {
415 info!(?course_module, "Course module information");
416 }
417 let mut tx = conn.begin().await?;
418 for user_id in users {
419 let mut num_completions = 0;
420 for course_module in course_modules.iter() {
421 let completion = create_automatic_course_module_completion_if_eligible(
422 &mut tx,
423 course_module,
424 user_id,
425 )
426 .await?;
427 if completion.is_some() {
428 num_completions += 1;
429 }
430 }
431 if num_completions > 0 {
432 update_module_completion_prerequisite_statuses_for_user(
433 &mut tx,
434 user_id,
435 course_id,
436 submodule_completions_required,
437 )
438 .await?;
439 }
440 }
441 tx.commit().await?;
442 info!("Reprocessing course module completions complete");
443 Ok(())
444}
445
446#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
447
448pub struct CourseInstanceCompletionSummary {
449 pub course_modules: Vec<CourseModule>,
450 pub users_with_course_module_completions: Vec<UserWithModuleCompletions>,
451}
452
453#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
454
455pub struct UserWithModuleCompletions {
456 pub completed_modules: Vec<CourseModuleCompletionWithRegistrationInfo>,
457 pub email: String,
458 pub first_name: Option<String>,
459 pub last_name: Option<String>,
460 pub user_id: Uuid,
461}
462
463#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
464
465pub struct UserCourseModuleCompletion {
466 pub course_module_id: Uuid,
467 pub grade: Option<i32>,
468 pub passed: bool,
469}
470
471impl From<CourseModuleCompletion> for UserCourseModuleCompletion {
472 fn from(course_module_completion: CourseModuleCompletion) -> Self {
473 Self {
474 course_module_id: course_module_completion.course_module_id,
475 grade: course_module_completion.grade,
476 passed: course_module_completion.passed,
477 }
478 }
479}
480
481impl UserWithModuleCompletions {
482 fn from_user_and_details(user: User, user_details: UserDetail) -> Self {
483 Self {
484 user_id: user.id,
485 first_name: user_details.first_name,
486 last_name: user_details.last_name,
487 email: user_details.email,
488 completed_modules: vec![],
489 }
490 }
491}
492
493pub async fn get_course_instance_completion_summary(
494 conn: &mut PgConnection,
495 course_instance: &CourseInstance,
496) -> ModelResult<CourseInstanceCompletionSummary> {
497 let course_modules = course_modules::get_by_course_id(conn, course_instance.course_id).await?;
498 let users_with_course_module_completions_list =
499 users::get_users_by_course_instance_enrollment(conn, course_instance.id).await?;
500 let user_id_to_details_map = crate::user_details::get_users_details_by_user_id_map(
501 conn,
502 &users_with_course_module_completions_list,
503 )
504 .await?;
505 let mut users_with_course_module_completions: HashMap<Uuid, UserWithModuleCompletions> =
506 users_with_course_module_completions_list
507 .into_iter()
508 .filter_map(|o| {
509 let details = user_id_to_details_map.get(&o.id);
510 details.map(|details| (o, details))
511 })
512 .map(|u| {
513 (
514 u.0.id,
515 UserWithModuleCompletions::from_user_and_details(u.0, u.1.clone()),
516 )
517 })
518 .collect();
519 let completions =
520 course_module_completions::get_all_with_registration_information_by_course_instance_id(
521 conn,
522 course_instance.id,
523 course_instance.course_id,
524 )
525 .await?;
526 completions.into_iter().for_each(|x| {
527 let user_with_completions = users_with_course_module_completions.get_mut(&x.user_id);
528 if let Some(completion) = user_with_completions {
529 completion.completed_modules.push(x);
530 }
531 });
532 Ok(CourseInstanceCompletionSummary {
533 course_modules,
534 users_with_course_module_completions: users_with_course_module_completions
535 .into_values()
536 .collect(),
537 })
538}
539
540#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
541
542pub struct TeacherManualCompletionRequest {
543 pub course_module_id: Uuid,
544 pub new_completions: Vec<TeacherManualCompletion>,
545 pub skip_duplicate_completions: bool,
546}
547
548#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
549
550pub struct TeacherManualCompletion {
551 pub user_id: Uuid,
552 pub grade: Option<i32>,
553 pub passed: bool,
554 pub completion_date: Option<DateTime<Utc>>,
555}
556
557pub async fn add_manual_completions(
558 conn: &mut PgConnection,
559 completion_giver_user_id: Uuid,
560 course_instance: &CourseInstance,
561 manual_completion_request: &TeacherManualCompletionRequest,
562) -> ModelResult<()> {
563 let course_module =
564 course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
565 if course_module.course_id != course_instance.course_id {
566 return Err(ModelError::new(
567 ModelErrorType::PreconditionFailed,
568 "Course module not part of the course.".to_string(),
569 None,
570 ));
571 }
572 let course = courses::get_course(conn, course_instance.course_id).await?;
573 let mut tx = conn.begin().await?;
574 for completion in manual_completion_request.new_completions.iter() {
575 let completion_receiver = users::get_by_id(&mut tx, completion.user_id).await?;
576 let completion_receiver_user_details =
577 crate::user_details::get_user_details_by_user_id(&mut tx, completion_receiver.id)
578 .await?;
579 let module_completed = course_module_completions::user_has_completed_course_module(
580 &mut tx,
581 completion.user_id,
582 manual_completion_request.course_module_id,
583 )
584 .await?;
585 if !module_completed || !manual_completion_request.skip_duplicate_completions {
586 course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
587 &mut tx,
588 NewCourseInstanceEnrollment {
589 user_id: completion_receiver.id,
590 course_id: course.id,
591 course_instance_id: course_instance.id,
592 },
593 )
594 .await?;
595
596 if completion.grade.is_some()
597 && (completion.grade > Some(5) || completion.grade < Some(0))
598 {
599 return Err(ModelError::new(
600 ModelErrorType::PreconditionFailed,
601 "Invalid grade".to_string(),
602 None,
603 ));
604 }
605 course_module_completions::insert(
606 &mut tx,
607 PKeyPolicy::Generate,
608 &NewCourseModuleCompletion {
609 course_id: course_instance.course_id,
610 course_module_id: manual_completion_request.course_module_id,
611 user_id: completion.user_id,
612 completion_date: completion.completion_date.unwrap_or_else(Utc::now),
613 completion_registration_attempt_date: None,
614 completion_language: course.language_code.clone(),
615 eligible_for_ects: true,
616 email: completion_receiver_user_details.email,
617 grade: completion.grade,
618 passed: if completion.grade == Some(0) {
619 false
620 } else {
621 completion.passed
622 },
623 },
624 CourseModuleCompletionGranter::User(completion_giver_user_id),
625 )
626 .await?;
627
628 crate::course_instance_enrollments::insert_enrollment_and_set_as_current(
630 &mut tx,
631 NewCourseInstanceEnrollment {
632 user_id: completion_receiver.id,
633 course_id: course.id,
634 course_instance_id: course_instance.id,
635 },
636 )
637 .await?;
638
639 update_module_completion_prerequisite_statuses_for_user(
640 &mut tx,
641 completion_receiver.id,
642 course.id,
643 course
644 .base_module_completion_requires_n_submodule_completions
645 .try_into()?,
646 )
647 .await?;
648 }
649
650 if suspected_cheaters::get_by_user_id_and_course_id(&mut tx, completion.user_id, course.id)
657 .await
658 .optional()?
659 .is_some()
660 {
661 suspected_cheaters::dismiss_by_user_id_and_course_id(
662 &mut tx,
663 completion.user_id,
664 course.id,
665 )
666 .await?;
667 }
668 }
669 tx.commit().await?;
670 Ok(())
671}
672
673#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
674
675pub struct ManualCompletionPreview {
676 pub already_completed_users: Vec<ManualCompletionPreviewUser>,
677 pub first_time_completing_users: Vec<ManualCompletionPreviewUser>,
678 pub non_enrolled_users: Vec<ManualCompletionPreviewUser>,
679}
680
681#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
682
683pub struct ManualCompletionPreviewUser {
684 pub user_id: Uuid,
685 pub first_name: Option<String>,
686 pub last_name: Option<String>,
687 pub grade: Option<i32>,
688 pub passed: bool,
689 pub previous_best_grade: Option<i32>,
690}
691
692pub async fn get_manual_completion_result_preview(
694 conn: &mut PgConnection,
695 course_instance: &CourseInstance,
696 manual_completion_request: &TeacherManualCompletionRequest,
697) -> ModelResult<ManualCompletionPreview> {
698 let course_module =
699 course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
700 if course_module.course_id != course_instance.course_id {
701 return Err(ModelError::new(
702 ModelErrorType::PreconditionFailed,
703 "Course module not part of the course.".to_string(),
704 None,
705 ));
706 }
707 let mut already_completed_users = vec![];
708 let mut first_time_completing_users = vec![];
709 let mut non_enrolled_users = vec![];
710 for completion in manual_completion_request.new_completions.iter() {
711 let user = users::get_by_id(conn, completion.user_id).await?;
712 let user_details = crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
713 let user = ManualCompletionPreviewUser {
714 user_id: user.id,
715 first_name: user_details.first_name,
716 last_name: user_details.last_name,
717 grade: completion.grade,
718 passed: completion.passed,
719 previous_best_grade: None,
720 };
721 let enrollment = course_instance_enrollments::get_by_user_and_course_instance_id(
722 conn,
723 completion.user_id,
724 course_instance.id,
725 )
726 .await
727 .optional()?;
728 if enrollment.is_none() {
729 non_enrolled_users.push(user.clone());
730 }
731 let module_completed = course_module_completions::user_has_completed_course_module(
732 conn,
733 completion.user_id,
734 manual_completion_request.course_module_id,
735 )
736 .await?;
737 if module_completed {
738 already_completed_users.push(user);
739 } else {
740 first_time_completing_users.push(user);
741 }
742 }
743 Ok(ManualCompletionPreview {
744 already_completed_users,
745 first_time_completing_users,
746 non_enrolled_users,
747 })
748}
749
750#[derive(Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)]
751
752pub struct UserCompletionInformation {
753 pub course_module_completion_id: Uuid,
754 pub course_name: String,
755 pub uh_course_code: String,
756 pub email: String,
757 pub ects_credits: Option<f32>,
758 pub enable_registering_completion_to_uh_open_university: bool,
759}
760
761pub async fn get_user_completion_information(
762 conn: &mut PgConnection,
763 user_id: Uuid,
764 course_module: &CourseModule,
765) -> ModelResult<UserCompletionInformation> {
766 let user = users::get_by_id(conn, user_id).await?;
767 let course = courses::get_course(conn, course_module.course_id).await?;
768 let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
769 conn,
770 course_module.id,
771 user.id,
772 )
773 .await?;
774 let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
776 ModelError::new(
777 ModelErrorType::InvalidRequest,
778 "Course module is missing uh_course_code.".to_string(),
779 None,
780 )
781 })?;
782 Ok(UserCompletionInformation {
783 course_module_completion_id: course_module_completion.id,
784 course_name: course_module
785 .name
786 .clone()
787 .unwrap_or_else(|| course.name.clone()),
788 uh_course_code,
789 ects_credits: course_module.ects_credits,
790 email: course_module_completion.email,
791 enable_registering_completion_to_uh_open_university: course_module
792 .enable_registering_completion_to_uh_open_university,
793 })
794}
795
796#[derive(Clone, PartialEq, Deserialize, Serialize, ToSchema)]
797
798pub struct UserModuleCompletionStatus {
799 pub completed: bool,
800 pub default: bool,
801 pub module_id: Uuid,
802 pub name: String,
803 pub order_number: i32,
804 pub prerequisite_modules_completed: bool,
805 pub grade: Option<i32>,
806 pub passed: Option<bool>,
807 pub enable_registering_completion_to_uh_open_university: bool,
808 pub certification_enabled: bool,
809 pub certificate_configuration_id: Option<Uuid>,
810}
811
812pub async fn get_user_module_completion_statuses_for_course(
814 conn: &mut PgConnection,
815 user_id: Uuid,
816 course_id: Uuid,
817) -> ModelResult<Vec<UserModuleCompletionStatus>> {
818 let course = courses::get_course(conn, course_id).await?;
819 let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
820
821 let course_module_completions_raw =
822 course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
823 .await?;
824
825 let course_module_completions: HashMap<Uuid, CourseModuleCompletion> =
826 course_module_completions_raw
827 .into_iter()
828 .sorted_by_key(|c| c.course_module_id)
829 .chunk_by(|c| c.course_module_id)
830 .into_iter()
831 .filter_map(|(module_id, group)| {
832 crate::course_module_completions::select_best_completion(group.collect())
833 .map(|best| (module_id, best))
834 })
835 .collect();
836
837 let all_default_certificate_configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(conn, course_id).await?;
838
839 let course_module_completion_statuses = course_modules
840 .into_iter()
841 .map(|module| {
842 let mut certificate_configuration_id = None;
843
844 let completion = course_module_completions
849 .get(&module.id)
850 .filter(|c| !c.needs_to_be_reviewed);
851 let passed = completion.map(|x| x.passed);
852 if module.certification_enabled && passed == Some(true) {
853 let default_certificate_configuration = all_default_certificate_configurations
855 .iter()
856 .find(|x| x.requirements.course_module_ids.contains(&module.id));
857 if let Some(default_certificate_configuration) = default_certificate_configuration {
858 certificate_configuration_id = Some(
859 default_certificate_configuration
860 .certificate_configuration
861 .id,
862 );
863 }
864 }
865 UserModuleCompletionStatus {
866 completed: completion.is_some(),
867 default: module.is_default_module(),
868 module_id: module.id,
869 name: module.name.unwrap_or_else(|| course.name.clone()),
870 order_number: module.order_number,
871 passed,
872 grade: completion.and_then(|x| x.grade),
873 prerequisite_modules_completed: completion
874 .is_some_and(|x| x.prerequisite_modules_completed),
875 enable_registering_completion_to_uh_open_university: module
876 .enable_registering_completion_to_uh_open_university,
877 certification_enabled: module.certification_enabled,
878 certificate_configuration_id,
879 }
880 })
881 .collect();
882 Ok(course_module_completion_statuses)
883}
884
885#[derive(Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)]
886
887pub struct CompletionRegistrationLink {
888 pub url: String,
889}
890
891pub async fn get_completion_registration_link_and_save_attempt(
892 conn: &mut PgConnection,
893 user_id: Uuid,
894 course_module: &CourseModule,
895) -> ModelResult<CompletionRegistrationLink> {
896 if !course_module.enable_registering_completion_to_uh_open_university {
897 return Err(ModelError::new(
898 ModelErrorType::InvalidRequest,
899 "Completion registration is not enabled for this course module.".to_string(),
900 None,
901 ));
902 }
903 let user = users::get_by_id(conn, user_id).await?;
904
905 let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
906 conn,
907 course_module.id,
908 user.id,
909 )
910 .await?;
911 course_module_completions::update_completion_registration_attempt_date(
912 conn,
913 course_module_completion.id,
914 Utc::now(),
915 )
916 .await?;
917 let registration_link = if let Some(link_override) =
918 course_module.completion_registration_link_override.as_ref()
919 {
920 link_override.clone()
921 } else {
922 let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
923 ModelError::new(
924 ModelErrorType::PreconditionFailed,
925 "Course module doesn't have an assossiated University of Helsinki course code."
926 .to_string(),
927 None,
928 )
929 })?;
930 open_university_registration_links::get_link_by_course_code(conn, &uh_course_code).await?
931 };
932
933 Ok(CompletionRegistrationLink {
934 url: registration_link,
935 })
936}
937
938#[cfg(test)]
939mod tests {
940 use chrono::Duration;
941 use user_exercise_states::{ReviewingStage, UserExerciseStateUpdate};
942
943 use super::*;
944
945 use crate::{
946 exercises::{ActivityProgress, GradingProgress},
947 suspected_cheaters::SuspectedCheaterStatus,
948 test_helper::*,
949 };
950
951 mod grant_automatic_completion_if_eligible {
952 use super::*;
953 use crate::{
954 chapters::NewChapter,
955 course_modules::{
956 self, AutomaticCompletionRequirements, CompletionPolicy, NewCourseModule,
957 },
958 exercises::{self, ActivityProgress, GradingProgress},
959 library::content_management,
960 user_exercise_states::{self, ReviewingStage, UserExerciseStateUpdate},
961 };
962
963 #[tokio::test]
964 async fn grants_automatic_completion_but_no_prerequisite_for_default_module() {
965 insert_data!(:tx);
966 let (mut tx, user, course, _instance, default_module, _submodule_1, _submodule_2) =
967 create_test_data(tx).await;
968 update_automatic_completion_status_and_grant_if_eligible(
969 tx.as_mut(),
970 &default_module,
971 user,
972 )
973 .await
974 .unwrap();
975 let statuses =
976 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
977 .await
978 .unwrap();
979 let status = statuses
980 .iter()
981 .find(|x| x.module_id == default_module.id)
982 .unwrap();
983 assert!(status.completed);
984 assert!(!status.prerequisite_modules_completed);
985 }
986
987 #[tokio::test]
988 async fn grants_automatic_completion_but_no_prerequisite_for_submodule() {
989 insert_data!(:tx);
990 let (mut tx, user, course, _instance, _default_module, submodule_1, _submodule_2) =
991 create_test_data(tx).await;
992 update_automatic_completion_status_and_grant_if_eligible(
993 tx.as_mut(),
994 &submodule_1,
995 user,
996 )
997 .await
998 .unwrap();
999 let statuses =
1000 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1001 .await
1002 .unwrap();
1003 let status = statuses
1004 .iter()
1005 .find(|x| x.module_id == submodule_1.id)
1006 .unwrap();
1007 assert!(status.completed);
1008 assert!(!status.prerequisite_modules_completed);
1009 }
1010
1011 #[tokio::test]
1012 async fn grants_automatic_completion_for_eligible_submodule_when_completing_default_module()
1013 {
1014 insert_data!(:tx);
1015 let (mut tx, user, course, _instance, default_module, submodule_1, submodule_2) =
1016 create_test_data(tx).await;
1017 update_automatic_completion_status_and_grant_if_eligible(
1018 tx.as_mut(),
1019 &default_module,
1020 user,
1021 )
1022 .await
1023 .unwrap();
1024 update_automatic_completion_status_and_grant_if_eligible(
1025 tx.as_mut(),
1026 &submodule_1,
1027 user,
1028 )
1029 .await
1030 .unwrap();
1031 update_automatic_completion_status_and_grant_if_eligible(
1032 tx.as_mut(),
1033 &submodule_2,
1034 user,
1035 )
1036 .await
1037 .unwrap();
1038 let statuses =
1039 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1040 .await
1041 .unwrap();
1042 statuses.iter().for_each(|x| {
1043 assert!(x.completed);
1044 assert!(x.prerequisite_modules_completed);
1045 });
1046 }
1047
1048 async fn create_test_data(
1049 mut tx: Tx<'_>,
1050 ) -> (
1051 Tx<'_>,
1052 Uuid,
1053 Uuid,
1054 Uuid,
1055 CourseModule,
1056 CourseModule,
1057 CourseModule,
1058 ) {
1059 insert_data!(tx: tx; :user, :org, :course, :instance, :course_module, :chapter, :page, :exercise);
1060 courses::set_cheater_detection_enabled(tx.as_mut(), course, false)
1064 .await
1065 .unwrap();
1066 let automatic_completion_policy =
1067 CompletionPolicy::Automatic(AutomaticCompletionRequirements {
1068 course_module_id: course_module.id,
1069 number_of_exercises_attempted_treshold: Some(0),
1070 number_of_points_treshold: Some(0),
1071 requires_exam: false,
1072 });
1073 courses::update_course_base_module_completion_count_requirement(tx.as_mut(), course, 1)
1074 .await
1075 .unwrap();
1076 let course_module_2 = course_modules::insert(
1077 tx.as_mut(),
1078 PKeyPolicy::Generate,
1079 &NewCourseModule::new(course, Some("Module 2".to_string()), 1),
1080 )
1081 .await
1082 .unwrap();
1083 let (chapter_2, page2) = content_management::create_new_chapter(
1084 tx.as_mut(),
1085 PKeyPolicy::Generate,
1086 &NewChapter {
1087 name: "chapter 2".to_string(),
1088 color: None,
1089 course_id: course,
1090 chapter_number: 2,
1091 front_page_id: None,
1092 opens_at: None,
1093 deadline: None,
1094 course_module_id: Some(course_module_2.id),
1095 },
1096 user,
1097 |_, _, _| unimplemented!(),
1098 |_| unimplemented!(),
1099 )
1100 .await
1101 .unwrap();
1102
1103 let exercise_2 = exercises::insert(
1104 tx.as_mut(),
1105 PKeyPolicy::Generate,
1106 course,
1107 "",
1108 page2.id,
1109 chapter_2.id,
1110 0,
1111 )
1112 .await
1113 .unwrap();
1114 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
1115 tx.as_mut(),
1116 user,
1117 exercise,
1118 Some(course),
1119 None,
1120 )
1121 .await
1122 .unwrap();
1123 user_exercise_states::update(
1124 tx.as_mut(),
1125 UserExerciseStateUpdate {
1126 id: user_exercise_state.id,
1127 score_given: Some(0.0),
1128 activity_progress: ActivityProgress::Completed,
1129 reviewing_stage: ReviewingStage::NotStarted,
1130 grading_progress: GradingProgress::FullyGraded,
1131 },
1132 )
1133 .await
1134 .unwrap();
1135 let user_exercise_state_2 = user_exercise_states::get_or_create_user_exercise_state(
1136 tx.as_mut(),
1137 user,
1138 exercise_2,
1139 Some(course),
1140 None,
1141 )
1142 .await
1143 .unwrap();
1144 user_exercise_states::update(
1145 tx.as_mut(),
1146 UserExerciseStateUpdate {
1147 id: user_exercise_state_2.id,
1148 score_given: Some(0.0),
1149 activity_progress: ActivityProgress::Completed,
1150 reviewing_stage: ReviewingStage::NotStarted,
1151 grading_progress: GradingProgress::FullyGraded,
1152 },
1153 )
1154 .await
1155 .unwrap();
1156 let default_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1157 .await
1158 .unwrap();
1159 let default_module = course_modules::update_automatic_completion_status(
1160 tx.as_mut(),
1161 default_module.id,
1162 &automatic_completion_policy,
1163 )
1164 .await
1165 .unwrap();
1166 let course_module = course_modules::update_automatic_completion_status(
1167 tx.as_mut(),
1168 course_module.id,
1169 &automatic_completion_policy,
1170 )
1171 .await
1172 .unwrap();
1173 let course_module_2 = course_modules::update_automatic_completion_status(
1174 tx.as_mut(),
1175 course_module_2.id,
1176 &automatic_completion_policy,
1177 )
1178 .await
1179 .unwrap();
1180 (
1181 tx,
1182 user,
1183 course,
1184 instance.id,
1185 default_module,
1186 course_module,
1187 course_module_2,
1188 )
1189 }
1190 }
1191
1192 #[tokio::test]
1193 async fn tags_suspected_cheater() {
1194 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1195
1196 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1197 .await
1198 .unwrap();
1199 let state = user_exercise_states::get_or_create_user_exercise_state(
1200 tx.as_mut(),
1201 user,
1202 exercise,
1203 Some(course),
1204 None,
1205 )
1206 .await
1207 .unwrap();
1208 user_exercise_states::update(
1209 tx.as_mut(),
1210 UserExerciseStateUpdate {
1211 id: state.id,
1212 score_given: Some(10.0),
1213 activity_progress: ActivityProgress::Completed,
1214 reviewing_stage: ReviewingStage::NotStarted,
1215 grading_progress: GradingProgress::FullyGraded,
1216 },
1217 )
1218 .await
1219 .unwrap();
1220
1221 let completion = course_module_completions::insert(
1222 tx.as_mut(),
1223 PKeyPolicy::Generate,
1224 &NewCourseModuleCompletion {
1225 course_id: course,
1226 course_module_id: course_module.id,
1227 user_id: user,
1228 completion_date: Utc::now() + Duration::days(1),
1229 completion_registration_attempt_date: None,
1230 completion_language: "en-US".to_string(),
1231 eligible_for_ects: false,
1232 email: "email".to_string(),
1233 grade: None,
1234 passed: true,
1235 },
1236 CourseModuleCompletionGranter::Automatic,
1237 )
1238 .await
1239 .unwrap();
1240 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1241 tx.as_mut(),
1242 course_module.id,
1243 259200,
1244 )
1245 .await
1246 .unwrap();
1247 check_and_insert_suspected_cheaters(
1248 tx.as_mut(),
1249 user,
1250 course,
1251 thresholds.duration_seconds,
1252 completion,
1253 )
1254 .await
1255 .unwrap();
1256
1257 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1258 tx.as_mut(),
1259 course,
1260 SuspectedCheaterStatus::Flagged,
1261 )
1262 .await
1263 .unwrap();
1264 assert_eq!(cheaters.len(), 1);
1265 assert_eq!(cheaters[0].user_id, user);
1266 }
1267
1268 #[tokio::test]
1269 async fn tagging_suspected_cheater_is_idempotent() {
1270 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1271
1272 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1273 .await
1274 .unwrap();
1275 let state = user_exercise_states::get_or_create_user_exercise_state(
1276 tx.as_mut(),
1277 user,
1278 exercise,
1279 Some(course),
1280 None,
1281 )
1282 .await
1283 .unwrap();
1284 user_exercise_states::update(
1285 tx.as_mut(),
1286 UserExerciseStateUpdate {
1287 id: state.id,
1288 score_given: Some(10.0),
1289 activity_progress: ActivityProgress::Completed,
1290 reviewing_stage: ReviewingStage::NotStarted,
1291 grading_progress: GradingProgress::FullyGraded,
1292 },
1293 )
1294 .await
1295 .unwrap();
1296
1297 let completion = course_module_completions::insert(
1298 tx.as_mut(),
1299 PKeyPolicy::Generate,
1300 &NewCourseModuleCompletion {
1301 course_id: course,
1302 course_module_id: course_module.id,
1303 user_id: user,
1304 completion_date: Utc::now() + Duration::days(1),
1305 completion_registration_attempt_date: None,
1306 completion_language: "en-US".to_string(),
1307 eligible_for_ects: false,
1308 email: "email".to_string(),
1309 grade: None,
1310 passed: true,
1311 },
1312 CourseModuleCompletionGranter::Automatic,
1313 )
1314 .await
1315 .unwrap();
1316 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1317 tx.as_mut(),
1318 course_module.id,
1319 259200,
1320 )
1321 .await
1322 .unwrap();
1323 check_and_insert_suspected_cheaters(
1324 tx.as_mut(),
1325 user,
1326 course,
1327 thresholds.duration_seconds,
1328 completion.clone(),
1329 )
1330 .await
1331 .unwrap();
1332 check_and_insert_suspected_cheaters(
1333 tx.as_mut(),
1334 user,
1335 course,
1336 thresholds.duration_seconds,
1337 completion,
1338 )
1339 .await
1340 .unwrap();
1341
1342 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1343 tx.as_mut(),
1344 course,
1345 SuspectedCheaterStatus::Flagged,
1346 )
1347 .await
1348 .unwrap();
1349 assert_eq!(cheaters.len(), 1);
1350 let completion_needing_review =
1351 course_module_completions::get_latest_by_course_and_user_ids(
1352 tx.as_mut(),
1353 course_module.id,
1354 user,
1355 )
1356 .await
1357 .unwrap();
1358 assert!(completion_needing_review.needs_to_be_reviewed);
1359
1360 suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1361 .await
1362 .unwrap();
1363 let archived_completion = course_module_completions::get_latest_by_course_and_user_ids(
1364 tx.as_mut(),
1365 course_module.id,
1366 user,
1367 )
1368 .await
1369 .unwrap();
1370 assert!(!archived_completion.needs_to_be_reviewed);
1371 check_and_insert_suspected_cheaters(
1372 tx.as_mut(),
1373 user,
1374 course,
1375 thresholds.duration_seconds,
1376 archived_completion,
1377 )
1378 .await
1379 .unwrap();
1380
1381 let visible_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1382 tx.as_mut(),
1383 course,
1384 SuspectedCheaterStatus::Flagged,
1385 )
1386 .await
1387 .unwrap();
1388 let archived_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1389 tx.as_mut(),
1390 course,
1391 SuspectedCheaterStatus::Dismissed,
1392 )
1393 .await
1394 .unwrap();
1395 assert!(visible_cheaters.is_empty());
1396 assert_eq!(archived_cheaters.len(), 1);
1397 let rechecked_completion = course_module_completions::get_latest_by_course_and_user_ids(
1398 tx.as_mut(),
1399 course_module.id,
1400 user,
1401 )
1402 .await
1403 .unwrap();
1404 assert!(!rechecked_completion.needs_to_be_reviewed);
1405 }
1406
1407 #[tokio::test]
1408 async fn confirming_then_dismissing_restores_grade() {
1409 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1410
1411 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1412 .await
1413 .unwrap();
1414 let state = user_exercise_states::get_or_create_user_exercise_state(
1415 tx.as_mut(),
1416 user,
1417 exercise,
1418 Some(course),
1419 None,
1420 )
1421 .await
1422 .unwrap();
1423 user_exercise_states::update(
1424 tx.as_mut(),
1425 UserExerciseStateUpdate {
1426 id: state.id,
1427 score_given: Some(10.0),
1428 activity_progress: ActivityProgress::Completed,
1429 reviewing_stage: ReviewingStage::NotStarted,
1430 grading_progress: GradingProgress::FullyGraded,
1431 },
1432 )
1433 .await
1434 .unwrap();
1435
1436 let completion = course_module_completions::insert(
1438 tx.as_mut(),
1439 PKeyPolicy::Generate,
1440 &NewCourseModuleCompletion {
1441 course_id: course,
1442 course_module_id: course_module.id,
1443 user_id: user,
1444 completion_date: Utc::now() + Duration::days(1),
1445 completion_registration_attempt_date: None,
1446 completion_language: "en-US".to_string(),
1447 eligible_for_ects: false,
1448 email: "email".to_string(),
1449 grade: Some(4),
1450 passed: true,
1451 },
1452 CourseModuleCompletionGranter::Automatic,
1453 )
1454 .await
1455 .unwrap();
1456 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1457 tx.as_mut(),
1458 course_module.id,
1459 259200,
1460 )
1461 .await
1462 .unwrap();
1463 check_and_insert_suspected_cheaters(
1464 tx.as_mut(),
1465 user,
1466 course,
1467 thresholds.duration_seconds,
1468 completion,
1469 )
1470 .await
1471 .unwrap();
1472
1473 suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
1475 .await
1476 .unwrap();
1477 let failed = course_module_completions::get_latest_by_course_and_user_ids(
1478 tx.as_mut(),
1479 course_module.id,
1480 user,
1481 )
1482 .await
1483 .unwrap();
1484 assert!(!failed.passed);
1485 assert_eq!(failed.grade, Some(0));
1486 let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1487 tx.as_mut(),
1488 course,
1489 SuspectedCheaterStatus::ConfirmedCheating,
1490 )
1491 .await
1492 .unwrap();
1493 assert_eq!(confirmed.len(), 1);
1494
1495 suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1497 .await
1498 .unwrap();
1499 let restored = course_module_completions::get_latest_by_course_and_user_ids(
1500 tx.as_mut(),
1501 course_module.id,
1502 user,
1503 )
1504 .await
1505 .unwrap();
1506 assert!(restored.passed);
1507 assert_eq!(restored.grade, Some(4));
1508 assert!(!restored.needs_to_be_reviewed);
1509 let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1510 tx.as_mut(),
1511 course,
1512 SuspectedCheaterStatus::Dismissed,
1513 )
1514 .await
1515 .unwrap();
1516 assert_eq!(dismissed.len(), 1);
1517 }
1518
1519 #[tokio::test]
1520 async fn manual_completion_prevents_flagging() {
1521 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module);
1522
1523 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1524 .await
1525 .unwrap();
1526
1527 let teacher = users::insert(
1529 tx.as_mut(),
1530 PKeyPolicy::Generate,
1531 "teacher-vouching@example.com",
1532 Some("Teacher"),
1533 Some("McVouch"),
1534 )
1535 .await
1536 .unwrap();
1537 course_module_completions::insert(
1538 tx.as_mut(),
1539 PKeyPolicy::Generate,
1540 &NewCourseModuleCompletion {
1541 course_id: course,
1542 course_module_id: course_module.id,
1543 user_id: user,
1544 completion_date: Utc::now(),
1545 completion_registration_attempt_date: None,
1546 completion_language: "en-US".to_string(),
1547 eligible_for_ects: false,
1548 email: "email".to_string(),
1549 grade: Some(5),
1550 passed: true,
1551 },
1552 CourseModuleCompletionGranter::User(teacher),
1553 )
1554 .await
1555 .unwrap();
1556
1557 let automatic_completion = course_module_completions::insert(
1559 tx.as_mut(),
1560 PKeyPolicy::Generate,
1561 &NewCourseModuleCompletion {
1562 course_id: course,
1563 course_module_id: course_module.id,
1564 user_id: user,
1565 completion_date: Utc::now() + Duration::days(1),
1566 completion_registration_attempt_date: None,
1567 completion_language: "en-US".to_string(),
1568 eligible_for_ects: false,
1569 email: "email".to_string(),
1570 grade: None,
1571 passed: true,
1572 },
1573 CourseModuleCompletionGranter::Automatic,
1574 )
1575 .await
1576 .unwrap();
1577 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1578 tx.as_mut(),
1579 course_module.id,
1580 259200,
1581 )
1582 .await
1583 .unwrap();
1584 check_and_insert_suspected_cheaters(
1585 tx.as_mut(),
1586 user,
1587 course,
1588 thresholds.duration_seconds,
1589 automatic_completion,
1590 )
1591 .await
1592 .unwrap();
1593
1594 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1596 tx.as_mut(),
1597 course,
1598 SuspectedCheaterStatus::Flagged,
1599 )
1600 .await
1601 .unwrap();
1602 assert!(cheaters.is_empty());
1603 }
1604
1605 #[tokio::test]
1606 async fn adding_manual_completion_dismisses_confirmed_suspicion_and_restores_grade() {
1607 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1608
1609 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1610 .await
1611 .unwrap();
1612 let state = user_exercise_states::get_or_create_user_exercise_state(
1613 tx.as_mut(),
1614 user,
1615 exercise,
1616 Some(course),
1617 None,
1618 )
1619 .await
1620 .unwrap();
1621 user_exercise_states::update(
1622 tx.as_mut(),
1623 UserExerciseStateUpdate {
1624 id: state.id,
1625 score_given: Some(10.0),
1626 activity_progress: ActivityProgress::Completed,
1627 reviewing_stage: ReviewingStage::NotStarted,
1628 grading_progress: GradingProgress::FullyGraded,
1629 },
1630 )
1631 .await
1632 .unwrap();
1633
1634 let completion = course_module_completions::insert(
1636 tx.as_mut(),
1637 PKeyPolicy::Generate,
1638 &NewCourseModuleCompletion {
1639 course_id: course,
1640 course_module_id: course_module.id,
1641 user_id: user,
1642 completion_date: Utc::now() + Duration::days(1),
1643 completion_registration_attempt_date: None,
1644 completion_language: "en-US".to_string(),
1645 eligible_for_ects: false,
1646 email: "email".to_string(),
1647 grade: Some(4),
1648 passed: true,
1649 },
1650 CourseModuleCompletionGranter::Automatic,
1651 )
1652 .await
1653 .unwrap();
1654 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1655 tx.as_mut(),
1656 course_module.id,
1657 259200,
1658 )
1659 .await
1660 .unwrap();
1661 check_and_insert_suspected_cheaters(
1662 tx.as_mut(),
1663 user,
1664 course,
1665 thresholds.duration_seconds,
1666 completion,
1667 )
1668 .await
1669 .unwrap();
1670 suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
1671 .await
1672 .unwrap();
1673
1674 let teacher = users::insert(
1677 tx.as_mut(),
1678 PKeyPolicy::Generate,
1679 "teacher-vouching@example.com",
1680 Some("Teacher"),
1681 Some("McVouch"),
1682 )
1683 .await
1684 .unwrap();
1685 add_manual_completions(
1686 tx.as_mut(),
1687 teacher,
1688 &instance,
1689 &TeacherManualCompletionRequest {
1690 course_module_id: course_module.id,
1691 new_completions: vec![TeacherManualCompletion {
1692 user_id: user,
1693 grade: Some(5),
1694 passed: true,
1695 completion_date: None,
1696 }],
1697 skip_duplicate_completions: false,
1698 },
1699 )
1700 .await
1701 .unwrap();
1702
1703 let flagged = suspected_cheaters::get_all_suspected_cheaters_in_course(
1705 tx.as_mut(),
1706 course,
1707 SuspectedCheaterStatus::Flagged,
1708 )
1709 .await
1710 .unwrap();
1711 let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1712 tx.as_mut(),
1713 course,
1714 SuspectedCheaterStatus::ConfirmedCheating,
1715 )
1716 .await
1717 .unwrap();
1718 let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1719 tx.as_mut(),
1720 course,
1721 SuspectedCheaterStatus::Dismissed,
1722 )
1723 .await
1724 .unwrap();
1725 assert!(flagged.is_empty());
1726 assert!(confirmed.is_empty());
1727 assert_eq!(dismissed.len(), 1);
1728
1729 let restored =
1731 course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
1732 tx.as_mut(),
1733 course_module.id,
1734 course,
1735 user,
1736 )
1737 .await
1738 .unwrap();
1739 assert!(restored.passed);
1740 assert_eq!(restored.grade, Some(4));
1741 assert!(!restored.needs_to_be_reviewed);
1742 }
1743
1744 #[tokio::test]
1745 async fn doesnt_tag_suspected_cheater() {
1746 insert_data!(:tx, user:user, :org, :course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1747
1748 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1749 .await
1750 .unwrap();
1751 let state = user_exercise_states::get_or_create_user_exercise_state(
1752 tx.as_mut(),
1753 user,
1754 exercise,
1755 Some(course),
1756 None,
1757 )
1758 .await
1759 .unwrap();
1760 user_exercise_states::update(
1761 tx.as_mut(),
1762 UserExerciseStateUpdate {
1763 id: state.id,
1764 score_given: Some(9.0),
1765 activity_progress: ActivityProgress::Completed,
1766 reviewing_stage: ReviewingStage::NotStarted,
1767 grading_progress: GradingProgress::FullyGraded,
1768 },
1769 )
1770 .await
1771 .unwrap();
1772
1773 course_module_completions::insert(
1774 tx.as_mut(),
1775 PKeyPolicy::Generate,
1776 &NewCourseModuleCompletion {
1777 course_id: course,
1778 course_module_id: course_module.id,
1779 user_id: user,
1780 completion_date: Utc::now() + Duration::days(3),
1781 completion_registration_attempt_date: None,
1782 completion_language: "en-US".to_string(),
1783 eligible_for_ects: false,
1784 email: "email".to_string(),
1785 grade: Some(9),
1786 passed: true,
1787 },
1788 CourseModuleCompletionGranter::Automatic,
1789 )
1790 .await
1791 .unwrap();
1792 suspected_cheaters::insert_thresholds_by_module_id(tx.as_mut(), course_module.id, 172800)
1793 .await
1794 .unwrap();
1795 update_automatic_completion_status_and_grant_if_eligible(tx.as_mut(), &course_module, user)
1796 .await
1797 .unwrap();
1798
1799 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1800 tx.as_mut(),
1801 course,
1802 SuspectedCheaterStatus::Flagged,
1803 )
1804 .await
1805 .unwrap();
1806 assert!(cheaters.is_empty());
1807 }
1808
1809 }