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