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`], the course
5//! code verdict comes from Suotar's `course-codes/validate`, and the result is stamped back by
6//! [`crate::course_module_suotar_configurations::record_config_check`].
7
8use std::borrow::Cow;
9
10use crate::course_module_suotar_configurations::{SuotarConfigCheck, SuotarModuleConfigFacts};
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_ALLOWED: &str = "Suotar does not accept this uh_course_code:";
16const NO_ECTS: &str = "No ects_credits, so there is nothing to register.";
17
18/// What Suotar's `course-codes/validate` said about a course code.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum CourseCodeVerdict {
21    Allowed,
22    /// Carries Suotar's own reason.
23    NotAllowed {
24        reason: String,
25    },
26}
27
28impl CourseCodeVerdict {
29    /// The verdict the last check stored for the module's current course code, for rechecking
30    /// without asking Suotar again.
31    pub fn stored(facts: &SuotarModuleConfigFacts) -> Option<Self> {
32        match facts.stored_course_code_allowed? {
33            true => Some(Self::Allowed),
34            false => Some(Self::NotAllowed {
35                reason: facts
36                    .stored_course_code_rejection
37                    .clone()
38                    .unwrap_or_default(),
39            }),
40        }
41    }
42}
43
44/// Checks one module's configuration.
45///
46/// `verdict` is `None` when Suotar gave no answer for the code, which leaves `course_code_allowed`
47/// `None`: never checked is not the same as checked and failed, and the Courses tab renders the two
48/// differently.
49pub fn check_module_config(
50    facts: &SuotarModuleConfigFacts,
51    verdict: Option<&CourseCodeVerdict>,
52) -> SuotarConfigCheck {
53    let mut problems: Vec<Cow<'static, str>> = Vec::new();
54
55    let uh_course_code = facts
56        .uh_course_code
57        .as_deref()
58        .map(str::trim)
59        .filter(|code| !code.is_empty());
60    let (course_code_allowed, checked_course_code, course_code_rejection) =
61        match (uh_course_code, verdict) {
62            (None, _) => {
63                problems.push(NO_COURSE_CODE.into());
64                (Some(false), None, None)
65            }
66            (Some(code), Some(CourseCodeVerdict::Allowed)) => {
67                (Some(true), Some(code.to_string()), None)
68            }
69            (Some(code), Some(CourseCodeVerdict::NotAllowed { reason })) => {
70                problems.push(format!("{COURSE_CODE_NOT_ALLOWED} {reason}").into());
71                (Some(false), Some(code.to_string()), Some(reason.clone()))
72            }
73            (Some(_), None) => (None, None, None),
74        };
75    if facts.ects_credits.is_none() {
76        problems.push(NO_ECTS.into());
77    }
78
79    SuotarConfigCheck {
80        course_code_allowed,
81        checked_course_code,
82        course_code_rejection,
83        message: (!problems.is_empty()).then(|| problems.join(" ")),
84    }
85}