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