headless_lms_models/
user_details.rs

1use std::collections::HashMap;
2
3use futures::Stream;
4
5use crate::{prelude::*, users::User};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8#[cfg_attr(feature = "ts_rs", derive(TS))]
9pub struct UserDetail {
10    pub user_id: Uuid,
11    pub created_at: DateTime<Utc>,
12    pub updated_at: DateTime<Utc>,
13    pub email: String,
14    pub first_name: Option<String>,
15    pub last_name: Option<String>,
16    pub search_helper: Option<String>,
17    pub country: Option<String>,
18    pub email_communication_consent: Option<bool>,
19}
20
21pub async fn get_user_details_by_user_id(
22    conn: &mut PgConnection,
23    user_id: Uuid,
24) -> ModelResult<UserDetail> {
25    let res = sqlx::query_as!(
26        UserDetail,
27        "
28SELECT *
29FROM user_details
30WHERE user_id = $1 ",
31        user_id
32    )
33    .fetch_one(conn)
34    .await?;
35    Ok(res)
36}
37
38pub async fn get_users_details_by_user_id_map(
39    conn: &mut PgConnection,
40    users: &[User],
41) -> ModelResult<HashMap<Uuid, UserDetail>> {
42    let ids = users.iter().map(|u| u.id).collect::<Vec<_>>();
43    let details = sqlx::query_as!(
44        UserDetail,
45        "
46SELECT *
47FROM user_details
48WHERE user_id IN (
49    SELECT UNNEST($1::uuid [])
50  )
51",
52        &ids
53    )
54    .fetch_all(conn)
55    .await?;
56    let mut res = HashMap::new();
57    details.into_iter().for_each(|d| {
58        res.insert(d.user_id, d);
59    });
60    Ok(res)
61}
62
63/// Includes all users who have returned an exercise on a course
64pub fn stream_users_details_having_user_exercise_states_on_course(
65    conn: &mut PgConnection,
66    course_id: Uuid,
67) -> impl Stream<Item = sqlx::Result<UserDetail>> + '_ {
68    sqlx::query_as!(
69        UserDetail,
70        "
71SELECT distinct (ud.user_id),
72 ud.created_at,
73 ud.updated_at,
74 ud.first_name,
75 ud.last_name,
76 ud.email,
77 ud.search_helper,
78 ud.country,
79 ud.email_communication_consent
80FROM user_details ud
81JOIN users u
82  ON u.id = ud.user_id
83JOIN user_exercise_states ues
84  ON ud.user_id = ues.user_id
85JOIN course_instances ci
86  ON ci.id = ues.course_instance_id
87WHERE ci.course_id = $1
88  AND u.deleted_at IS NULL
89  AND ues.deleted_at IS NULL
90  AND ci.deleted_at IS NULL;
91        ",
92        course_id
93    )
94    .fetch(conn)
95}
96
97pub async fn search_for_user_details_by_email(
98    conn: &mut PgConnection,
99    email: &str,
100) -> ModelResult<Vec<UserDetail>> {
101    let res = sqlx::query_as!(
102        UserDetail,
103        "
104SELECT *
105FROM user_details
106WHERE lower(email::text) LIKE '%' || lower($1) || '%'
107LIMIT 1000;
108",
109        email.trim(),
110    )
111    .fetch_all(conn)
112    .await?;
113    Ok(res)
114}
115
116pub async fn search_for_user_details_by_other_details(
117    conn: &mut PgConnection,
118    search: &str,
119) -> ModelResult<Vec<UserDetail>> {
120    let res = sqlx::query_as!(
121        UserDetail,
122        "
123SELECT *
124FROM user_details
125WHERE lower(search_helper::text) LIKE '%' || lower($1) || '%'
126LIMIT 1000;
127",
128        search.trim(),
129    )
130    .fetch_all(conn)
131    .await?;
132    Ok(res)
133}
134
135pub async fn search_for_user_details_fuzzy_match(
136    conn: &mut PgConnection,
137    search: &str,
138) -> ModelResult<Vec<UserDetail>> {
139    // For email domains, the fuzzy match returns too much results that have the same domain
140    // To combat this, we omit the email domain from the fuzzy match
141    let search = search.split('@').next().unwrap_or(search);
142
143    let res = sqlx::query_as!(
144        UserDetail,
145        "
146SELECT user_id,
147  created_at,
148  updated_at,
149  email,
150  first_name,
151  last_name,
152  search_helper,
153  country,
154  email_communication_consent
155FROM (
156    SELECT *,
157      LOWER($1) <<->search_helper AS dist
158    FROM user_details
159    ORDER BY dist, LENGTH(search_helper)
160    LIMIT 100
161  ) search
162WHERE dist < 0.7;
163",
164        search.trim(),
165    )
166    .fetch_all(conn)
167    .await?;
168    Ok(res)
169}
170
171/// Retrieves all users enrolled in a specific course
172pub async fn get_users_by_course_id(
173    conn: &mut PgConnection,
174    course_id: Uuid,
175) -> ModelResult<Vec<UserDetail>> {
176    let res = sqlx::query_as!(
177        UserDetail,
178        r#"
179SELECT d.user_id,
180  d.created_at,
181  d.updated_at,
182  d.email,
183  d.first_name,
184  d.last_name,
185  d.search_helper,
186  d.country,
187  d.email_communication_consent
188FROM course_instance_enrollments e
189  JOIN user_details d ON e.user_id = d.user_id
190WHERE e.course_id = $1
191  AND e.deleted_at IS NULL
192        "#,
193        course_id
194    )
195    .fetch_all(conn)
196    .await?;
197
198    Ok(res)
199}
200
201pub async fn update_user_country(
202    conn: &mut PgConnection,
203    user_id: Uuid,
204    country: &str,
205) -> Result<(), sqlx::Error> {
206    sqlx::query!(
207        r#"
208UPDATE user_details
209SET country = $1
210WHERE user_id = $2
211"#,
212        country,
213        user_id,
214    )
215    .execute(conn)
216    .await?;
217    Ok(())
218}
219
220pub async fn update_user_email_communication_consent(
221    conn: &mut PgConnection,
222    user_id: Uuid,
223    email_communication_consent: bool,
224) -> Result<(), sqlx::Error> {
225    sqlx::query!(
226        r#"
227UPDATE user_details
228SET email_communication_consent = $1
229WHERE user_id = $2
230"#,
231        email_communication_consent,
232        user_id,
233    )
234    .execute(conn)
235    .await?;
236    Ok(())
237}
238
239pub async fn update_user_info(
240    conn: &mut PgConnection,
241    user_id: Uuid,
242    email: &str,
243    first_name: &str,
244    last_name: &str,
245    country: &str,
246    email_communication_consent: bool,
247) -> Result<UserDetail, sqlx::Error> {
248    let updated_user = sqlx::query_as!(
249        UserDetail,
250        r#"
251UPDATE user_details
252SET email = $1,
253  first_name = $2,
254  last_name = $3,
255  country = $4,
256  email_communication_consent = $5
257WHERE user_id = $6
258RETURNING *
259"#,
260        email,
261        first_name,
262        last_name,
263        country,
264        email_communication_consent,
265        user_id,
266    )
267    .fetch_one(conn)
268    .await?;
269
270    Ok(updated_user)
271}