Skip to main content

headless_lms_models/library/credit_registration/
config_validation.rs

1//! Deciding whether one module's credit-registration configuration is usable.
2//!
3//! Pure: the facts are gathered by
4//! [`crate::course_module_suotar_configurations::get_config_facts_for_enabled_modules`] and the
5//! verdict is stamped back by
6//! [`crate::course_module_suotar_configurations::record_config_check`].
7
8use crate::course_module_suotar_configurations::{SuotarConfigCheck, SuotarModuleConfigFacts};
9
10use super::grade_mapping::{GradeScaleFamily, grade_scale_family};
11
12/// The problems the check reports, in the order they block a registration. English on purpose:
13/// this is operator diagnostics stored on the row, not student-facing copy.
14const NO_COURSE_CODE: &str = "No uh_course_code, so nothing can be submitted.";
15const COURSE_CODE_NOT_FOUND: &str = "The study registry does not know this uh_course_code.";
16const NO_REALISATION: &str = "No active course unit realisation, so the roster is never listed.";
17const NO_ECTS: &str = "No ects_credits, so there is nothing to register.";
18const NO_PRODUCT_ID: &str =
19    "No open_university_product_id, so the re-enrol guidance has no working link.";
20const NO_PRODUCT_TOKEN: &str =
21    "No product access token has been resolved, so the re-enrol guidance has no working link.";
22const UNKNOWN_GRADE_SCALE: &str =
23    "The grade scale override is not a scale the study registry accepts.";
24const NUMERIC_SCALE_ON_UNGRADED_COMPLETIONS: &str =
25    "The grade scale override is numeric but the module has passed completions with no grade.";
26const OLD_FLOW_ALSO_ENABLED: &str = "enable_registering_completion_to_uh_open_university is on as well, which would register the same completion twice.";
27
28/// Checks one module's configuration.
29///
30/// `course_code_resolves` is left `None` while no listing has been attempted: never checked is not
31/// the same as checked and failed, and the Courses tab renders the two differently.
32pub fn check_module_config(facts: &SuotarModuleConfigFacts) -> SuotarConfigCheck {
33    let mut problems: Vec<&str> = Vec::new();
34
35    let has_course_code = facts
36        .uh_course_code
37        .as_deref()
38        .is_some_and(|code| !code.trim().is_empty());
39    let course_code_resolves = if !has_course_code {
40        problems.push(NO_COURSE_CODE);
41        Some(false)
42    } else if facts.course_code_not_found {
43        problems.push(COURSE_CODE_NOT_FOUND);
44        Some(false)
45    } else if facts.listed_successfully {
46        Some(true)
47    } else {
48        None
49    };
50    if facts.active_realisation_count == 0 {
51        problems.push(NO_REALISATION);
52    }
53    if facts.ects_credits.is_none() {
54        problems.push(NO_ECTS);
55    }
56
57    let has_product_id = facts
58        .open_university_product_id
59        .as_deref()
60        .is_some_and(|id| !id.trim().is_empty());
61    if !has_product_id {
62        problems.push(NO_PRODUCT_ID);
63    } else if !facts.product_token_found {
64        problems.push(NO_PRODUCT_TOKEN);
65    }
66
67    match facts.grade_scale_id.as_deref().map(grade_scale_family) {
68        Some(None) => problems.push(UNKNOWN_GRADE_SCALE),
69        Some(Some(GradeScaleFamily::Numeric)) if facts.has_passed_completions_without_a_grade => {
70            problems.push(NUMERIC_SCALE_ON_UNGRADED_COMPLETIONS)
71        }
72        _ => {}
73    }
74    if facts.old_flow_also_enabled {
75        problems.push(OLD_FLOW_ALSO_ENABLED);
76    }
77
78    SuotarConfigCheck {
79        course_code_resolves,
80        // Never left unknown, so `course_module_suotar_configurations_check_result` holds even
81        // when the course code could not be judged: no product configured is a definite "no token".
82        product_token_found: Some(has_product_id && facts.product_token_found),
83        message: (!problems.is_empty()).then(|| problems.join(" ")),
84    }
85}