Skip to main content

headless_lms_credit_registration/registry/
requests.rs

1//! What one item of a request to the study registry asks.
2
3use chrono::{DateTime, Utc};
4use headless_lms_models::library::credit_registration::grade_mapping::MappedGrade;
5
6use super::answers::{EnrolmentAnswer, ImportAnswer, PersonAnswer, VerificationAnswer};
7use super::batch::{BatchEntry, BatchOptions, BatchReply, BatchRequest};
8use super::ids::{AttainmentId, CourseCode, StudentNumber};
9use super::{RegistryOperation, StudyRegistry};
10
11/// Who a student number belongs to in Sisu.
12pub(crate) struct PersonLookup {
13    pub student_number: StudentNumber,
14}
15
16impl BatchRequest for PersonLookup {
17    const OPERATION: RegistryOperation = RegistryOperation::ResolvePersons;
18    type Answer = PersonAnswer;
19
20    async fn send<Registry: StudyRegistry, K>(
21        registry: &mut Registry,
22        entries: Vec<BatchEntry<K, Self>>,
23        options: BatchOptions,
24    ) -> BatchReply<K, Self, PersonAnswer> {
25        registry.resolve_persons(entries, options).await
26    }
27}
28
29/// A student's enrolments and existing attainments on one course code.
30pub(crate) struct EnrolmentLookup {
31    pub student_number: StudentNumber,
32    pub course_code: CourseCode,
33}
34
35impl BatchRequest for EnrolmentLookup {
36    const OPERATION: RegistryOperation = RegistryOperation::ResolveEnrolments;
37    type Answer = EnrolmentAnswer;
38
39    async fn send<Registry: StudyRegistry, K>(
40        registry: &mut Registry,
41        entries: Vec<BatchEntry<K, Self>>,
42        options: BatchOptions,
43    ) -> BatchReply<K, Self, EnrolmentAnswer> {
44        registry.resolve_enrolments(entries, options).await
45    }
46}
47
48/// One attainment to register, exactly as it goes out.
49pub(crate) struct AttainmentSubmission {
50    pub student_number: StudentNumber,
51    pub course_code: CourseCode,
52    pub enrolment_id: String,
53    pub attained_at: DateTime<Utc>,
54    pub attainment_language: String,
55    pub grade: MappedGrade,
56    pub credits: Credits,
57}
58
59impl BatchRequest for AttainmentSubmission {
60    const OPERATION: RegistryOperation = RegistryOperation::ImportAttainments;
61    type Answer = ImportAnswer;
62
63    async fn send<Registry: StudyRegistry, K>(
64        registry: &mut Registry,
65        entries: Vec<BatchEntry<K, Self>>,
66        options: BatchOptions,
67    ) -> BatchReply<K, Self, ImportAnswer> {
68        registry.import_attainments(entries, options).await
69    }
70}
71
72/// ECTS credits as they go on the wire: finite, and rounded to the tenth credits never go finer
73/// than.
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub(crate) struct Credits(f64);
76
77impl Credits {
78    /// `None` for a value that is not finite, which Suotar refuses. Rounding also removes the
79    /// f32-to-f64 widening error, which would send 2.7 as 2.700000047683716.
80    pub(crate) fn from_stored(credits: f32) -> Option<Self> {
81        credits
82            .is_finite()
83            .then(|| Self((f64::from(credits) * 10.0).round() / 10.0))
84    }
85
86    pub(crate) fn as_f64(self) -> f64 {
87        self.0
88    }
89}
90
91/// What became of one submission of ours.
92pub(crate) struct VerificationRequest {
93    pub submitted_attainment_id: AttainmentId,
94}
95
96impl BatchRequest for VerificationRequest {
97    const OPERATION: RegistryOperation = RegistryOperation::VerifyAttainments;
98    type Answer = VerificationAnswer;
99
100    async fn send<Registry: StudyRegistry, K>(
101        registry: &mut Registry,
102        entries: Vec<BatchEntry<K, Self>>,
103        options: BatchOptions,
104    ) -> BatchReply<K, Self, VerificationAnswer> {
105        registry.verify_attainments(entries, options).await
106    }
107}
108
109/// One course code whose roster is due.
110#[derive(Clone)]
111pub(crate) struct RosterCode {
112    pub course_code: CourseCode,
113    /// The code failed in a batch before, so it goes in a request of its own.
114    pub is_fetched_alone: bool,
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn stored_credits_go_out_rounded_to_a_tenth_and_only_when_finite() {
123        assert_eq!(Credits::from_stored(2.7).map(Credits::as_f64), Some(2.7));
124        assert_eq!(Credits::from_stored(f32::NAN), None);
125        assert_eq!(Credits::from_stored(f32::INFINITY), None);
126    }
127}