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