headless_lms_credit_registration/runtime/suotar/
rosters.rs1use futures::future::join_all;
5use headless_lms_models::credit_registrations::CreditRegistrationErrorCode;
6use headless_lms_models::library::credit_registration::study_registry::RosterPerson;
7use headless_lms_utils::services::suotar::{
8 ListByCourseRequestItem, SuotarEndpoint, SuotarItemStatus, endpoints, new_request_item_id,
9};
10use secrecy::ExposeSecret;
11use tracing::Instrument;
12
13use super::decode::{registry_error, roster, roster_person};
14use super::encode::roster_item;
15use super::gate::Exchange;
16use super::{InteractiveSuotar, SuotarStudyRegistry};
17use crate::registry::{
18 CourseCode, RegistryError, RosterCode, RosterListing, RosterSearch, StudentNumber,
19};
20
21const ENDPOINT: SuotarEndpoint = SuotarEndpoint::ListByCourse;
22
23pub(super) async fn list(
24 registry: &mut SuotarStudyRegistry<'_>,
25 request: &[RosterCode],
26) -> Result<RosterListing, RegistryError> {
27 registry.gate.spend(ENDPOINT, 1);
28 let items: Vec<ListByCourseRequestItem> = request
29 .iter()
30 .map(|code| roster_item(&code.course_code, new_request_item_id()))
31 .collect();
32 let request_item_ids: Vec<String> = items
33 .iter()
34 .map(|item| item.request_item_id.clone())
35 .collect();
36 let span = super::request_span(ENDPOINT, items.len());
37 let response = registry
38 .client
39 .post::<endpoints::ListByCourse>(registry.call_context(Vec::new()), items)
40 .instrument(span)
41 .await;
42 let response = match response {
43 Ok(response) => response,
44 Err(error) => {
45 let refusal = registry_error(&error);
46 let is_known_bad_code = matches!(request, [only] if only.is_fetched_alone)
48 && refusal.blames_request_items()
49 && !refusal.kind.is_outage();
50 registry.gate.record(
51 ENDPOINT,
52 if is_known_bad_code {
53 Exchange::RefusedAlone(&error)
54 } else {
55 Exchange::Refused(&error)
56 },
57 );
58 return Err(refusal);
59 }
60 };
61 registry.gate.record(
62 ENDPOINT,
63 Exchange::answered(
64 &response,
65 "Every course code of the batch came back unavailable.",
66 ),
67 );
68 Ok(RosterListing {
69 duration: response.duration,
70 rosters: request
71 .iter()
72 .zip(&request_item_ids)
73 .map(|(code, request_item_id)| roster(&response, request_item_id, &code.course_code))
74 .collect(),
75 })
76}
77
78pub(super) async fn fetch_one(
79 registry: &InteractiveSuotar<'_>,
80 course_code: &CourseCode,
81) -> Option<Vec<RosterPerson>> {
82 let request_item_id = new_request_item_id();
83 let span = super::request_span(ENDPOINT, 1);
84 let response = registry
85 .client
86 .post::<endpoints::ListByCourse>(
87 registry.call_context(),
88 vec![roster_item(course_code, request_item_id.clone())],
89 )
90 .instrument(span)
91 .await
92 .inspect_err(|error| {
93 warn!(error = %error, "Could not list a course code's roster for an admin");
94 })
95 .ok()?;
96 match roster(&response, &request_item_id, course_code) {
97 Ok(people) => Some(people),
98 Err(CreditRegistrationErrorCode::CourseCodeNotFound) => Some(Vec::new()),
99 Err(_) => None,
100 }
101}
102
103pub(super) async fn search(
104 registry: &InteractiveSuotar<'_>,
105 codes: &[CourseCode],
106 student_number: &StudentNumber,
107) -> RosterSearch {
108 let responses = join_all(codes.iter().map(|course_code| {
112 let span = super::request_span(ENDPOINT, 1);
113 registry
114 .client
115 .post::<endpoints::ListByCourse>(
116 registry.call_context(),
117 vec![roster_item(course_code, new_request_item_id())],
118 )
119 .instrument(span)
120 }))
121 .await;
122 let mut has_unanswered_code = false;
123 let mut person = None;
124 for response in responses {
125 let response = match response {
126 Ok(response) => response,
127 Err(error) => {
128 warn!(
129 error = %error,
130 "Could not list a course code's roster for a linking mail resend"
131 );
132 has_unanswered_code = true;
133 continue;
134 }
135 };
136 person = person.or_else(|| {
137 response
138 .items
139 .iter()
140 .filter(|item| item.status == SuotarItemStatus::Ok)
141 .filter_map(|item| item.result.as_ref())
142 .flat_map(|result| result.people.iter())
143 .find(|candidate| {
144 candidate.student_number.expose_secret() == student_number.expose()
145 })
146 .map(roster_person)
147 });
148 }
149 RosterSearch {
150 person,
151 has_unanswered_code,
152 }
153}