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