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
85WHERE ues.course_id = $1
86  AND u.deleted_at IS NULL
87  AND ues.deleted_at IS NULL
88        ",
89        course_id
90    )
91    .fetch(conn)
92}
93
94pub async fn search_for_user_details_by_email(
95    conn: &mut PgConnection,
96    email: &str,
97) -> ModelResult<Vec<UserDetail>> {
98    let res = sqlx::query_as!(
99        UserDetail,
100        "
101SELECT *
102FROM user_details
103WHERE lower(email::text) LIKE '%' || lower($1) || '%'
104LIMIT 1000;
105",
106        email.trim(),
107    )
108    .fetch_all(conn)
109    .await?;
110    Ok(res)
111}
112
113pub async fn search_for_user_details_by_other_details(
114    conn: &mut PgConnection,
115    search: &str,
116) -> ModelResult<Vec<UserDetail>> {
117    let res = sqlx::query_as!(
118        UserDetail,
119        "
120SELECT *
121FROM user_details
122WHERE lower(search_helper::text) LIKE '%' || lower($1) || '%'
123LIMIT 1000;
124",
125        search.trim(),
126    )
127    .fetch_all(conn)
128    .await?;
129    Ok(res)
130}
131
132pub async fn search_for_user_details_fuzzy_match(
133    conn: &mut PgConnection,
134    search: &str,
135) -> ModelResult<Vec<UserDetail>> {
136    // For email domains, the fuzzy match returns too much results that have the same domain
137    // To combat this, we omit the email domain from the fuzzy match
138    let search = search.split('@').next().unwrap_or(search);
139
140    let res = sqlx::query_as!(
141        UserDetail,
142        "
143SELECT user_id,
144  created_at,
145  updated_at,
146  email,
147  first_name,
148  last_name,
149  search_helper,
150  country,
151  email_communication_consent
152FROM (
153    SELECT *,
154      LOWER($1) <<->search_helper AS dist
155    FROM user_details
156    ORDER BY dist, LENGTH(search_helper)
157    LIMIT 100
158  ) search
159WHERE dist < 0.7;
160",
161        search.trim(),
162    )
163    .fetch_all(conn)
164    .await?;
165    Ok(res)
166}
167
168/// Retrieves all users enrolled in a specific course
169pub async fn get_users_by_course_id(
170    conn: &mut PgConnection,
171    course_id: Uuid,
172) -> ModelResult<Vec<UserDetail>> {
173    let res = sqlx::query_as!(
174        UserDetail,
175        r#"
176SELECT d.user_id,
177  d.created_at,
178  d.updated_at,
179  d.email,
180  d.first_name,
181  d.last_name,
182  d.search_helper,
183  d.country,
184  d.email_communication_consent
185FROM course_instance_enrollments e
186  JOIN user_details d ON e.user_id = d.user_id
187WHERE e.course_id = $1
188  AND e.deleted_at IS NULL
189        "#,
190        course_id
191    )
192    .fetch_all(conn)
193    .await?;
194
195    Ok(res)
196}
197
198/// Retrieves user details for a list of user IDs
199pub async fn get_user_details_by_user_ids(
200    conn: &mut PgConnection,
201    user_ids: &[Uuid],
202) -> ModelResult<Vec<UserDetail>> {
203    let res = sqlx::query_as!(
204        UserDetail,
205        r#"
206SELECT *
207FROM user_details
208WHERE user_id = ANY($1::uuid[])
209        "#,
210        user_ids
211    )
212    .fetch_all(conn)
213    .await?;
214
215    Ok(res)
216}
217
218/// Retrieves user details for a list of user IDs, but only for users who are enrolled in the specified course
219pub async fn get_user_details_by_user_ids_for_course(
220    conn: &mut PgConnection,
221    user_ids: &[Uuid],
222    course_id: Uuid,
223) -> ModelResult<Vec<UserDetail>> {
224    let res = sqlx::query_as!(
225        UserDetail,
226        r#"
227SELECT ud.*
228FROM user_details ud
229JOIN user_course_settings ucs ON ud.user_id = ucs.user_id
230WHERE ud.user_id = ANY($1::uuid[])
231  AND ucs.current_course_id = $2
232  AND ucs.deleted_at IS NULL
233        "#,
234        user_ids,
235        course_id
236    )
237    .fetch_all(conn)
238    .await?;
239
240    Ok(res)
241}
242
243/// Retrieves user details for a single user ID, but only if the user is enrolled in the specified course
244pub async fn get_user_details_by_user_id_for_course(
245    conn: &mut PgConnection,
246    user_id: Uuid,
247    course_id: Uuid,
248) -> ModelResult<UserDetail> {
249    let res = sqlx::query_as!(
250        UserDetail,
251        r#"
252SELECT ud.*
253FROM user_details ud
254JOIN user_course_settings ucs ON ud.user_id = ucs.user_id
255WHERE ud.user_id = $1
256  AND ucs.current_course_id = $2
257  AND ucs.deleted_at IS NULL
258        "#,
259        user_id,
260        course_id
261    )
262    .fetch_one(conn)
263    .await?;
264
265    Ok(res)
266}
267
268pub async fn update_user_country(
269    conn: &mut PgConnection,
270    user_id: Uuid,
271    country: &str,
272) -> Result<(), sqlx::Error> {
273    sqlx::query!(
274        r#"
275UPDATE user_details
276SET country = $1
277WHERE user_id = $2
278"#,
279        country,
280        user_id,
281    )
282    .execute(conn)
283    .await?;
284    Ok(())
285}
286
287pub async fn update_user_email_communication_consent(
288    conn: &mut PgConnection,
289    user_id: Uuid,
290    email_communication_consent: bool,
291) -> Result<(), sqlx::Error> {
292    sqlx::query!(
293        r#"
294UPDATE user_details
295SET email_communication_consent = $1
296WHERE user_id = $2
297"#,
298        email_communication_consent,
299        user_id,
300    )
301    .execute(conn)
302    .await?;
303    Ok(())
304}
305
306pub async fn update_user_info(
307    conn: &mut PgConnection,
308    user_id: Uuid,
309    email: &str,
310    first_name: &str,
311    last_name: &str,
312    country: &str,
313    email_communication_consent: bool,
314) -> Result<UserDetail, sqlx::Error> {
315    let updated_user = sqlx::query_as!(
316        UserDetail,
317        r#"
318UPDATE user_details
319SET email = $1,
320  first_name = $2,
321  last_name = $3,
322  country = $4,
323  email_communication_consent = $5
324WHERE user_id = $6
325RETURNING *
326"#,
327        email,
328        first_name,
329        last_name,
330        country,
331        email_communication_consent,
332        user_id,
333    )
334    .fetch_one(conn)
335    .await?;
336
337    Ok(updated_user)
338}