1use chrono::{DateTime, Utc};
2use itertools::Itertools;
3use std::collections::HashMap;
4
5use crate::{
6 course_exams,
7 course_instance_enrollments::{self, NewCourseInstanceEnrollment},
8 course_instances::{self, CourseInstance},
9 course_module_completions::{
10 self, CourseModuleCompletion, CourseModuleCompletionGranter,
11 CourseModuleCompletionWithRegistrationInfo, NewCourseModuleCompletion,
12 },
13 course_modules::{self, AutomaticCompletionRequirements, CompletionPolicy, CourseModule},
14 courses, exams, open_university_registration_links,
15 prelude::*,
16 suspected_cheaters::{self, Threshold},
17 user_course_settings,
18 user_details::UserDetail,
19 user_exercise_states,
20 users::{self, User},
21};
22
23pub async fn update_automatic_completion_status_and_grant_if_eligible(
27 conn: &mut PgConnection,
28 course_module: &CourseModule,
29 user_id: Uuid,
30) -> ModelResult<()> {
31 let mut tx = conn.begin().await?;
32 let completion =
33 create_automatic_course_module_completion_if_eligible(&mut tx, course_module, user_id)
34 .await?;
35 if let Some(completion) = completion {
36 let course = courses::get_course(&mut tx, course_module.course_id).await?;
37 let submodule_completions_required = course
38 .base_module_completion_requires_n_submodule_completions
39 .try_into()?;
40 update_module_completion_prerequisite_statuses_for_user(
41 &mut tx,
42 user_id,
43 course_module.course_id,
44 submodule_completions_required,
45 )
46 .await?;
47
48 if let Some(thresholds) =
49 suspected_cheaters::get_thresholds_by_module_id(&mut tx, completion.course_module_id)
50 .await?
51 {
52 check_and_insert_suspected_cheaters(
53 &mut tx,
54 user_id,
55 course.id,
56 &thresholds,
57 completion,
58 )
59 .await?;
60 }
61 }
62 tx.commit().await?;
63 Ok(())
64}
65
66pub async fn check_and_insert_suspected_cheaters(
67 conn: &mut PgConnection,
68 user_id: Uuid,
69 course_id: Uuid,
70 thresholds: &Threshold,
71 completion: CourseModuleCompletion,
72) -> ModelResult<()> {
73 let total_points = user_exercise_states::get_user_total_course_points(conn, user_id, course_id)
74 .await?
75 .unwrap_or(0.0);
76
77 let completed_module = course_modules::get_by_id(conn, completion.course_module_id).await?;
78 let is_default_module = completed_module.is_default_module();
79
80 let student_duration_seconds = if is_default_module {
81 course_instances::get_student_duration(conn, completion.user_id, course_id)
82 .await?
83 .unwrap_or(0)
84 } else {
85 let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
86 let default_completion = course_module_completions::get_all_by_course_module_and_user_ids(
87 conn,
88 default_module.id,
89 completion.user_id,
90 )
91 .await?
92 .into_iter()
93 .max_by_key(|c| c.completion_date);
94
95 if let Some(default_completion) = default_completion {
96 let duration =
97 (completion.completion_date - default_completion.completion_date).num_seconds();
98 duration.max(0)
99 } else {
100 course_instances::get_student_duration(conn, completion.user_id, course_id)
102 .await?
103 .unwrap_or(0)
104 }
105 };
106
107 if (student_duration_seconds as i32) < thresholds.duration_seconds {
108 suspected_cheaters::insert(
109 conn,
110 completion.user_id,
111 course_id,
112 Some(student_duration_seconds as i32),
113 total_points as i32,
114 )
115 .await?;
116
117 course_module_completions::update_needs_to_be_reviewed(conn, completion.id, true).await?;
118 }
119
120 Ok(())
121}
122
123#[instrument(skip(conn))]
126async fn create_automatic_course_module_completion_if_eligible(
127 conn: &mut PgConnection,
128 course_module: &CourseModule,
129 user_id: Uuid,
130) -> ModelResult<Option<CourseModuleCompletion>> {
131 let existing_completion =
132 course_module_completions::get_automatic_completion_by_course_module_course_and_user_ids(
133 conn,
134 course_module.id,
135 course_module.course_id,
136 user_id,
137 )
138 .await
139 .optional()?;
140 if let Some(existing_completion) = existing_completion {
141 Ok(Some(existing_completion))
143 } else {
144 let eligible =
145 user_is_eligible_for_automatic_completion(conn, course_module, user_id).await?;
146 if eligible {
147 let course = courses::get_course(conn, course_module.course_id).await?;
148 let user = users::get_by_id(conn, user_id).await?;
149 if user.deleted_at.is_some() {
150 warn!("Cannot create a completion for a deleted user");
151 return Ok(None);
152 }
153 let user_details =
154 crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
155 let completion = course_module_completions::insert(
156 conn,
157 PKeyPolicy::Generate,
158 &NewCourseModuleCompletion {
159 course_id: course_module.course_id,
160 course_module_id: course_module.id,
161 user_id,
162 completion_date: Utc::now(),
163 completion_registration_attempt_date: None,
164 completion_language: course.language_code,
165 eligible_for_ects: true,
166 email: user_details.email,
167 grade: None,
168 passed: true,
169 },
170 CourseModuleCompletionGranter::Automatic,
171 )
172 .await?;
173 info!("Created a completion");
174 Ok(Some(completion))
175 } else {
176 Ok(None)
178 }
179 }
180}
181
182#[instrument(skip(conn))]
183async fn user_is_eligible_for_automatic_completion(
184 conn: &mut PgConnection,
185 course_module: &CourseModule,
186 user_id: Uuid,
187) -> ModelResult<bool> {
188 match &course_module.completion_policy {
189 CompletionPolicy::Automatic(requirements) => {
190 let eligible = user_passes_automatic_completion_exercise_tresholds(
191 conn,
192 user_id,
193 requirements,
194 course_module.course_id,
195 )
196 .await?;
197 if eligible {
198 if requirements.requires_exam {
199 info!("To complete this module automatically, the user must pass an exam.");
200 user_has_passed_exam_for_the_course_based_on_points(
201 conn,
202 user_id,
203 course_module.course_id,
204 )
205 .await
206 } else {
207 Ok(true)
208 }
209 } else {
210 Ok(false)
211 }
212 }
213 CompletionPolicy::Manual => Ok(false),
214 }
215}
216
217#[instrument(skip(conn))]
225pub async fn user_can_take_exam(
226 conn: &mut PgConnection,
227 exam_id: Uuid,
228 user_id: Uuid,
229) -> ModelResult<bool> {
230 let course_ids = course_exams::get_course_ids_by_exam_id(conn, exam_id).await?;
231 let settings = user_course_settings::get_all_by_user_and_multiple_current_courses(
232 conn,
233 &course_ids,
234 user_id,
235 )
236 .await?;
237 let mut can_take_exam = true;
239 for course_id in course_ids {
240 let default_module = course_modules::get_default_by_course_id(conn, course_id).await?;
241 if let CompletionPolicy::Automatic(requirements) = &default_module.completion_policy {
242 if let Some(s) = settings.iter().find(|x| x.current_course_id == course_id) {
243 let eligible = user_passes_automatic_completion_exercise_tresholds(
244 conn,
245 s.user_id,
246 requirements,
247 s.current_course_id,
248 )
249 .await?;
250 if eligible {
251 can_take_exam = true;
253 break;
254 }
255 }
256 can_take_exam = false;
259 }
260 }
261 Ok(can_take_exam)
262}
263
264async fn user_has_passed_exam_for_the_course_based_on_points(
267 conn: &mut PgConnection,
268 user_id: Uuid,
269 course_id: Uuid,
270) -> ModelResult<bool> {
271 let now = Utc::now();
272 let exam_ids = course_exams::get_exam_ids_by_course_id(conn, course_id).await?;
273 for exam_id in exam_ids {
274 let exam = exams::get(conn, exam_id).await?;
275 if exam.minimum_points_treshold == 0 || exam.grade_manually {
277 continue;
278 }
279 if exam.ended_at_or(now, false) {
280 let points =
281 user_exercise_states::get_user_total_exam_points(conn, user_id, exam_id).await?;
282 if let Some(points) = points {
283 if points >= exam.minimum_points_treshold as f32 {
284 return Ok(true);
285 }
286 }
287 }
288 }
289 Ok(false)
290}
291
292async fn user_passes_automatic_completion_exercise_tresholds(
293 conn: &mut PgConnection,
294 user_id: Uuid,
295 requirements: &AutomaticCompletionRequirements,
296 course_id: Uuid,
297) -> ModelResult<bool> {
298 let user_metrics = user_exercise_states::get_single_module_metrics(
299 conn,
300 course_id,
301 requirements.course_module_id,
302 user_id,
303 )
304 .await?;
305 let attempted_exercises: i32 = user_metrics.attempted_exercises.unwrap_or(0) as i32;
306 let exercise_points = user_metrics.score_given.unwrap_or(0.0) as i32;
307 let eligible = requirements.passes_exercise_tresholds(attempted_exercises, exercise_points);
308 Ok(eligible)
309}
310
311#[instrument(skip(conn))]
314async fn update_module_completion_prerequisite_statuses_for_user(
315 conn: &mut PgConnection,
316 user_id: Uuid,
317 course_id: Uuid,
318 base_module_completion_requires_n_submodule_completions: u32,
319) -> ModelResult<()> {
320 let default_course_module = course_modules::get_default_by_course_id(conn, course_id).await?;
321 let course_module_completions =
322 course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
323 .await?;
324 let default_module_is_completed = course_module_completions
325 .iter()
326 .any(|x| x.course_module_id == default_course_module.id);
327 let submodule_completions = course_module_completions
328 .iter()
329 .filter(|x| x.course_module_id != default_course_module.id)
330 .unique_by(|x| x.course_module_id)
331 .count();
332 let enough_submodule_completions = submodule_completions
333 >= base_module_completion_requires_n_submodule_completions.try_into()?;
334 let completions_needing_processing: Vec<_> = course_module_completions
335 .into_iter()
336 .filter(|x| !x.prerequisite_modules_completed)
337 .collect();
338 for completion in completions_needing_processing {
339 if completion.course_module_id == default_course_module.id {
340 if enough_submodule_completions {
341 course_module_completions::update_prerequisite_modules_completed(
342 conn,
343 completion.id,
344 true,
345 )
346 .await?;
347 }
348 } else if default_module_is_completed {
349 course_module_completions::update_prerequisite_modules_completed(
350 conn,
351 completion.id,
352 true,
353 )
354 .await?;
355 }
356 }
357 Ok(())
358}
359
360#[instrument(skip(conn))]
362pub async fn process_all_course_completions(
363 conn: &mut PgConnection,
364 course_id: Uuid,
365) -> ModelResult<()> {
366 info!("Reprocessing course completions");
367 let course = courses::get_course(conn, course_id).await?;
368 let submodule_completions_required = course
369 .base_module_completion_requires_n_submodule_completions
370 .try_into()?;
371 let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
372 let users =
374 crate::users::get_all_user_ids_with_user_exercise_states_on_course(conn, course_id).await?;
375 info!(users = ?users.len(), course_modules = ?course_modules.len(), ?submodule_completions_required, "Completion reprocessing info");
376 for course_module in course_modules.iter() {
377 info!(?course_module, "Course module information");
378 }
379 let mut tx = conn.begin().await?;
380 for user_id in users {
381 let mut num_completions = 0;
382 for course_module in course_modules.iter() {
383 let completion = create_automatic_course_module_completion_if_eligible(
384 &mut tx,
385 course_module,
386 user_id,
387 )
388 .await?;
389 if completion.is_some() {
390 num_completions += 1;
391 }
392 }
393 if num_completions > 0 {
394 update_module_completion_prerequisite_statuses_for_user(
395 &mut tx,
396 user_id,
397 course_id,
398 submodule_completions_required,
399 )
400 .await?;
401 }
402 }
403 tx.commit().await?;
404 info!("Reprocessing course module completions complete");
405 Ok(())
406}
407
408#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
409#[cfg_attr(feature = "ts_rs", derive(TS))]
410pub struct CourseInstanceCompletionSummary {
411 pub course_modules: Vec<CourseModule>,
412 pub users_with_course_module_completions: Vec<UserWithModuleCompletions>,
413}
414
415#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
416#[cfg_attr(feature = "ts_rs", derive(TS))]
417pub struct UserWithModuleCompletions {
418 pub completed_modules: Vec<CourseModuleCompletionWithRegistrationInfo>,
419 pub email: String,
420 pub first_name: Option<String>,
421 pub last_name: Option<String>,
422 pub user_id: Uuid,
423}
424
425#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
426#[cfg_attr(feature = "ts_rs", derive(TS))]
427pub struct UserCourseModuleCompletion {
428 pub course_module_id: Uuid,
429 pub grade: Option<i32>,
430 pub passed: bool,
431}
432
433impl From<CourseModuleCompletion> for UserCourseModuleCompletion {
434 fn from(course_module_completion: CourseModuleCompletion) -> Self {
435 Self {
436 course_module_id: course_module_completion.course_module_id,
437 grade: course_module_completion.grade,
438 passed: course_module_completion.passed,
439 }
440 }
441}
442
443impl UserWithModuleCompletions {
444 fn from_user_and_details(user: User, user_details: UserDetail) -> Self {
445 Self {
446 user_id: user.id,
447 first_name: user_details.first_name,
448 last_name: user_details.last_name,
449 email: user_details.email,
450 completed_modules: vec![],
451 }
452 }
453}
454
455pub async fn get_course_instance_completion_summary(
456 conn: &mut PgConnection,
457 course_instance: &CourseInstance,
458) -> ModelResult<CourseInstanceCompletionSummary> {
459 let course_modules = course_modules::get_by_course_id(conn, course_instance.course_id).await?;
460 let users_with_course_module_completions_list =
461 users::get_users_by_course_instance_enrollment(conn, course_instance.id).await?;
462 let user_id_to_details_map = crate::user_details::get_users_details_by_user_id_map(
463 conn,
464 &users_with_course_module_completions_list,
465 )
466 .await?;
467 let mut users_with_course_module_completions: HashMap<Uuid, UserWithModuleCompletions> =
468 users_with_course_module_completions_list
469 .into_iter()
470 .filter_map(|o| {
471 let details = user_id_to_details_map.get(&o.id);
472 details.map(|details| (o, details))
473 })
474 .map(|u| {
475 (
476 u.0.id,
477 UserWithModuleCompletions::from_user_and_details(u.0, u.1.clone()),
478 )
479 })
480 .collect();
481 let completions =
482 course_module_completions::get_all_with_registration_information_by_course_instance_id(
483 conn,
484 course_instance.id,
485 course_instance.course_id,
486 )
487 .await?;
488 completions.into_iter().for_each(|x| {
489 let user_with_completions = users_with_course_module_completions.get_mut(&x.user_id);
490 if let Some(completion) = user_with_completions {
491 completion.completed_modules.push(x);
492 }
493 });
494 Ok(CourseInstanceCompletionSummary {
495 course_modules,
496 users_with_course_module_completions: users_with_course_module_completions
497 .into_values()
498 .collect(),
499 })
500}
501
502#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
503#[cfg_attr(feature = "ts_rs", derive(TS))]
504pub struct TeacherManualCompletionRequest {
505 pub course_module_id: Uuid,
506 pub new_completions: Vec<TeacherManualCompletion>,
507 pub skip_duplicate_completions: bool,
508}
509
510#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
511#[cfg_attr(feature = "ts_rs", derive(TS))]
512pub struct TeacherManualCompletion {
513 pub user_id: Uuid,
514 pub grade: Option<i32>,
515 pub passed: bool,
516 pub completion_date: Option<DateTime<Utc>>,
517}
518
519pub async fn add_manual_completions(
520 conn: &mut PgConnection,
521 completion_giver_user_id: Uuid,
522 course_instance: &CourseInstance,
523 manual_completion_request: &TeacherManualCompletionRequest,
524) -> ModelResult<()> {
525 let course_module =
526 course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
527 if course_module.course_id != course_instance.course_id {
528 return Err(ModelError::new(
529 ModelErrorType::PreconditionFailed,
530 "Course module not part of the course.".to_string(),
531 None,
532 ));
533 }
534 let course = courses::get_course(conn, course_instance.course_id).await?;
535 let mut tx = conn.begin().await?;
536 for completion in manual_completion_request.new_completions.iter() {
537 let completion_receiver = users::get_by_id(&mut tx, completion.user_id).await?;
538 let completion_receiver_user_details =
539 crate::user_details::get_user_details_by_user_id(&mut tx, completion_receiver.id)
540 .await?;
541 let module_completed = course_module_completions::user_has_completed_course_module(
542 &mut tx,
543 completion.user_id,
544 manual_completion_request.course_module_id,
545 )
546 .await?;
547 if !module_completed || !manual_completion_request.skip_duplicate_completions {
548 course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
549 &mut tx,
550 NewCourseInstanceEnrollment {
551 user_id: completion_receiver.id,
552 course_id: course.id,
553 course_instance_id: course_instance.id,
554 },
555 )
556 .await?;
557
558 if completion.grade.is_some()
559 && (completion.grade > Some(5) || completion.grade < Some(0))
560 {
561 return Err(ModelError::new(
562 ModelErrorType::PreconditionFailed,
563 "Invalid grade".to_string(),
564 None,
565 ));
566 }
567 course_module_completions::insert(
568 &mut tx,
569 PKeyPolicy::Generate,
570 &NewCourseModuleCompletion {
571 course_id: course_instance.course_id,
572 course_module_id: manual_completion_request.course_module_id,
573 user_id: completion.user_id,
574 completion_date: completion.completion_date.unwrap_or_else(Utc::now),
575 completion_registration_attempt_date: None,
576 completion_language: course.language_code.clone(),
577 eligible_for_ects: true,
578 email: completion_receiver_user_details.email,
579 grade: completion.grade,
580 passed: if completion.grade == Some(0) {
581 false
582 } else {
583 completion.passed
584 },
585 },
586 CourseModuleCompletionGranter::User(completion_giver_user_id),
587 )
588 .await?;
589
590 crate::course_instance_enrollments::insert_enrollment_and_set_as_current(
592 &mut tx,
593 NewCourseInstanceEnrollment {
594 user_id: completion_receiver.id,
595 course_id: course.id,
596 course_instance_id: course_instance.id,
597 },
598 )
599 .await?;
600
601 update_module_completion_prerequisite_statuses_for_user(
602 &mut tx,
603 completion_receiver.id,
604 course.id,
605 course
606 .base_module_completion_requires_n_submodule_completions
607 .try_into()?,
608 )
609 .await?;
610 }
611 }
612 tx.commit().await?;
613 Ok(())
614}
615
616#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
617#[cfg_attr(feature = "ts_rs", derive(TS))]
618pub struct ManualCompletionPreview {
619 pub already_completed_users: Vec<ManualCompletionPreviewUser>,
620 pub first_time_completing_users: Vec<ManualCompletionPreviewUser>,
621 pub non_enrolled_users: Vec<ManualCompletionPreviewUser>,
622}
623
624#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
625#[cfg_attr(feature = "ts_rs", derive(TS))]
626pub struct ManualCompletionPreviewUser {
627 pub user_id: Uuid,
628 pub first_name: Option<String>,
629 pub last_name: Option<String>,
630 pub grade: Option<i32>,
631 pub passed: bool,
632 pub previous_best_grade: Option<i32>,
633}
634
635pub async fn get_manual_completion_result_preview(
637 conn: &mut PgConnection,
638 course_instance: &CourseInstance,
639 manual_completion_request: &TeacherManualCompletionRequest,
640) -> ModelResult<ManualCompletionPreview> {
641 let course_module =
642 course_modules::get_by_id(conn, manual_completion_request.course_module_id).await?;
643 if course_module.course_id != course_instance.course_id {
644 return Err(ModelError::new(
645 ModelErrorType::PreconditionFailed,
646 "Course module not part of the course.".to_string(),
647 None,
648 ));
649 }
650 let mut already_completed_users = vec![];
651 let mut first_time_completing_users = vec![];
652 let mut non_enrolled_users = vec![];
653 for completion in manual_completion_request.new_completions.iter() {
654 let user = users::get_by_id(conn, completion.user_id).await?;
655 let user_details = crate::user_details::get_user_details_by_user_id(conn, user.id).await?;
656 let user = ManualCompletionPreviewUser {
657 user_id: user.id,
658 first_name: user_details.first_name,
659 last_name: user_details.last_name,
660 grade: completion.grade,
661 passed: completion.passed,
662 previous_best_grade: None,
663 };
664 let enrollment = course_instance_enrollments::get_by_user_and_course_instance_id(
665 conn,
666 completion.user_id,
667 course_instance.id,
668 )
669 .await
670 .optional()?;
671 if enrollment.is_none() {
672 non_enrolled_users.push(user.clone());
673 }
674 let module_completed = course_module_completions::user_has_completed_course_module(
675 conn,
676 completion.user_id,
677 manual_completion_request.course_module_id,
678 )
679 .await?;
680 if module_completed {
681 already_completed_users.push(user);
682 } else {
683 first_time_completing_users.push(user);
684 }
685 }
686 Ok(ManualCompletionPreview {
687 already_completed_users,
688 first_time_completing_users,
689 non_enrolled_users,
690 })
691}
692
693#[derive(Clone, PartialEq, Deserialize, Serialize)]
694#[cfg_attr(feature = "ts_rs", derive(TS))]
695pub struct UserCompletionInformation {
696 pub course_module_completion_id: Uuid,
697 pub course_name: String,
698 pub uh_course_code: String,
699 pub email: String,
700 pub ects_credits: Option<f32>,
701 pub enable_registering_completion_to_uh_open_university: bool,
702}
703
704pub async fn get_user_completion_information(
705 conn: &mut PgConnection,
706 user_id: Uuid,
707 course_module: &CourseModule,
708) -> ModelResult<UserCompletionInformation> {
709 let user = users::get_by_id(conn, user_id).await?;
710 let course = courses::get_course(conn, course_module.course_id).await?;
711 let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
712 conn,
713 course_module.id,
714 user.id,
715 )
716 .await?;
717 let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
719 ModelError::new(
720 ModelErrorType::InvalidRequest,
721 "Course module is missing uh_course_code.".to_string(),
722 None,
723 )
724 })?;
725 Ok(UserCompletionInformation {
726 course_module_completion_id: course_module_completion.id,
727 course_name: course_module
728 .name
729 .clone()
730 .unwrap_or_else(|| course.name.clone()),
731 uh_course_code,
732 ects_credits: course_module.ects_credits,
733 email: course_module_completion.email,
734 enable_registering_completion_to_uh_open_university: course_module
735 .enable_registering_completion_to_uh_open_university,
736 })
737}
738
739#[derive(Clone, PartialEq, Deserialize, Serialize)]
740#[cfg_attr(feature = "ts_rs", derive(TS))]
741pub struct UserModuleCompletionStatus {
742 pub completed: bool,
743 pub default: bool,
744 pub module_id: Uuid,
745 pub name: String,
746 pub order_number: i32,
747 pub prerequisite_modules_completed: bool,
748 pub grade: Option<i32>,
749 pub passed: Option<bool>,
750 pub enable_registering_completion_to_uh_open_university: bool,
751 pub certification_enabled: bool,
752 pub certificate_configuration_id: Option<Uuid>,
753 pub needs_to_be_reviewed: bool,
754}
755
756pub async fn get_user_module_completion_statuses_for_course(
758 conn: &mut PgConnection,
759 user_id: Uuid,
760 course_id: Uuid,
761) -> ModelResult<Vec<UserModuleCompletionStatus>> {
762 let course = courses::get_course(conn, course_id).await?;
763 let course_modules = course_modules::get_by_course_id(conn, course_id).await?;
764
765 let course_module_completions_raw =
766 course_module_completions::get_all_by_course_id_and_user_id(conn, course_id, user_id)
767 .await?;
768
769 let course_module_completions: HashMap<Uuid, CourseModuleCompletion> =
770 course_module_completions_raw
771 .into_iter()
772 .sorted_by_key(|c| c.course_module_id)
773 .chunk_by(|c| c.course_module_id)
774 .into_iter()
775 .filter_map(|(module_id, group)| {
776 crate::course_module_completions::select_best_completion(group.collect())
777 .map(|best| (module_id, best))
778 })
779 .collect();
780
781 let all_default_certificate_configurations = crate::certificate_configurations::get_default_certificate_configurations_and_requirements_by_course(conn, course_id).await?;
782
783 let course_module_completion_statuses = course_modules
784 .into_iter()
785 .map(|module| {
786 let mut certificate_configuration_id = None;
787
788 let completion = course_module_completions.get(&module.id);
789 let passed = completion.map(|x| x.passed);
790 if module.certification_enabled && passed == Some(true) {
791 let default_certificate_configuration = all_default_certificate_configurations
793 .iter()
794 .find(|x| x.requirements.course_module_ids.contains(&module.id));
795 if let Some(default_certificate_configuration) = default_certificate_configuration {
796 certificate_configuration_id = Some(
797 default_certificate_configuration
798 .certificate_configuration
799 .id,
800 );
801 }
802 }
803 UserModuleCompletionStatus {
804 completed: completion.is_some(),
805 default: module.is_default_module(),
806 module_id: module.id,
807 name: module.name.unwrap_or_else(|| course.name.clone()),
808 order_number: module.order_number,
809 passed,
810 grade: completion.and_then(|x| x.grade),
811 prerequisite_modules_completed: completion
812 .is_some_and(|x| x.prerequisite_modules_completed),
813 enable_registering_completion_to_uh_open_university: module
814 .enable_registering_completion_to_uh_open_university,
815 certification_enabled: module.certification_enabled,
816 certificate_configuration_id,
817 needs_to_be_reviewed: completion.is_some_and(|x| x.needs_to_be_reviewed),
818 }
819 })
820 .collect();
821 Ok(course_module_completion_statuses)
822}
823
824#[derive(Clone, PartialEq, Deserialize, Serialize)]
825#[cfg_attr(feature = "ts_rs", derive(TS))]
826pub struct CompletionRegistrationLink {
827 pub url: String,
828}
829
830pub async fn get_completion_registration_link_and_save_attempt(
831 conn: &mut PgConnection,
832 user_id: Uuid,
833 course_module: &CourseModule,
834) -> ModelResult<CompletionRegistrationLink> {
835 if !course_module.enable_registering_completion_to_uh_open_university {
836 return Err(ModelError::new(
837 ModelErrorType::InvalidRequest,
838 "Completion registration is not enabled for this course module.".to_string(),
839 None,
840 ));
841 }
842 let user = users::get_by_id(conn, user_id).await?;
843
844 let course_module_completion = course_module_completions::get_latest_by_course_and_user_ids(
845 conn,
846 course_module.id,
847 user.id,
848 )
849 .await?;
850 course_module_completions::update_completion_registration_attempt_date(
851 conn,
852 course_module_completion.id,
853 Utc::now(),
854 )
855 .await?;
856 let registration_link = if let Some(link_override) =
857 course_module.completion_registration_link_override.as_ref()
858 {
859 link_override.clone()
860 } else {
861 let uh_course_code = course_module.uh_course_code.clone().ok_or_else(|| {
862 ModelError::new(
863 ModelErrorType::PreconditionFailed,
864 "Course module doesn't have an assossiated University of Helsinki course code."
865 .to_string(),
866 None,
867 )
868 })?;
869 open_university_registration_links::get_link_by_course_code(conn, &uh_course_code).await?
870 };
871
872 Ok(CompletionRegistrationLink {
873 url: registration_link,
874 })
875}
876
877#[cfg(test)]
878mod tests {
879 use chrono::Duration;
880 use user_exercise_states::{ReviewingStage, UserExerciseStateUpdate};
881
882 use super::*;
883
884 use crate::{
885 exercises::{ActivityProgress, GradingProgress},
886 test_helper::*,
887 };
888
889 mod grant_automatic_completion_if_eligible {
890 use super::*;
891 use crate::{
892 chapters::NewChapter,
893 course_modules::{
894 self, AutomaticCompletionRequirements, CompletionPolicy, NewCourseModule,
895 },
896 exercises::{self, ActivityProgress, GradingProgress},
897 library::content_management,
898 user_exercise_states::{self, ReviewingStage, UserExerciseStateUpdate},
899 };
900
901 #[tokio::test]
902 async fn grants_automatic_completion_but_no_prerequisite_for_default_module() {
903 insert_data!(:tx);
904 let (mut tx, user, course, _instance, default_module, _submodule_1, _submodule_2) =
905 create_test_data(tx).await;
906 update_automatic_completion_status_and_grant_if_eligible(
907 tx.as_mut(),
908 &default_module,
909 user,
910 )
911 .await
912 .unwrap();
913 let statuses =
914 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
915 .await
916 .unwrap();
917 let status = statuses
918 .iter()
919 .find(|x| x.module_id == default_module.id)
920 .unwrap();
921 assert!(status.completed);
922 assert!(!status.prerequisite_modules_completed);
923 }
924
925 #[tokio::test]
926 async fn grants_automatic_completion_but_no_prerequisite_for_submodule() {
927 insert_data!(:tx);
928 let (mut tx, user, course, _instance, _default_module, submodule_1, _submodule_2) =
929 create_test_data(tx).await;
930 update_automatic_completion_status_and_grant_if_eligible(
931 tx.as_mut(),
932 &submodule_1,
933 user,
934 )
935 .await
936 .unwrap();
937 let statuses =
938 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
939 .await
940 .unwrap();
941 let status = statuses
942 .iter()
943 .find(|x| x.module_id == submodule_1.id)
944 .unwrap();
945 assert!(status.completed);
946 assert!(!status.prerequisite_modules_completed);
947 }
948
949 #[tokio::test]
950 async fn grants_automatic_completion_for_eligible_submodule_when_completing_default_module()
951 {
952 insert_data!(:tx);
953 let (mut tx, user, course, _instance, default_module, submodule_1, submodule_2) =
954 create_test_data(tx).await;
955 update_automatic_completion_status_and_grant_if_eligible(
956 tx.as_mut(),
957 &default_module,
958 user,
959 )
960 .await
961 .unwrap();
962 update_automatic_completion_status_and_grant_if_eligible(
963 tx.as_mut(),
964 &submodule_1,
965 user,
966 )
967 .await
968 .unwrap();
969 update_automatic_completion_status_and_grant_if_eligible(
970 tx.as_mut(),
971 &submodule_2,
972 user,
973 )
974 .await
975 .unwrap();
976 let statuses =
977 get_user_module_completion_statuses_for_course(tx.as_mut(), user, course)
978 .await
979 .unwrap();
980 statuses.iter().for_each(|x| {
981 assert!(x.completed);
982 assert!(x.prerequisite_modules_completed);
983 });
984 }
985
986 async fn create_test_data(
987 mut tx: Tx<'_>,
988 ) -> (
989 Tx<'_>,
990 Uuid,
991 Uuid,
992 Uuid,
993 CourseModule,
994 CourseModule,
995 CourseModule,
996 ) {
997 insert_data!(tx: tx; :user, :org, :course, :instance, :course_module, :chapter, :page, :exercise);
998 let automatic_completion_policy =
999 CompletionPolicy::Automatic(AutomaticCompletionRequirements {
1000 course_module_id: course_module.id,
1001 number_of_exercises_attempted_treshold: Some(0),
1002 number_of_points_treshold: Some(0),
1003 requires_exam: false,
1004 });
1005 courses::update_course_base_module_completion_count_requirement(tx.as_mut(), course, 1)
1006 .await
1007 .unwrap();
1008 let course_module_2 = course_modules::insert(
1009 tx.as_mut(),
1010 PKeyPolicy::Generate,
1011 &NewCourseModule::new(course, Some("Module 2".to_string()), 1),
1012 )
1013 .await
1014 .unwrap();
1015 let (chapter_2, page2) = content_management::create_new_chapter(
1016 tx.as_mut(),
1017 PKeyPolicy::Generate,
1018 &NewChapter {
1019 name: "chapter 2".to_string(),
1020 color: None,
1021 course_id: course,
1022 chapter_number: 2,
1023 front_page_id: None,
1024 opens_at: None,
1025 deadline: None,
1026 course_module_id: Some(course_module_2.id),
1027 },
1028 user,
1029 |_, _, _| unimplemented!(),
1030 |_| unimplemented!(),
1031 )
1032 .await
1033 .unwrap();
1034
1035 let exercise_2 = exercises::insert(
1036 tx.as_mut(),
1037 PKeyPolicy::Generate,
1038 course,
1039 "",
1040 page2.id,
1041 chapter_2.id,
1042 0,
1043 )
1044 .await
1045 .unwrap();
1046 let user_exercise_state = user_exercise_states::get_or_create_user_exercise_state(
1047 tx.as_mut(),
1048 user,
1049 exercise,
1050 Some(course),
1051 None,
1052 )
1053 .await
1054 .unwrap();
1055 user_exercise_states::update(
1056 tx.as_mut(),
1057 UserExerciseStateUpdate {
1058 id: user_exercise_state.id,
1059 score_given: Some(0.0),
1060 activity_progress: ActivityProgress::Completed,
1061 reviewing_stage: ReviewingStage::NotStarted,
1062 grading_progress: GradingProgress::FullyGraded,
1063 },
1064 )
1065 .await
1066 .unwrap();
1067 let user_exercise_state_2 = user_exercise_states::get_or_create_user_exercise_state(
1068 tx.as_mut(),
1069 user,
1070 exercise_2,
1071 Some(course),
1072 None,
1073 )
1074 .await
1075 .unwrap();
1076 user_exercise_states::update(
1077 tx.as_mut(),
1078 UserExerciseStateUpdate {
1079 id: user_exercise_state_2.id,
1080 score_given: Some(0.0),
1081 activity_progress: ActivityProgress::Completed,
1082 reviewing_stage: ReviewingStage::NotStarted,
1083 grading_progress: GradingProgress::FullyGraded,
1084 },
1085 )
1086 .await
1087 .unwrap();
1088 let default_module = course_modules::get_default_by_course_id(tx.as_mut(), course)
1089 .await
1090 .unwrap();
1091 let default_module = course_modules::update_automatic_completion_status(
1092 tx.as_mut(),
1093 default_module.id,
1094 &automatic_completion_policy,
1095 )
1096 .await
1097 .unwrap();
1098 let course_module = course_modules::update_automatic_completion_status(
1099 tx.as_mut(),
1100 course_module.id,
1101 &automatic_completion_policy,
1102 )
1103 .await
1104 .unwrap();
1105 let course_module_2 = course_modules::update_automatic_completion_status(
1106 tx.as_mut(),
1107 course_module_2.id,
1108 &automatic_completion_policy,
1109 )
1110 .await
1111 .unwrap();
1112 (
1113 tx,
1114 user,
1115 course,
1116 instance.id,
1117 default_module,
1118 course_module,
1119 course_module_2,
1120 )
1121 }
1122 }
1123
1124 #[tokio::test]
1125 async fn tags_suspected_cheater() {
1126 insert_data!(:tx, user:user, :org, course:course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1127
1128 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1129 .await
1130 .unwrap();
1131 let state = user_exercise_states::get_or_create_user_exercise_state(
1132 tx.as_mut(),
1133 user,
1134 exercise,
1135 Some(course),
1136 None,
1137 )
1138 .await
1139 .unwrap();
1140 user_exercise_states::update(
1141 tx.as_mut(),
1142 UserExerciseStateUpdate {
1143 id: state.id,
1144 score_given: Some(10.0),
1145 activity_progress: ActivityProgress::Completed,
1146 reviewing_stage: ReviewingStage::NotStarted,
1147 grading_progress: GradingProgress::FullyGraded,
1148 },
1149 )
1150 .await
1151 .unwrap();
1152
1153 let completion = course_module_completions::insert(
1154 tx.as_mut(),
1155 PKeyPolicy::Generate,
1156 &NewCourseModuleCompletion {
1157 course_id: course,
1158 course_module_id: course_module.id,
1159 user_id: user,
1160 completion_date: Utc::now() + Duration::days(1),
1161 completion_registration_attempt_date: None,
1162 completion_language: "en-US".to_string(),
1163 eligible_for_ects: false,
1164 email: "email".to_string(),
1165 grade: None,
1166 passed: true,
1167 },
1168 CourseModuleCompletionGranter::Automatic,
1169 )
1170 .await
1171 .unwrap();
1172 let thresholds = suspected_cheaters::insert_thresholds_by_module_id(
1173 tx.as_mut(),
1174 course_module.id,
1175 259200,
1176 )
1177 .await
1178 .unwrap();
1179 check_and_insert_suspected_cheaters(tx.as_mut(), user, course, &thresholds, completion)
1180 .await
1181 .unwrap();
1182
1183 let cheaters =
1184 suspected_cheaters::get_all_suspected_cheaters_in_course(tx.as_mut(), course, false)
1185 .await
1186 .unwrap();
1187 assert_eq!(cheaters.len(), 1);
1188 assert_eq!(cheaters[0].user_id, user);
1189 }
1190
1191 #[tokio::test]
1192 async fn doesnt_tag_suspected_cheater() {
1193 insert_data!(:tx, user:user, :org, :course, instance:instance, course_module:course_module, :chapter, :page, :exercise);
1194
1195 crate::library::course_instances::enroll(tx.as_mut(), user, instance.id, &[])
1196 .await
1197 .unwrap();
1198 let state = user_exercise_states::get_or_create_user_exercise_state(
1199 tx.as_mut(),
1200 user,
1201 exercise,
1202 Some(course),
1203 None,
1204 )
1205 .await
1206 .unwrap();
1207 user_exercise_states::update(
1208 tx.as_mut(),
1209 UserExerciseStateUpdate {
1210 id: state.id,
1211 score_given: Some(9.0),
1212 activity_progress: ActivityProgress::Completed,
1213 reviewing_stage: ReviewingStage::NotStarted,
1214 grading_progress: GradingProgress::FullyGraded,
1215 },
1216 )
1217 .await
1218 .unwrap();
1219
1220 course_module_completions::insert(
1221 tx.as_mut(),
1222 PKeyPolicy::Generate,
1223 &NewCourseModuleCompletion {
1224 course_id: course,
1225 course_module_id: course_module.id,
1226 user_id: user,
1227 completion_date: Utc::now() + Duration::days(3),
1228 completion_registration_attempt_date: None,
1229 completion_language: "en-US".to_string(),
1230 eligible_for_ects: false,
1231 email: "email".to_string(),
1232 grade: Some(9),
1233 passed: true,
1234 },
1235 CourseModuleCompletionGranter::Automatic,
1236 )
1237 .await
1238 .unwrap();
1239 suspected_cheaters::insert_thresholds_by_module_id(tx.as_mut(), course_module.id, 172800)
1240 .await
1241 .unwrap();
1242 update_automatic_completion_status_and_grant_if_eligible(tx.as_mut(), &course_module, user)
1243 .await
1244 .unwrap();
1245
1246 let cheaters =
1247 suspected_cheaters::get_all_suspected_cheaters_in_course(tx.as_mut(), course, false)
1248 .await
1249 .unwrap();
1250 assert!(cheaters.is_empty());
1251 }
1252
1253 }