1use chrono::{DateTime, Utc};
2use itertools::Itertools;
3use std::collections::{HashMap, HashSet};
4use utoipa::ToSchema;
5
6use crate::{
7 course_exams,
8 course_instance_enrollments::{self, NewCourseInstanceEnrollment},
9 course_instances::{self, CourseInstance},
10 course_module_completions::{
11 self, CourseModuleCompletion, CourseModuleCompletionGranter,
12 CourseModuleCompletionWithRegistrationInfo, NewCourseModuleCompletion,
13 },
14 course_modules::{self, AutomaticCompletionRequirements, CompletionPolicy, CourseModule},
15 courses, exams, 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: Option<String>,
757 pub email: String,
758 pub ects_credits: Option<f32>,
759 pub enable_registering_completion_to_uh_open_university: bool,
760 pub enable_credit_registration_via_suotar: bool,
761}
762
763pub async fn get_user_completion_information(
764 conn: &mut PgConnection,
765 user_id: Uuid,
766 course_module: &CourseModule,
767) -> ModelResult<UserCompletionInformation> {
768 let user = users::get_by_id(conn, user_id).await?;
769 let course = courses::get_course(conn, course_module.course_id).await?;
770 let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
771 conn,
772 course_module.id,
773 user.id,
774 )
775 .await?;
776 let credit_registration_config =
777 course_modules::get_credit_registration_config(conn, course_module.id).await?;
778 if course_module.uh_course_code.is_none()
781 && !credit_registration_config.enable_credit_registration_via_suotar
782 {
783 return Err(ModelError::new(
784 ModelErrorType::InvalidRequest,
785 "Course module is missing uh_course_code.".to_string(),
786 None,
787 ));
788 }
789 Ok(UserCompletionInformation {
790 course_module_completion_id: course_module_completion.id,
791 course_name: course_module
792 .name
793 .clone()
794 .unwrap_or_else(|| course.name.clone()),
795 uh_course_code: course_module.uh_course_code.clone(),
796 ects_credits: course_module.ects_credits,
797 email: course_module_completion.email,
798 enable_registering_completion_to_uh_open_university: course_module
799 .enable_registering_completion_to_uh_open_university,
800 enable_credit_registration_via_suotar: credit_registration_config
801 .enable_credit_registration_via_suotar,
802 })
803}
804
805#[derive(Clone, PartialEq, Deserialize, Serialize, ToSchema)]
806
807pub struct UserModuleCompletionStatus {
808 pub completed: bool,
809 pub default: bool,
810 pub module_id: Uuid,
811 pub name: String,
812 pub order_number: i32,
813 pub prerequisite_modules_completed: bool,
814 pub grade: Option<i32>,
815 pub passed: Option<bool>,
816 pub enable_registering_completion_to_uh_open_university: bool,
817 pub enable_credit_registration_via_suotar: bool,
818 pub certification_enabled: bool,
819 pub certificate_configuration_id: Option<Uuid>,
820}
821
822pub async fn get_user_module_completion_statuses_for_course(
824 conn: &mut PgConnection,
825 user_id: Uuid,
826 course_id: Uuid,
827) -> ModelResult<Vec<UserModuleCompletionStatus>> {
828 let course = courses::get_course(conn, course_id).await?;
829 let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
830
831 let course_module_completions_raw =
832 course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
833 .await?;
834
835 let course_module_completions: HashMap<Uuid, CourseModuleCompletion> =
836 course_module_completions_raw
837 .into_iter()
838 .sorted_by_key(|c| c.course_module_id)
839 .chunk_by(|c| c.course_module_id)
840 .into_iter()
841 .filter_map(|(module_id, group)| {
842 crate::course_module_completions::select_best_completion(group.collect())
843 .map(|best| (module_id, best))
844 })
845 .collect();
846
847 let all_default_certificate_configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(conn, course_id).await?;
848
849 let credit_registration_enabled_module_ids: HashSet<Uuid> =
850 course_modules::get_credit_registration_enabled_ids_for_course(conn, course_id)
851 .await?
852 .into_iter()
853 .collect();
854
855 let course_module_completion_statuses = course_modules
856 .into_iter()
857 .map(|module| {
858 let mut certificate_configuration_id = None;
859
860 let completion = course_module_completions
865 .get(&module.id)
866 .filter(|c| !c.needs_to_be_reviewed);
867 let passed = completion.map(|x| x.passed);
868 if module.certification_enabled && passed == Some(true) {
869 let default_certificate_configuration = all_default_certificate_configurations
871 .iter()
872 .find(|x| x.requirements.course_module_ids.contains(&module.id));
873 if let Some(default_certificate_configuration) = default_certificate_configuration {
874 certificate_configuration_id = Some(
875 default_certificate_configuration
876 .certificate_configuration
877 .id,
878 );
879 }
880 }
881 UserModuleCompletionStatus {
882 completed: completion.is_some(),
883 default: module.is_default_module(),
884 module_id: module.id,
885 name: module.name.unwrap_or_else(|| course.name.clone()),
886 order_number: module.order_number,
887 passed,
888 grade: completion.and_then(|x| x.grade),
889 prerequisite_modules_completed: completion
890 .is_some_and(|x| x.prerequisite_modules_completed),
891 enable_registering_completion_to_uh_open_university: module
892 .enable_registering_completion_to_uh_open_university,
893 enable_credit_registration_via_suotar: credit_registration_enabled_module_ids
894 .contains(&module.id),
895 certification_enabled: module.certification_enabled,
896 certificate_configuration_id,
897 }
898 })
899 .collect();
900 Ok(course_module_completion_statuses)
901}
902
903#[derive(Clone, PartialEq, Deserialize, Serialize, utoipa::ToSchema)]
904
905pub struct CompletionRegistrationLink {
906 pub url: String,
907}
908
909pub async fn get_completion_registration_link_and_save_attempt(
910 conn: &mut PgConnection,
911 user_id: Uuid,
912 course_module: &CourseModule,
913) -> ModelResult<CompletionRegistrationLink> {
914 if !course_module.enable_registering_completion_to_uh_open_university {
915 return Err(ModelError::new(
916 ModelErrorType::InvalidRequest,
917 "Completion registration is not enabled for this course module.".to_string(),
918 None,
919 ));
920 }
921 let user = users::get_by_id(conn, user_id).await?;
922
923 let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
924 conn,
925 course_module.id,
926 user.id,
927 )
928 .await?;
929 course_module_completions::update_completion_registration_attempt_date(
930 conn,
931 course_module_completion.id,
932 Utc::now(),
933 )
934 .await?;
935 let registration_link = if let Some(link_override) =
936 course_module.completion_registration_link_override.as_ref()
937 {
938 link_override.clone()
939 } else {
940 let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
941 ModelError::new(
942 ModelErrorType::PreconditionFailed,
943 "Course module doesn't have an assossiated University of Helsinki course code."
944 .to_string(),
945 None,
946 )
947 })?;
948 open_university_registration_links::get_link_by_course_code(conn, &uh_course_code).await?
949 };
950
951 Ok(CompletionRegistrationLink {
952 url: registration_link,
953 })
954}
955
956#[cfg(test)]
957mod tests {
958 use chrono::Duration;
959 use user_exercise_states::{ReviewingStage, UserExerciseStateUpdate};
960
961 use super::*;
962
963 use crate::{
964 exercises::{ActivityProgress, GradingProgress},
965 suspected_cheaters::SuspectedCheaterStatus,
966 test_helper::*,
967 };
968
969 mod grant_automatic_completion_if_eligible {
970 use super::*;
971 use crate::{
972 chapters::NewChapter,
973 course_modules::{
974 self, AutomaticCompletionRequirements, CompletionPolicy, NewCourseModule,
975 },
976 exercises::{self, ActivityProgress, GradingProgress},
977 library::content_management,
978 user_exercise_states::{self, ReviewingStage, UserExerciseStateUpdate},
979 };
980
981 #[tokio::test]
982 async fn grants_automatic_completion_but_no_prerequisite_for_default_module() {
983 insert_data!(:tx);
984 let (mut tx, user, course, _instance, default_module, _submodule_1, _submodule_2) =
985 create_test_data(tx).await;
986 update_automatic_completion_status_and_grant_if_eligible(
987 tx.as_mut(),
988 &default_module,
989 user,
990 )
991 .await
992 .unwrap();
993 let statuses =
994 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
995 .await
996 .unwrap();
997 let status = statuses
998 .iter()
999 .find(|x| x.module_id == default_module.id)
1000 .unwrap();
1001 assert!(status.completed);
1002 assert!(!status.prerequisite_modules_completed);
1003 }
1004
1005 #[tokio::test]
1006 async fn grants_automatic_completion_but_no_prerequisite_for_submodule() {
1007 insert_data!(:tx);
1008 let (mut tx, user, course, _instance, _default_module, submodule_1, _submodule_2) =
1009 create_test_data(tx).await;
1010 update_automatic_completion_status_and_grant_if_eligible(
1011 tx.as_mut(),
1012 &submodule_1,
1013 user,
1014 )
1015 .await
1016 .unwrap();
1017 let statuses =
1018 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1019 .await
1020 .unwrap();
1021 let status = statuses
1022 .iter()
1023 .find(|x| x.module_id == submodule_1.id)
1024 .unwrap();
1025 assert!(status.completed);
1026 assert!(!status.prerequisite_modules_completed);
1027 }
1028
1029 #[tokio::test]
1030 async fn grants_automatic_completion_for_eligible_submodule_when_completing_default_module()
1031 {
1032 insert_data!(:tx);
1033 let (mut tx, user, course, _instance, default_module, submodule_1, submodule_2) =
1034 create_test_data(tx).await;
1035 update_automatic_completion_status_and_grant_if_eligible(
1036 tx.as_mut(),
1037 &default_module,
1038 user,
1039 )
1040 .await
1041 .unwrap();
1042 update_automatic_completion_status_and_grant_if_eligible(
1043 tx.as_mut(),
1044 &submodule_1,
1045 user,
1046 )
1047 .await
1048 .unwrap();
1049 update_automatic_completion_status_and_grant_if_eligible(
1050 tx.as_mut(),
1051 &submodule_2,
1052 user,
1053 )
1054 .await
1055 .unwrap();
1056 let statuses =
1057 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
1058 .await
1059 .unwrap();
1060 statuses.iter().for_each(|x| {
1061 assert!(x.completed);
1062 assert!(x.prerequisite_modules_completed);
1063 });
1064 }
1065
1066 async fn create_test_data(
1067 mut tx: Tx<'_>,
1068 ) -> (
1069 Tx<'_>,
1070 Uuid,
1071 Uuid,
1072 Uuid,
1073 CourseModule,
1074 CourseModule,
1075 CourseModule,
1076 ) {
1077 insert_data!(tx: tx; :user, :org, :course, :instance, :course_module, :chapter, :page, :exercise);
1078 courses::set_cheater_detection_enabled(tx.as_mut(), course, false)
1082 .await
1083 .unwrap();
1084 let automatic_completion_policy =
1085 CompletionPolicy::Automatic(AutomaticCompletionRequirements {
1086 course_module_id: course_module.id,
1087 number_of_exercises_attempted_treshold: Some(0),
1088 number_of_points_treshold: Some(0),
1089 requires_exam: false,
1090 });
1091 courses::update_course_base_module_completion_count_requirement(tx.as_mut(), course, 1)
1092 .await
1093 .unwrap();
1094 let course_module_2 = course_modules::insert(
1095 tx.as_mut(),
1096 PKeyPolicy::Generate,
1097 &NewCourseModule::new(course, Some("Module 2".to_string()), 1),
1098 )
1099 .await
1100 .unwrap();
1101 let (chapter_2, page2) = content_management::create_new_chapter(
1102 tx.as_mut(),
1103 PKeyPolicy::Generate,
1104 &NewChapter {
1105 name: "chapter 2".to_string(),
1106 color: None,
1107 course_id: course,
1108 chapter_number: 2,
1109 front_page_id: None,
1110 opens_at: None,
1111 deadline: None,
1112 course_module_id: Some(course_module_2.id),
1113 },
1114 user,
1115 |_, _, _| unimplemented!(),
1116 |_| unimplemented!(),
1117 )
1118 .await
1119 .unwrap();
1120
1121 let exercise_2 = exercises::insert(
1122 tx.as_mut(),
1123 PKeyPolicy::Generate,
1124 course,
1125 "",
1126 page2.id,
1127 chapter_2.id,
1128 0,
1129 )
1130 .await
1131 .unwrap();
1132 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
1133 tx.as_mut(),
1134 user,
1135 exercise,
1136 Some(course),
1137 None,
1138 )
1139 .await
1140 .unwrap();
1141 user_exercise_states::update(
1142 tx.as_mut(),
1143 UserExerciseStateUpdate {
1144 id: user_exercise_state.id,
1145 score_given: Some(0.0),
1146 activity_progress: ActivityProgress::Completed,
1147 reviewing_stage: ReviewingStage::NotStarted,
1148 grading_progress: GradingProgress::FullyGraded,
1149 },
1150 )
1151 .await
1152 .unwrap();
1153 let user_exercise_state_2 = user_exercise_states::get_or_create_user_exercise_state(
1154 tx.as_mut(),
1155 user,
1156 exercise_2,
1157 Some(course),
1158 None,
1159 )
1160 .await
1161 .unwrap();
1162 user_exercise_states::update(
1163 tx.as_mut(),
1164 UserExerciseStateUpdate {
1165 id: user_exercise_state_2.id,
1166 score_given: Some(0.0),
1167 activity_progress: ActivityProgress::Completed,
1168 reviewing_stage: ReviewingStage::NotStarted,
1169 grading_progress: GradingProgress::FullyGraded,
1170 },
1171 )
1172 .await
1173 .unwrap();
1174 let default_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1175 .await
1176 .unwrap();
1177 let default_module = course_modules::update_automatic_completion_status(
1178 tx.as_mut(),
1179 default_module.id,
1180 &automatic_completion_policy,
1181 )
1182 .await
1183 .unwrap();
1184 let course_module = course_modules::update_automatic_completion_status(
1185 tx.as_mut(),
1186 course_module.id,
1187 &automatic_completion_policy,
1188 )
1189 .await
1190 .unwrap();
1191 let course_module_2 = course_modules::update_automatic_completion_status(
1192 tx.as_mut(),
1193 course_module_2.id,
1194 &automatic_completion_policy,
1195 )
1196 .await
1197 .unwrap();
1198 (
1199 tx,
1200 user,
1201 course,
1202 instance.id,
1203 default_module,
1204 course_module,
1205 course_module_2,
1206 )
1207 }
1208 }
1209
1210 #[tokio::test]
1211 async fn tags_suspected_cheater() {
1212 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1213
1214 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1215 .await
1216 .unwrap();
1217 let state = user_exercise_states::get_or_create_user_exercise_state(
1218 tx.as_mut(),
1219 user,
1220 exercise,
1221 Some(course),
1222 None,
1223 )
1224 .await
1225 .unwrap();
1226 user_exercise_states::update(
1227 tx.as_mut(),
1228 UserExerciseStateUpdate {
1229 id: state.id,
1230 score_given: Some(10.0),
1231 activity_progress: ActivityProgress::Completed,
1232 reviewing_stage: ReviewingStage::NotStarted,
1233 grading_progress: GradingProgress::FullyGraded,
1234 },
1235 )
1236 .await
1237 .unwrap();
1238
1239 let completion = course_module_completions::insert(
1240 tx.as_mut(),
1241 PKeyPolicy::Generate,
1242 &NewCourseModuleCompletion {
1243 course_id: course,
1244 course_module_id: course_module.id,
1245 user_id: user,
1246 completion_date: Utc::now() + Duration::days(1),
1247 completion_registration_attempt_date: None,
1248 completion_language: "en-US".to_string(),
1249 eligible_for_ects: false,
1250 email: "email".to_string(),
1251 grade: None,
1252 passed: true,
1253 },
1254 CourseModuleCompletionGranter::Automatic,
1255 )
1256 .await
1257 .unwrap();
1258 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1259 tx.as_mut(),
1260 course_module.id,
1261 259200,
1262 )
1263 .await
1264 .unwrap();
1265 check_and_insert_suspected_cheaters(
1266 tx.as_mut(),
1267 user,
1268 course,
1269 thresholds.duration_seconds,
1270 completion,
1271 )
1272 .await
1273 .unwrap();
1274
1275 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1276 tx.as_mut(),
1277 course,
1278 SuspectedCheaterStatus::Flagged,
1279 )
1280 .await
1281 .unwrap();
1282 assert_eq!(cheaters.len(), 1);
1283 assert_eq!(cheaters[0].user_id, user);
1284 }
1285
1286 #[tokio::test]
1287 async fn tagging_suspected_cheater_is_idempotent() {
1288 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1289
1290 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1291 .await
1292 .unwrap();
1293 let state = user_exercise_states::get_or_create_user_exercise_state(
1294 tx.as_mut(),
1295 user,
1296 exercise,
1297 Some(course),
1298 None,
1299 )
1300 .await
1301 .unwrap();
1302 user_exercise_states::update(
1303 tx.as_mut(),
1304 UserExerciseStateUpdate {
1305 id: state.id,
1306 score_given: Some(10.0),
1307 activity_progress: ActivityProgress::Completed,
1308 reviewing_stage: ReviewingStage::NotStarted,
1309 grading_progress: GradingProgress::FullyGraded,
1310 },
1311 )
1312 .await
1313 .unwrap();
1314
1315 let completion = course_module_completions::insert(
1316 tx.as_mut(),
1317 PKeyPolicy::Generate,
1318 &NewCourseModuleCompletion {
1319 course_id: course,
1320 course_module_id: course_module.id,
1321 user_id: user,
1322 completion_date: Utc::now() + Duration::days(1),
1323 completion_registration_attempt_date: None,
1324 completion_language: "en-US".to_string(),
1325 eligible_for_ects: false,
1326 email: "email".to_string(),
1327 grade: None,
1328 passed: true,
1329 },
1330 CourseModuleCompletionGranter::Automatic,
1331 )
1332 .await
1333 .unwrap();
1334 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1335 tx.as_mut(),
1336 course_module.id,
1337 259200,
1338 )
1339 .await
1340 .unwrap();
1341 check_and_insert_suspected_cheaters(
1342 tx.as_mut(),
1343 user,
1344 course,
1345 thresholds.duration_seconds,
1346 completion.clone(),
1347 )
1348 .await
1349 .unwrap();
1350 check_and_insert_suspected_cheaters(
1351 tx.as_mut(),
1352 user,
1353 course,
1354 thresholds.duration_seconds,
1355 completion,
1356 )
1357 .await
1358 .unwrap();
1359
1360 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1361 tx.as_mut(),
1362 course,
1363 SuspectedCheaterStatus::Flagged,
1364 )
1365 .await
1366 .unwrap();
1367 assert_eq!(cheaters.len(), 1);
1368 let completion_needing_review =
1369 course_module_completions::get_latest_by_course_and_user_ids(
1370 tx.as_mut(),
1371 course_module.id,
1372 user,
1373 )
1374 .await
1375 .unwrap();
1376 assert!(completion_needing_review.needs_to_be_reviewed);
1377
1378 suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1379 .await
1380 .unwrap();
1381 let archived_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!(!archived_completion.needs_to_be_reviewed);
1389 check_and_insert_suspected_cheaters(
1390 tx.as_mut(),
1391 user,
1392 course,
1393 thresholds.duration_seconds,
1394 archived_completion,
1395 )
1396 .await
1397 .unwrap();
1398
1399 let visible_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1400 tx.as_mut(),
1401 course,
1402 SuspectedCheaterStatus::Flagged,
1403 )
1404 .await
1405 .unwrap();
1406 let archived_cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1407 tx.as_mut(),
1408 course,
1409 SuspectedCheaterStatus::Dismissed,
1410 )
1411 .await
1412 .unwrap();
1413 assert!(visible_cheaters.is_empty());
1414 assert_eq!(archived_cheaters.len(), 1);
1415 let rechecked_completion = course_module_completions::get_latest_by_course_and_user_ids(
1416 tx.as_mut(),
1417 course_module.id,
1418 user,
1419 )
1420 .await
1421 .unwrap();
1422 assert!(!rechecked_completion.needs_to_be_reviewed);
1423 }
1424
1425 #[tokio::test]
1426 async fn confirming_then_dismissing_restores_grade() {
1427 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1428
1429 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1430 .await
1431 .unwrap();
1432 let state = user_exercise_states::get_or_create_user_exercise_state(
1433 tx.as_mut(),
1434 user,
1435 exercise,
1436 Some(course),
1437 None,
1438 )
1439 .await
1440 .unwrap();
1441 user_exercise_states::update(
1442 tx.as_mut(),
1443 UserExerciseStateUpdate {
1444 id: state.id,
1445 score_given: Some(10.0),
1446 activity_progress: ActivityProgress::Completed,
1447 reviewing_stage: ReviewingStage::NotStarted,
1448 grading_progress: GradingProgress::FullyGraded,
1449 },
1450 )
1451 .await
1452 .unwrap();
1453
1454 let completion = course_module_completions::insert(
1456 tx.as_mut(),
1457 PKeyPolicy::Generate,
1458 &NewCourseModuleCompletion {
1459 course_id: course,
1460 course_module_id: course_module.id,
1461 user_id: user,
1462 completion_date: Utc::now() + Duration::days(1),
1463 completion_registration_attempt_date: None,
1464 completion_language: "en-US".to_string(),
1465 eligible_for_ects: false,
1466 email: "email".to_string(),
1467 grade: Some(4),
1468 passed: true,
1469 },
1470 CourseModuleCompletionGranter::Automatic,
1471 )
1472 .await
1473 .unwrap();
1474 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1475 tx.as_mut(),
1476 course_module.id,
1477 259200,
1478 )
1479 .await
1480 .unwrap();
1481 check_and_insert_suspected_cheaters(
1482 tx.as_mut(),
1483 user,
1484 course,
1485 thresholds.duration_seconds,
1486 completion,
1487 )
1488 .await
1489 .unwrap();
1490
1491 suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
1493 .await
1494 .unwrap();
1495 let failed = course_module_completions::get_latest_by_course_and_user_ids(
1496 tx.as_mut(),
1497 course_module.id,
1498 user,
1499 )
1500 .await
1501 .unwrap();
1502 assert!(!failed.passed);
1503 assert_eq!(failed.grade, Some(0));
1504 let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1505 tx.as_mut(),
1506 course,
1507 SuspectedCheaterStatus::ConfirmedCheating,
1508 )
1509 .await
1510 .unwrap();
1511 assert_eq!(confirmed.len(), 1);
1512
1513 suspected_cheaters::dismiss_by_user_id_and_course_id(tx.as_mut(), user, course)
1515 .await
1516 .unwrap();
1517 let restored = course_module_completions::get_latest_by_course_and_user_ids(
1518 tx.as_mut(),
1519 course_module.id,
1520 user,
1521 )
1522 .await
1523 .unwrap();
1524 assert!(restored.passed);
1525 assert_eq!(restored.grade, Some(4));
1526 assert!(!restored.needs_to_be_reviewed);
1527 let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1528 tx.as_mut(),
1529 course,
1530 SuspectedCheaterStatus::Dismissed,
1531 )
1532 .await
1533 .unwrap();
1534 assert_eq!(dismissed.len(), 1);
1535 }
1536
1537 #[tokio::test]
1538 async fn manual_completion_prevents_flagging() {
1539 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module);
1540
1541 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1542 .await
1543 .unwrap();
1544
1545 let teacher = users::insert(
1547 tx.as_mut(),
1548 PKeyPolicy::Generate,
1549 "teacher-vouching@example.com",
1550 Some("Teacher"),
1551 Some("McVouch"),
1552 )
1553 .await
1554 .unwrap();
1555 course_module_completions::insert(
1556 tx.as_mut(),
1557 PKeyPolicy::Generate,
1558 &NewCourseModuleCompletion {
1559 course_id: course,
1560 course_module_id: course_module.id,
1561 user_id: user,
1562 completion_date: Utc::now(),
1563 completion_registration_attempt_date: None,
1564 completion_language: "en-US".to_string(),
1565 eligible_for_ects: false,
1566 email: "email".to_string(),
1567 grade: Some(5),
1568 passed: true,
1569 },
1570 CourseModuleCompletionGranter::User(teacher),
1571 )
1572 .await
1573 .unwrap();
1574
1575 let automatic_completion = course_module_completions::insert(
1577 tx.as_mut(),
1578 PKeyPolicy::Generate,
1579 &NewCourseModuleCompletion {
1580 course_id: course,
1581 course_module_id: course_module.id,
1582 user_id: user,
1583 completion_date: Utc::now() + Duration::days(1),
1584 completion_registration_attempt_date: None,
1585 completion_language: "en-US".to_string(),
1586 eligible_for_ects: false,
1587 email: "email".to_string(),
1588 grade: None,
1589 passed: true,
1590 },
1591 CourseModuleCompletionGranter::Automatic,
1592 )
1593 .await
1594 .unwrap();
1595 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1596 tx.as_mut(),
1597 course_module.id,
1598 259200,
1599 )
1600 .await
1601 .unwrap();
1602 check_and_insert_suspected_cheaters(
1603 tx.as_mut(),
1604 user,
1605 course,
1606 thresholds.duration_seconds,
1607 automatic_completion,
1608 )
1609 .await
1610 .unwrap();
1611
1612 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1614 tx.as_mut(),
1615 course,
1616 SuspectedCheaterStatus::Flagged,
1617 )
1618 .await
1619 .unwrap();
1620 assert!(cheaters.is_empty());
1621 }
1622
1623 #[tokio::test]
1624 async fn adding_manual_completion_dismisses_confirmed_suspicion_and_restores_grade() {
1625 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1626
1627 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1628 .await
1629 .unwrap();
1630 let state = user_exercise_states::get_or_create_user_exercise_state(
1631 tx.as_mut(),
1632 user,
1633 exercise,
1634 Some(course),
1635 None,
1636 )
1637 .await
1638 .unwrap();
1639 user_exercise_states::update(
1640 tx.as_mut(),
1641 UserExerciseStateUpdate {
1642 id: state.id,
1643 score_given: Some(10.0),
1644 activity_progress: ActivityProgress::Completed,
1645 reviewing_stage: ReviewingStage::NotStarted,
1646 grading_progress: GradingProgress::FullyGraded,
1647 },
1648 )
1649 .await
1650 .unwrap();
1651
1652 let completion = course_module_completions::insert(
1654 tx.as_mut(),
1655 PKeyPolicy::Generate,
1656 &NewCourseModuleCompletion {
1657 course_id: course,
1658 course_module_id: course_module.id,
1659 user_id: user,
1660 completion_date: Utc::now() + Duration::days(1),
1661 completion_registration_attempt_date: None,
1662 completion_language: "en-US".to_string(),
1663 eligible_for_ects: false,
1664 email: "email".to_string(),
1665 grade: Some(4),
1666 passed: true,
1667 },
1668 CourseModuleCompletionGranter::Automatic,
1669 )
1670 .await
1671 .unwrap();
1672 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1673 tx.as_mut(),
1674 course_module.id,
1675 259200,
1676 )
1677 .await
1678 .unwrap();
1679 check_and_insert_suspected_cheaters(
1680 tx.as_mut(),
1681 user,
1682 course,
1683 thresholds.duration_seconds,
1684 completion,
1685 )
1686 .await
1687 .unwrap();
1688 suspected_cheaters::confirm_cheater_by_user_id_and_course_id(tx.as_mut(), user, course)
1689 .await
1690 .unwrap();
1691
1692 let teacher = users::insert(
1695 tx.as_mut(),
1696 PKeyPolicy::Generate,
1697 "teacher-vouching@example.com",
1698 Some("Teacher"),
1699 Some("McVouch"),
1700 )
1701 .await
1702 .unwrap();
1703 add_manual_completions(
1704 tx.as_mut(),
1705 teacher,
1706 &instance,
1707 &TeacherManualCompletionRequest {
1708 course_module_id: course_module.id,
1709 new_completions: vec![TeacherManualCompletion {
1710 user_id: user,
1711 grade: Some(5),
1712 passed: true,
1713 completion_date: None,
1714 }],
1715 skip_duplicate_completions: false,
1716 },
1717 )
1718 .await
1719 .unwrap();
1720
1721 let flagged = suspected_cheaters::get_all_suspected_cheaters_in_course(
1723 tx.as_mut(),
1724 course,
1725 SuspectedCheaterStatus::Flagged,
1726 )
1727 .await
1728 .unwrap();
1729 let confirmed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1730 tx.as_mut(),
1731 course,
1732 SuspectedCheaterStatus::ConfirmedCheating,
1733 )
1734 .await
1735 .unwrap();
1736 let dismissed = suspected_cheaters::get_all_suspected_cheaters_in_course(
1737 tx.as_mut(),
1738 course,
1739 SuspectedCheaterStatus::Dismissed,
1740 )
1741 .await
1742 .unwrap();
1743 assert!(flagged.is_empty());
1744 assert!(confirmed.is_empty());
1745 assert_eq!(dismissed.len(), 1);
1746
1747 let restored =
1749 course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
1750 tx.as_mut(),
1751 course_module.id,
1752 course,
1753 user,
1754 )
1755 .await
1756 .unwrap();
1757 assert!(restored.passed);
1758 assert_eq!(restored.grade, Some(4));
1759 assert!(!restored.needs_to_be_reviewed);
1760 }
1761
1762 #[tokio::test]
1763 async fn doesnt_tag_suspected_cheater() {
1764 insert_data!(:tx, user:user, :org, :course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1765
1766 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1767 .await
1768 .unwrap();
1769 let state = user_exercise_states::get_or_create_user_exercise_state(
1770 tx.as_mut(),
1771 user,
1772 exercise,
1773 Some(course),
1774 None,
1775 )
1776 .await
1777 .unwrap();
1778 user_exercise_states::update(
1779 tx.as_mut(),
1780 UserExerciseStateUpdate {
1781 id: state.id,
1782 score_given: Some(9.0),
1783 activity_progress: ActivityProgress::Completed,
1784 reviewing_stage: ReviewingStage::NotStarted,
1785 grading_progress: GradingProgress::FullyGraded,
1786 },
1787 )
1788 .await
1789 .unwrap();
1790
1791 course_module_completions::insert(
1792 tx.as_mut(),
1793 PKeyPolicy::Generate,
1794 &NewCourseModuleCompletion {
1795 course_id: course,
1796 course_module_id: course_module.id,
1797 user_id: user,
1798 completion_date: Utc::now() + Duration::days(3),
1799 completion_registration_attempt_date: None,
1800 completion_language: "en-US".to_string(),
1801 eligible_for_ects: false,
1802 email: "email".to_string(),
1803 grade: Some(9),
1804 passed: true,
1805 },
1806 CourseModuleCompletionGranter::Automatic,
1807 )
1808 .await
1809 .unwrap();
1810 suspected_cheaters::insert_thresholds_by_module_id(tx.as_mut(), course_module.id, 172800)
1811 .await
1812 .unwrap();
1813 update_automatic_completion_status_and_grant_if_eligible(tx.as_mut(), &course_module, user)
1814 .await
1815 .unwrap();
1816
1817 let cheaters = suspected_cheaters::get_all_suspected_cheaters_in_course(
1818 tx.as_mut(),
1819 course,
1820 SuspectedCheaterStatus::Flagged,
1821 )
1822 .await
1823 .unwrap();
1824 assert!(cheaters.is_empty());
1825 }
1826
1827 }