Skip to main content

headless_lms_models/course_module_suotar_configurations/
listing.rs

1//! Enrolment discovery's per-module listing: which modules a roster request covers, and what the
2//! last listing of each found.
3
4use crate::credit_registrations::CreditRegistrationErrorCode;
5use crate::prelude::*;
6
7/// An active module and the facts a `list-by-course` request for it needs.
8#[derive(Debug, Clone, PartialEq)]
9pub struct ModuleToList {
10    pub course_module_id: Uuid,
11    pub course_id: Uuid,
12    pub uh_course_code: String,
13    /// The language the mails this listing sets off are written in.
14    pub course_language_code: String,
15}
16
17/// Every active, listable module of one course, unpaginated.
18pub async fn get_active_modules_for_course(
19    conn: &mut PgConnection,
20    course_id: Uuid,
21) -> ModelResult<Vec<ModuleToList>> {
22    let res = sqlx::query_as!(
23        ModuleToList,
24        r#"
25SELECT acm.course_module_id AS "course_module_id!",
26  acm.course_id AS "course_id!",
27  TRIM(cm.uh_course_code) AS "uh_course_code!",
28  co.language_code AS "course_language_code!"
29FROM credit_registration_active_course_modules acm
30  JOIN course_modules cm ON cm.id = acm.course_module_id AND cm.deleted_at IS NULL
31  JOIN courses co ON co.id = acm.course_id AND co.deleted_at IS NULL
32WHERE TRIM(COALESCE(cm.uh_course_code, '')) <> ''
33  AND acm.course_id = $1
34ORDER BY cm.order_number,
35  cm.id
36        "#,
37        course_id,
38    )
39    .fetch_all(conn)
40    .await?;
41    Ok(res)
42}
43
44/// One active module's last successful listing, and whether the attempts since then have been
45/// failing.
46#[derive(Debug, Clone, PartialEq)]
47pub struct ModuleDiscoveryReport {
48    pub course_id: Uuid,
49    pub course_name: String,
50    pub course_module_id: Uuid,
51    pub course_module_name: Option<String>,
52    pub uh_course_code: Option<String>,
53    pub last_listed_at: Option<DateTime<Utc>>,
54    pub last_listing_attempted_at: Option<DateTime<Utc>>,
55    pub last_listing_error: Option<CreditRegistrationErrorCode>,
56    pub consecutive_listing_failures: i32,
57}
58
59/// Every active module's listing status, for the account-linking dashboard.
60pub async fn get_active_discovery_reports(
61    conn: &mut PgConnection,
62) -> ModelResult<Vec<ModuleDiscoveryReport>> {
63    let res = sqlx::query_as!(
64        ModuleDiscoveryReport,
65        r#"
66SELECT cm.course_id,
67  c.name AS course_name,
68  conf.course_module_id,
69  cm.name AS course_module_name,
70  cm.uh_course_code,
71  conf.last_listed_at,
72  conf.last_listing_attempted_at,
73  conf.last_listing_error AS "last_listing_error?",
74  conf.consecutive_listing_failures
75FROM course_module_suotar_configurations conf
76  JOIN credit_registration_active_course_modules acm ON acm.course_module_id = conf.course_module_id
77  JOIN course_modules cm ON cm.id = conf.course_module_id AND cm.deleted_at IS NULL
78  JOIN courses c ON c.id = cm.course_id AND c.deleted_at IS NULL
79WHERE conf.deleted_at IS NULL
80  AND c.deleted_at IS NULL
81ORDER BY c.name,
82  cm.order_number
83        "#,
84    )
85    .fetch_all(conn)
86    .await?;
87    Ok(res)
88}
89
90/// Records a listing attempt whose roster never arrived. `last_listed_at` keeps describing the last
91/// roster that did, because a failed listing must not read as an empty course.
92pub async fn mark_listing_failed(
93    conn: &mut PgConnection,
94    course_module_id: Uuid,
95    error: CreditRegistrationErrorCode,
96) -> ModelResult<()> {
97    sqlx::query!(
98        r#"
99UPDATE course_module_suotar_configurations
100SET last_listing_attempted_at = now(),
101  last_listing_error = $2,
102  consecutive_listing_failures = consecutive_listing_failures + 1
103WHERE course_module_id = $1
104  AND deleted_at IS NULL
105        "#,
106        course_module_id,
107        error as CreditRegistrationErrorCode,
108    )
109    .execute(conn)
110    .await?;
111    Ok(())
112}
113
114/// Records a listing whose roster arrived and clears the failure streak. `fed_linking` is whether
115/// account linking was on for it; only then does it move `last_listed_at`. Sibling of
116/// [`mark_listing_failed`].
117pub async fn mark_listing_succeeded(
118    conn: &mut PgConnection,
119    course_module_id: Uuid,
120    fed_linking: bool,
121) -> ModelResult<()> {
122    sqlx::query!(
123        r#"
124UPDATE course_module_suotar_configurations
125SET last_listed_at = CASE
126    WHEN $2 THEN now()
127    ELSE last_listed_at
128  END,
129  last_listing_attempted_at = now(),
130  last_listing_error = NULL,
131  consecutive_listing_failures = 0
132WHERE course_module_id = $1
133  AND deleted_at IS NULL
134        "#,
135        course_module_id,
136        fed_linking,
137    )
138    .execute(conn)
139    .await?;
140    Ok(())
141}