headless_lms_server/programs/
open_university_registration_link_fetcher.rs1use std::env;
2
3use crate::setup_tracing;
4use anyhow::Context;
5use chrono::{NaiveDateTime, Utc};
6use dotenv::dotenv;
7use headless_lms_models as models;
8use reqwest::Client;
9use serde::{Deserialize, Serialize};
10use sqlx::{PgConnection, PgPool};
11
12const OPEN_UNIVERSITY_REGISTRATION_BASE_URL: &str =
13 "https://www.avoin.helsinki.fi/palvelut/esittely.aspx?s=";
14const OPEN_UNIVERSITY_COURSE_URL: &str = "OPEN_UNIVERSITY_COURSE_URL";
15const OPEN_UNIVERSITY_TOKEN: &str = "OPEN_UNIVERSITY_TOKEN";
16
17pub async fn main() -> anyhow::Result<()> {
18 unsafe { env::set_var("RUST_LOG", "info,actix_web=info,sqlx=warn") };
20 dotenv().ok();
21 setup_tracing()?;
22 let database_url = env::var("DATABASE_URL")
23 .unwrap_or_else(|_| "postgres://localhost/headless_lms_dev".to_string());
24 let open_university_course_url = env::var(OPEN_UNIVERSITY_COURSE_URL);
25 let open_university_token = env::var(OPEN_UNIVERSITY_TOKEN);
26 match (open_university_course_url, open_university_token) {
27 (Ok(url), Ok(token)) => {
28 tracing::info!("Fetching and updating Open University completion links.");
29 let db_pool = PgPool::connect(&database_url).await?;
30 let mut conn = db_pool.acquire().await?;
31 let res = fetch_and_update_completion_links(&mut conn, &url, &token).await;
32 match res {
33 Ok(updates) => {
34 tracing::info!("{} registration completion links were updated.", updates)
35 }
36 Err(err) => tracing::error!(
37 "Updating open university completion links resulted in an error: {:#?}",
38 err,
39 ),
40 };
41 }
42 _ => {
43 tracing::info!(
44 "Open university completion link fetch job was a no-op; environment values {} and {} need to be defined.",
45 OPEN_UNIVERSITY_COURSE_URL,
46 OPEN_UNIVERSITY_TOKEN,
47 );
48 }
49 }
50 Ok(())
51}
52
53async fn fetch_and_update_completion_links(
56 conn: &mut PgConnection,
57 open_university_course_url: &str,
58 open_university_token: &str,
59) -> anyhow::Result<u32> {
60 let mut updates = 0;
61 let client = Client::default();
62 let now = Utc::now().naive_utc();
63 for uh_course_code in
64 models::course_modules::get_all_uh_course_codes_for_open_university(conn).await?
65 {
66 let url = format!("{}{}", &open_university_course_url, &uh_course_code);
67 let infos =
69 get_open_university_info_for_course_code(&client, &url, open_university_token).await;
70 match infos {
71 Ok(infos) => {
72 let best_candidate = select_best_candidate(now, infos);
74 if let Some(open_university_info) = best_candidate {
75 let res = update_course_registration_link(
77 conn,
78 &uh_course_code,
79 &open_university_info,
80 )
81 .await;
82 if res.is_err() {
83 tracing::error!(
84 "Failed to update link for course code {}",
85 &uh_course_code
86 );
87 } else {
88 updates += 1;
89 }
90 }
91 }
92 _ => {
93 tracing::error!(
94 "Failed to get completion registration info for course code '{}'.",
95 uh_course_code,
96 );
97 }
98 }
99 }
100 Ok(updates)
101}
102
103#[derive(Serialize, Deserialize, Debug)]
104struct OpenUniversityInfo {
105 #[serde(rename = "oodi_id")]
106 link: String,
107 #[serde(rename = "alkupvm")]
108 start_date: NaiveDateTime,
109 #[serde(rename = "loppupvm")]
110 end_date: NaiveDateTime,
111}
112
113async fn get_open_university_info_for_course_code(
114 client: &Client,
115 course_url: &str,
116 token: &str,
117) -> anyhow::Result<Vec<OpenUniversityInfo>> {
118 let res = client
119 .get(course_url)
120 .header("Authorized", format!("Basic {}", token))
121 .send()
122 .await
123 .context("Failed to send a request to Open University.")?;
124 let alternatives: Vec<OpenUniversityInfo> = res.json().await?;
125 Ok(alternatives)
126}
127
128fn select_best_candidate(
129 now: NaiveDateTime,
130 c: Vec<OpenUniversityInfo>,
131) -> Option<OpenUniversityInfo> {
132 c.into_iter()
133 .filter(|x| x.start_date <= now)
134 .max_by(|a, b| a.end_date.cmp(&b.end_date))
135}
136
137async fn update_course_registration_link(
138 conn: &mut PgConnection,
139 uh_course_code: &str,
140 open_university_info: &OpenUniversityInfo,
141) -> anyhow::Result<()> {
142 let full_url = format!(
143 "{}{}",
144 OPEN_UNIVERSITY_REGISTRATION_BASE_URL, open_university_info.link,
145 );
146 models::open_university_registration_links::upsert(conn, uh_course_code, &full_url).await?;
147 Ok(())
148}