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