Skip to main content

headless_lms_models/
user_course_settings.rs

1use crate::{course_instance_enrollments::CourseInstanceEnrollment, prelude::*};
2use utoipa::ToSchema;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
5
6pub struct UserCourseSettings {
7    pub user_id: Uuid,
8    pub course_language_group_id: Uuid,
9    pub created_at: DateTime<Utc>,
10    pub updated_at: DateTime<Utc>,
11    pub deleted_at: Option<DateTime<Utc>>,
12    pub current_course_id: Uuid,
13    pub current_course_instance_id: Uuid,
14    /// Whether the user has hidden this course from their personal "My courses" list. Does not
15    /// affect course progress.
16    pub hidden: bool,
17}
18
19/// Creates new user course settings based on the enrollment or updates an existing one.
20pub async fn upsert_user_course_settings_for_enrollment(
21    conn: &mut PgConnection,
22    course_instance_enrollment: &CourseInstanceEnrollment,
23) -> ModelResult<UserCourseSettings> {
24    use crate::{courses, user_chapter_locking_statuses};
25
26    let course = courses::get_course(conn, course_instance_enrollment.course_id).await?;
27
28    let user_course_settings = sqlx::query_as!(
29        UserCourseSettings,
30        "
31INSERT INTO user_course_settings (
32    user_id,
33    course_language_group_id,
34    current_course_id,
35    current_course_instance_id
36  )
37SELECT $1,
38  course_language_group_id,
39  $2,
40  $3
41FROM courses
42WHERE id = $2
43  AND deleted_at IS NULL ON CONFLICT (user_id, course_language_group_id) DO
44UPDATE
45SET current_course_id = $2,
46  current_course_instance_id = $3,
47  deleted_at = NULL
48RETURNING *;
49        ",
50        course_instance_enrollment.user_id,
51        course_instance_enrollment.course_id,
52        course_instance_enrollment.course_instance_id
53    )
54    .fetch_one(&mut *conn)
55    .await?;
56
57    if course.chapter_locking_enabled {
58        let _ = user_chapter_locking_statuses::get_or_init_all_for_course(
59            &mut *conn,
60            course_instance_enrollment.user_id,
61            course_instance_enrollment.course_id,
62        )
63        .await?;
64    }
65
66    Ok(user_course_settings)
67}
68
69pub async fn get_user_course_settings(
70    conn: &mut PgConnection,
71    user_id: Uuid,
72    course_language_group_id: Uuid,
73) -> ModelResult<UserCourseSettings> {
74    let user_course_settings = sqlx::query_as!(
75        UserCourseSettings,
76        "
77SELECT *
78FROM user_course_settings
79WHERE user_id = $1
80  AND course_language_group_id = $2
81  AND deleted_at IS NULL;
82        ",
83        user_id,
84        course_language_group_id
85    )
86    .fetch_one(conn)
87    .await?;
88    Ok(user_course_settings)
89}
90
91pub async fn get_user_course_settings_by_course_id(
92    conn: &mut PgConnection,
93    user_id: Uuid,
94    course_id: Uuid,
95) -> ModelResult<Option<UserCourseSettings>> {
96    let user_course_settings = sqlx::query_as!(
97        UserCourseSettings,
98        "
99SELECT ucs.*
100FROM courses c
101  JOIN user_course_settings ucs ON (
102    ucs.course_language_group_id = c.course_language_group_id
103  )
104WHERE c.id = $1
105  AND ucs.user_id = $2
106  AND c.deleted_at IS NULL
107  AND ucs.deleted_at IS NULL;
108        ",
109        course_id,
110        user_id,
111    )
112    .fetch_optional(conn)
113    .await?;
114    Ok(user_course_settings)
115}
116
117/// Sets whether the given course is hidden from the user's personal "My courses" list. The course
118/// is matched by its language group, so any language version of the course resolves to the same
119/// settings row.
120///
121/// Returns the number of settings rows updated. This is 0 when the user has no settings for the
122/// course (e.g. they only hold a role in it and never enrolled), in which case the course cannot be
123/// hidden.
124pub async fn set_hidden(
125    conn: &mut PgConnection,
126    user_id: Uuid,
127    course_id: Uuid,
128    hidden: bool,
129) -> ModelResult<u64> {
130    let res = sqlx::query!(
131        "
132UPDATE user_course_settings ucs
133SET hidden = $3
134FROM courses c
135WHERE c.id = $1
136  AND ucs.course_language_group_id = c.course_language_group_id
137  AND ucs.user_id = $2
138  AND ucs.deleted_at IS NULL
139  AND c.deleted_at IS NULL
140        ",
141        course_id,
142        user_id,
143        hidden,
144    )
145    .execute(conn)
146    .await?;
147    Ok(res.rows_affected())
148}
149
150/// Gets all of the user's course settings that have their current course id included in the provided
151/// list.
152///
153/// The distinction for current courses is stated, because multiple courses can share the same
154/// course settings if they are different language versions of each other. Course settings that may
155/// exist for inactive courses will be omited. This behavior can be desireable in some cases, and
156/// should not be changed.
157///
158/// Note that this function doesn't create any settings that are missing for the user, so the amount
159/// of results may be less than the amount of courses provided.
160pub async fn get_all_by_user_and_multiple_current_courses(
161    conn: &mut PgConnection,
162    course_ids: &[Uuid],
163    user_id: Uuid,
164) -> ModelResult<Vec<UserCourseSettings>> {
165    let res = sqlx::query_as!(
166        UserCourseSettings,
167        "
168SELECT *
169FROM user_course_settings
170WHERE current_course_id = ANY($1)
171  AND user_id = $2
172  AND deleted_at IS NULL
173        ",
174        course_ids,
175        user_id,
176    )
177    .fetch_all(conn)
178    .await?;
179    Ok(res)
180}
181
182/// Returns all non-deleted user course settings for a user.
183pub async fn get_all_by_user_id(
184    conn: &mut PgConnection,
185    user_id: Uuid,
186) -> ModelResult<Vec<UserCourseSettings>> {
187    let res = sqlx::query_as!(
188        UserCourseSettings,
189        "
190SELECT *
191FROM user_course_settings
192WHERE user_id = $1
193  AND deleted_at IS NULL
194        ",
195        user_id,
196    )
197    .fetch_all(conn)
198    .await?;
199    Ok(res)
200}
201
202pub async fn get_all_by_course_id(
203    conn: &mut PgConnection,
204    course_id: Uuid,
205) -> ModelResult<Vec<UserCourseSettings>> {
206    let res = sqlx::query_as!(
207        UserCourseSettings,
208        "
209SELECT ucs.*
210FROM courses c
211  JOIN user_course_settings ucs ON (
212    ucs.course_language_group_id = c.course_language_group_id
213  )
214WHERE c.id = $1
215  AND c.deleted_at IS NULL
216  AND ucs.deleted_at IS NULL
217        ",
218        course_id
219    )
220    .fetch_all(conn)
221    .await?;
222    Ok(res)
223}
224
225#[cfg(test)]
226mod test {
227    use super::*;
228    use crate::{
229        course_instance_enrollments::{self, NewCourseInstanceEnrollment},
230        course_instances::{self, NewCourseInstance},
231        test_helper::*,
232    };
233
234    #[tokio::test]
235    async fn upserts_user_course_settings() {
236        insert_data!(:tx, :user, :org, :course, :instance);
237
238        let enrollment = course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
239            tx.as_mut(),
240            NewCourseInstanceEnrollment {
241                course_id: course,
242                course_instance_id: instance.id,
243                user_id: user,
244            },
245        )
246        .await
247        .unwrap();
248        let settings = upsert_user_course_settings_for_enrollment(tx.as_mut(), &enrollment)
249            .await
250            .unwrap();
251        assert_eq!(settings.current_course_id, enrollment.course_id);
252        assert_eq!(
253            settings.current_course_instance_id,
254            enrollment.course_instance_id
255        );
256
257        let instance_2 = course_instances::insert(
258            tx.as_mut(),
259            PKeyPolicy::Generate,
260            NewCourseInstance {
261                course_id: course,
262                name: Some("instance-2"),
263                description: None,
264                teacher_in_charge_name: "teacher",
265                teacher_in_charge_email: "teacher@example.com",
266                support_email: None,
267                opening_time: None,
268                closing_time: None,
269            },
270        )
271        .await
272        .unwrap()
273        .id;
274        let enrollment_2 = course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
275            tx.as_mut(),
276            NewCourseInstanceEnrollment {
277                course_id: course,
278                course_instance_id: instance_2,
279                user_id: user,
280            },
281        )
282        .await
283        .unwrap();
284        let settings_2 = upsert_user_course_settings_for_enrollment(tx.as_mut(), &enrollment_2)
285            .await
286            .unwrap();
287        assert_eq!(
288            settings_2.current_course_instance_id,
289            enrollment_2.course_instance_id
290        );
291    }
292
293    #[tokio::test]
294    async fn settings_default_to_not_hidden() {
295        insert_data!(:tx, :user, :org, :course, :instance);
296        course_instance_enrollments::insert_enrollment_and_set_as_current(
297            tx.as_mut(),
298            NewCourseInstanceEnrollment {
299                course_id: course,
300                course_instance_id: instance.id,
301                user_id: user,
302            },
303        )
304        .await
305        .unwrap();
306
307        let settings = get_user_course_settings_by_course_id(tx.as_mut(), user, course)
308            .await
309            .unwrap()
310            .unwrap();
311        assert!(!settings.hidden);
312    }
313
314    #[tokio::test]
315    async fn set_hidden_toggles_the_flag() {
316        insert_data!(:tx, :user, :org, :course, :instance);
317        course_instance_enrollments::insert_enrollment_and_set_as_current(
318            tx.as_mut(),
319            NewCourseInstanceEnrollment {
320                course_id: course,
321                course_instance_id: instance.id,
322                user_id: user,
323            },
324        )
325        .await
326        .unwrap();
327
328        let updated = set_hidden(tx.as_mut(), user, course, true).await.unwrap();
329        assert_eq!(updated, 1);
330        assert!(
331            get_user_course_settings_by_course_id(tx.as_mut(), user, course)
332                .await
333                .unwrap()
334                .unwrap()
335                .hidden
336        );
337
338        set_hidden(tx.as_mut(), user, course, false).await.unwrap();
339        assert!(
340            !get_user_course_settings_by_course_id(tx.as_mut(), user, course)
341                .await
342                .unwrap()
343                .unwrap()
344                .hidden
345        );
346    }
347
348    #[tokio::test]
349    async fn set_hidden_without_settings_updates_nothing() {
350        insert_data!(:tx, :user, :org, :course);
351
352        // The user only exists, has never enrolled, so there is no settings row to hide.
353        let updated = set_hidden(tx.as_mut(), user, course, true).await.unwrap();
354        assert_eq!(updated, 0);
355    }
356}