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::{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 _ = user_chapter_locking_statuses::get_or_init_all_for_course(
55            &mut *conn,
56            course_instance_enrollment.user_id,
57            course_instance_enrollment.course_id,
58        )
59        .await?;
60    }
61
62    Ok(user_course_settings)
63}
64
65pub async fn get_user_course_settings(
66    conn: &mut PgConnection,
67    user_id: Uuid,
68    course_language_group_id: Uuid,
69) -> ModelResult<UserCourseSettings> {
70    let user_course_settings = sqlx::query_as!(
71        UserCourseSettings,
72        "
73SELECT *
74FROM user_course_settings
75WHERE user_id = $1
76  AND course_language_group_id = $2
77  AND deleted_at IS NULL;
78        ",
79        user_id,
80        course_language_group_id
81    )
82    .fetch_one(conn)
83    .await?;
84    Ok(user_course_settings)
85}
86
87pub async fn get_user_course_settings_by_course_id(
88    conn: &mut PgConnection,
89    user_id: Uuid,
90    course_id: Uuid,
91) -> ModelResult<Option<UserCourseSettings>> {
92    let user_course_settings = sqlx::query_as!(
93        UserCourseSettings,
94        "
95SELECT ucs.*
96FROM courses c
97  JOIN user_course_settings ucs ON (
98    ucs.course_language_group_id = c.course_language_group_id
99  )
100WHERE c.id = $1
101  AND ucs.user_id = $2
102  AND c.deleted_at IS NULL
103  AND ucs.deleted_at IS NULL;
104        ",
105        course_id,
106        user_id,
107    )
108    .fetch_optional(conn)
109    .await?;
110    Ok(user_course_settings)
111}
112
113/// Gets all of the user's course settings that have their current course id included in the provided
114/// list.
115///
116/// The distinction for current courses is stated, because multiple courses can share the same
117/// course settings if they are different language versions of each other. Course settings that may
118/// exist for inactive courses will be omited. This behavior can be desireable in some cases, and
119/// should not be changed.
120///
121/// Note that this function doesn't create any settings that are missing for the user, so the amount
122/// of results may be less than the amount of courses provided.
123pub async fn get_all_by_user_and_multiple_current_courses(
124    conn: &mut PgConnection,
125    course_ids: &[Uuid],
126    user_id: Uuid,
127) -> ModelResult<Vec<UserCourseSettings>> {
128    let res = sqlx::query_as!(
129        UserCourseSettings,
130        "
131SELECT *
132FROM user_course_settings
133WHERE current_course_id = ANY($1)
134  AND user_id = $2
135  AND deleted_at IS NULL
136        ",
137        course_ids,
138        user_id,
139    )
140    .fetch_all(conn)
141    .await?;
142    Ok(res)
143}
144
145/// Returns all non-deleted user course settings for a user.
146pub async fn get_all_by_user_id(
147    conn: &mut PgConnection,
148    user_id: Uuid,
149) -> ModelResult<Vec<UserCourseSettings>> {
150    let res = sqlx::query_as!(
151        UserCourseSettings,
152        "
153SELECT *
154FROM user_course_settings
155WHERE user_id = $1
156  AND deleted_at IS NULL
157        ",
158        user_id,
159    )
160    .fetch_all(conn)
161    .await?;
162    Ok(res)
163}
164
165pub async fn get_all_by_course_id(
166    conn: &mut PgConnection,
167    course_id: Uuid,
168) -> ModelResult<Vec<UserCourseSettings>> {
169    let res = sqlx::query_as!(
170        UserCourseSettings,
171        "
172SELECT ucs.*
173FROM courses c
174  JOIN user_course_settings ucs ON (
175    ucs.course_language_group_id = c.course_language_group_id
176  )
177WHERE c.id = $1
178  AND c.deleted_at IS NULL
179  AND ucs.deleted_at IS NULL
180        ",
181        course_id
182    )
183    .fetch_all(conn)
184    .await?;
185    Ok(res)
186}
187
188#[cfg(test)]
189mod test {
190    use super::*;
191    use crate::{
192        course_instance_enrollments::{self, NewCourseInstanceEnrollment},
193        course_instances::{self, NewCourseInstance},
194        test_helper::*,
195    };
196
197    #[tokio::test]
198    async fn upserts_user_course_settings() {
199        insert_data!(:tx, :user, :org, :course, :instance);
200
201        let enrollment = course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
202            tx.as_mut(),
203            NewCourseInstanceEnrollment {
204                course_id: course,
205                course_instance_id: instance.id,
206                user_id: user,
207            },
208        )
209        .await
210        .unwrap();
211        let settings = upsert_user_course_settings_for_enrollment(tx.as_mut(), &enrollment)
212            .await
213            .unwrap();
214        assert_eq!(settings.current_course_id, enrollment.course_id);
215        assert_eq!(
216            settings.current_course_instance_id,
217            enrollment.course_instance_id
218        );
219
220        let instance_2 = course_instances::insert(
221            tx.as_mut(),
222            PKeyPolicy::Generate,
223            NewCourseInstance {
224                course_id: course,
225                name: Some("instance-2"),
226                description: None,
227                teacher_in_charge_name: "teacher",
228                teacher_in_charge_email: "teacher@example.com",
229                support_email: None,
230                opening_time: None,
231                closing_time: None,
232            },
233        )
234        .await
235        .unwrap()
236        .id;
237        let enrollment_2 = course_instance_enrollments::insert_enrollment_if_it_doesnt_exist(
238            tx.as_mut(),
239            NewCourseInstanceEnrollment {
240                course_id: course,
241                course_instance_id: instance_2,
242                user_id: user,
243            },
244        )
245        .await
246        .unwrap();
247        let settings_2 = upsert_user_course_settings_for_enrollment(tx.as_mut(), &enrollment_2)
248            .await
249            .unwrap();
250        assert_eq!(
251            settings_2.current_course_instance_id,
252            enrollment_2.course_instance_id
253        );
254    }
255}