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