headless_lms_models/
user_course_settings.rs

1use crate::{course_instance_enrollments::CourseInstanceEnrollment, prelude::*};
2
3#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
4#[cfg_attr(feature = "ts_rs", derive(TS))]
5pub struct UserCourseSettings {
6    pub user_id: Uuid,
7    pub course_language_group_id: Uuid,
8    pub created_at: DateTime<Utc>,
9    pub updated_at: DateTime<Utc>,
10    pub deleted_at: Option<DateTime<Utc>>,
11    pub current_course_id: Uuid,
12    pub current_course_instance_id: Uuid,
13}
14
15/// Creates new user course settings based on the enrollment or updates an existing one.
16pub async fn upsert_user_course_settings_for_enrollment(
17    conn: &mut PgConnection,
18    course_instance_enrollment: &CourseInstanceEnrollment,
19) -> ModelResult<UserCourseSettings> {
20    use crate::{chapters, courses, user_chapter_locking_statuses};
21
22    let course = courses::get_course(conn, course_instance_enrollment.course_id).await?;
23
24    let user_course_settings = sqlx::query_as!(
25        UserCourseSettings,
26        "
27INSERT INTO user_course_settings (
28    user_id,
29    course_language_group_id,
30    current_course_id,
31    current_course_instance_id
32  )
33SELECT $1,
34  course_language_group_id,
35  $2,
36  $3
37FROM courses
38WHERE id = $2
39  AND deleted_at IS NULL ON CONFLICT (user_id, course_language_group_id) DO
40UPDATE
41SET current_course_id = $2,
42  current_course_instance_id = $3,
43  deleted_at = NULL
44RETURNING *;
45        ",
46        course_instance_enrollment.user_id,
47        course_instance_enrollment.course_id,
48        course_instance_enrollment.course_instance_id
49    )
50    .fetch_one(&mut *conn)
51    .await?;
52
53    if course.chapter_locking_enabled {
54        let existing_statuses = user_chapter_locking_statuses::get_by_user_and_course(
55            &mut *conn,
56            course_instance_enrollment.user_id,
57            course_instance_enrollment.course_id,
58        )
59        .await?;
60
61        let has_unlocked_or_completed = existing_statuses.iter().any(|s| {
62            matches!(
63                s.status,
64                user_chapter_locking_statuses::ChapterLockingStatus::Unlocked
65                    | user_chapter_locking_statuses::ChapterLockingStatus::CompletedAndLocked
66            )
67        });
68
69        if !has_unlocked_or_completed {
70            chapters::unlock_first_chapters_for_user(
71                &mut *conn,
72                course_instance_enrollment.user_id,
73                course_instance_enrollment.course_id,
74            )
75            .await?;
76        }
77    }
78
79    Ok(user_course_settings)
80}
81
82pub async fn get_user_course_settings(
83    conn: &mut PgConnection,
84    user_id: Uuid,
85    course_language_group_id: Uuid,
86) -> ModelResult<UserCourseSettings> {
87    let user_course_settings = sqlx::query_as!(
88        UserCourseSettings,
89        "
90SELECT *
91FROM user_course_settings
92WHERE user_id = $1
93  AND course_language_group_id = $2
94  AND deleted_at IS NULL;
95        ",
96        user_id,
97        course_language_group_id
98    )
99    .fetch_one(conn)
100    .await?;
101    Ok(user_course_settings)
102}
103
104pub async fn get_user_course_settings_by_course_id(
105    conn: &mut PgConnection,
106    user_id: Uuid,
107    course_id: Uuid,
108) -> ModelResult<Option<UserCourseSettings>> {
109    let user_course_settings = sqlx::query_as!(
110        UserCourseSettings,
111        "
112SELECT ucs.*
113FROM courses c
114  JOIN user_course_settings ucs ON (
115    ucs.course_language_group_id = c.course_language_group_id
116  )
117WHERE c.id = $1
118  AND ucs.user_id = $2
119  AND c.deleted_at IS NULL
120  AND ucs.deleted_at IS NULL;
121        ",
122        course_id,
123        user_id,
124    )
125    .fetch_optional(conn)
126    .await?;
127    Ok(user_course_settings)
128}
129
130/// Gets all of the user's course settings that have their current course id included in the provided
131/// list.
132///
133/// The distinction for current courses is stated, because multiple courses can share the same
134/// course settings if they are different language versions of each other. Course settings that may
135/// exist for inactive courses will be omited. This behavior can be desireable in some cases, and
136/// should not be changed.
137///
138/// Note that this function doesn't create any settings that are missing for the user, so the amount
139/// of results may be less than the amount of courses provided.
140pub async fn get_all_by_user_and_multiple_current_courses(
141    conn: &mut PgConnection,
142    course_ids: &[Uuid],
143    user_id: Uuid,
144) -> ModelResult<Vec<UserCourseSettings>> {
145    let res = sqlx::query_as!(
146        UserCourseSettings,
147        "
148SELECT *
149FROM user_course_settings
150WHERE current_course_id = ANY($1)
151  AND user_id = $2
152  AND deleted_at IS NULL
153        ",
154        course_ids,
155        user_id,
156    )
157    .fetch_all(conn)
158    .await?;
159    Ok(res)
160}
161
162/// Returns all non-deleted user course settings for a user.
163pub async fn get_all_by_user_id(
164    conn: &mut PgConnection,
165    user_id: Uuid,
166) -> ModelResult<Vec<UserCourseSettings>> {
167    let res = sqlx::query_as!(
168        UserCourseSettings,
169        "
170SELECT *
171FROM user_course_settings
172WHERE user_id = $1
173  AND deleted_at IS NULL
174        ",
175        user_id,
176    )
177    .fetch_all(conn)
178    .await?;
179    Ok(res)
180}
181
182pub async fn get_all_by_course_id(
183    conn: &mut PgConnection,
184    course_id: Uuid,
185) -> ModelResult<Vec<UserCourseSettings>> {
186    let res = sqlx::query_as!(
187        UserCourseSettings,
188        "
189SELECT ucs.*
190FROM courses c
191  JOIN user_course_settings ucs ON (
192    ucs.course_language_group_id = c.course_language_group_id
193  )
194WHERE c.id = $1
195  AND c.deleted_at IS NULL
196  AND ucs.deleted_at IS NULL
197        ",
198        course_id
199    )
200    .fetch_all(conn)
201    .await?;
202    Ok(res)
203}
204
205#[cfg(test)]
206mod test {
207    use super::*;
208    use crate::{
209        course_instance_enrollments::{self, NewCourseInstanceEnrollment},
210        course_instances::{self, NewCourseInstance},
211        test_helper::*,
212    };
213
214    #[tokio::test]
215    async fn upserts_user_course_settings() {
216        insert_data!(:tx, :user, :org, :course, :instance);
217
218        let enrollment = course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
219            tx.as_mut(),
220            NewCourseInstanceEnrollment {
221                course_id: course,
222                course_instance_id: instance.id,
223                user_id: user,
224            },
225        )
226        .await
227        .unwrap();
228        let settings = upsert_user_course_settings_for_enrollment(tx.as_mut(), &enrollment)
229            .await
230            .unwrap();
231        assert_eq!(settings.current_course_id, enrollment.course_id);
232        assert_eq!(
233            settings.current_course_instance_id,
234            enrollment.course_instance_id
235        );
236
237        let instance_2 = course_instances::insert(
238            tx.as_mut(),
239            PKeyPolicy::Generate,
240            NewCourseInstance {
241                course_id: course,
242                name: Some("instance-2"),
243                description: None,
244                teacher_in_charge_name: "teacher",
245                teacher_in_charge_email: "teacher@example.com",
246                support_email: None,
247                opening_time: None,
248                closing_time: None,
249            },
250        )
251        .await
252        .unwrap()
253        .id;
254        let enrollment_2 = course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
255            tx.as_mut(),
256            NewCourseInstanceEnrollment {
257                course_id: course,
258                course_instance_id: instance_2,
259                user_id: user,
260            },
261        )
262        .await
263        .unwrap();
264        let settings_2 = upsert_user_course_settings_for_enrollment(tx.as_mut(), &enrollment_2)
265            .await
266            .unwrap();
267        assert_eq!(
268            settings_2.current_course_instance_id,
269            enrollment_2.course_instance_id
270        );
271    }
272}