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 pub hidden: bool,
17}
18
19pub 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
117pub 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
150pub 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
182pub 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 let updated = set_hidden(tx.as_mut(), user, course, true).await.unwrap();
354 assert_eq!(updated, 0);
355 }
356}