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