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