1use crate::{
2 course_instances::CourseInstance, course_module_completions::CourseModuleCompletion,
3 courses::Course, prelude::*, user_course_settings::UserCourseSettings,
4};
5use utoipa::ToSchema;
6
7#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
8
9pub struct CourseInstanceEnrollment {
10 pub user_id: Uuid,
11 pub course_id: Uuid,
12 pub course_instance_id: Uuid,
13 pub created_at: DateTime<Utc>,
14 pub updated_at: DateTime<Utc>,
15 pub deleted_at: Option<DateTime<Utc>>,
16}
17
18#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
19
20pub struct CourseInstanceEnrollmentsInfo {
21 pub course_instance_enrollments: Vec<CourseInstanceEnrollment>,
22 pub course_instances: Vec<CourseInstance>,
23 pub courses: Vec<Course>,
24 pub user_course_settings: Vec<UserCourseSettings>,
25 pub course_module_completions: Vec<CourseModuleCompletion>,
26}
27
28#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
29
30pub struct CourseEnrollmentInfo {
31 pub course_id: Uuid,
32 pub course: Course,
33 pub course_instances: Vec<CourseInstance>,
34 pub user_course_settings: Option<UserCourseSettings>,
35 pub course_module_completions: Vec<CourseModuleCompletion>,
36 pub first_enrolled_at: DateTime<Utc>,
37 pub is_current: bool,
38}
39
40#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
41
42pub struct CourseEnrollmentsInfo {
43 pub course_enrollments: Vec<CourseEnrollmentInfo>,
44}
45
46pub async fn insert(
47 conn: &mut PgConnection,
48 user_id: Uuid,
49 course_id: Uuid,
50 course_instance_id: Uuid,
51) -> ModelResult<()> {
52 sqlx::query!(
53 "
54INSERT INTO course_instance_enrollments (user_id, course_id, course_instance_id)
55VALUES ($1, $2, $3)
56",
57 user_id,
58 course_id,
59 course_instance_id,
60 )
61 .execute(conn)
62 .await?;
63 Ok(())
64}
65
66#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
67pub struct NewCourseInstanceEnrollment {
68 pub user_id: Uuid,
69 pub course_id: Uuid,
70 pub course_instance_id: Uuid,
71}
72
73pub async fn insert_enrollment_if_it_doesnt_exist(
79 conn: &mut PgConnection,
80 enrollment: NewCourseInstanceEnrollment,
81) -> ModelResult<CourseInstanceEnrollment> {
82 let enrollment = sqlx::query_as!(
83 CourseInstanceEnrollment,
84 "
85INSERT INTO course_instance_enrollments (user_id, course_id, course_instance_id)
86VALUES ($1, $2, $3)
87ON CONFLICT (user_id, course_id, course_instance_id)
88DO UPDATE SET deleted_at = NULL
89RETURNING *;
90",
91 enrollment.user_id,
92 enrollment.course_id,
93 enrollment.course_instance_id,
94 )
95 .fetch_one(conn)
96 .await?;
97 Ok(enrollment)
98}
99
100pub async fn insert_enrollment_and_set_as_current(
101 conn: &mut PgConnection,
102 new_enrollment: NewCourseInstanceEnrollment,
103) -> ModelResult<CourseInstanceEnrollment> {
104 let mut tx = conn.begin().await?;
105
106 let enrollment = insert_enrollment_if_it_doesnt_exist(&mut tx, new_enrollment).await?;
107 crate::user_course_settings::upsert_user_course_settings_for_enrollment(&mut tx, &enrollment)
108 .await?;
109 tx.commit().await?;
110
111 Ok(enrollment)
112}
113
114pub async fn get_by_user_and_course_instance_id(
115 conn: &mut PgConnection,
116 user_id: Uuid,
117 course_instance_id: Uuid,
118) -> ModelResult<CourseInstanceEnrollment> {
119 let res = sqlx::query_as!(
120 CourseInstanceEnrollment,
121 "
122SELECT *
123FROM course_instance_enrollments
124WHERE user_id = $1
125 AND course_instance_id = $2
126 AND deleted_at IS NULL
127 ",
128 user_id,
129 course_instance_id
130 )
131 .fetch_one(conn)
132 .await?;
133 Ok(res)
134}
135
136pub async fn get_by_user_id(
137 conn: &mut PgConnection,
138 user_id: Uuid,
139) -> ModelResult<Vec<CourseInstanceEnrollment>> {
140 let res = sqlx::query_as!(
141 CourseInstanceEnrollment,
142 "
143SELECT *
144FROM course_instance_enrollments
145WHERE user_id = $1
146 AND deleted_at IS NULL
147 ",
148 user_id
149 )
150 .fetch_all(conn)
151 .await?;
152 Ok(res)
153}
154
155pub async fn get_by_user_id_and_course_ids(
156 conn: &mut PgConnection,
157 user_id: Uuid,
158 course_ids: &[Uuid],
159) -> ModelResult<Vec<CourseInstanceEnrollment>> {
160 let res = sqlx::query_as!(
161 CourseInstanceEnrollment,
162 "
163SELECT *
164FROM course_instance_enrollments
165WHERE user_id = $1
166 AND course_id = ANY($2)
167 AND deleted_at IS NULL
168 ",
169 user_id,
170 course_ids
171 )
172 .fetch_all(conn)
173 .await?;
174 Ok(res)
175}
176
177pub async fn get_course_instance_enrollments_info_for_user(
178 conn: &mut PgConnection,
179 user_id: Uuid,
180) -> ModelResult<CourseInstanceEnrollmentsInfo> {
181 let course_instance_enrollments = get_by_user_id(conn, user_id).await?;
182
183 let course_instance_ids: Vec<Uuid> = course_instance_enrollments
184 .iter()
185 .map(|e| e.course_instance_id)
186 .collect();
187
188 let course_instances = crate::course_instances::get_by_ids(conn, &course_instance_ids).await?;
189
190 let course_ids: Vec<Uuid> = course_instances.iter().map(|e| e.course_id).collect();
191
192 let courses = crate::courses::get_by_ids(conn, &course_ids).await?;
193
194 let course_module_completions =
195 crate::course_module_completions::get_all_by_user_id(conn, user_id).await?;
196
197 let user_course_settings =
199 crate::user_course_settings::get_all_by_user_and_multiple_current_courses(
200 conn,
201 &course_ids,
202 user_id,
203 )
204 .await?;
205
206 Ok(CourseInstanceEnrollmentsInfo {
207 course_instance_enrollments,
208 course_instances,
209 courses,
210 user_course_settings,
211 course_module_completions,
212 })
213}
214
215struct CourseEnrollmentRow {
216 course_id: Uuid,
217 first_enrolled_at: Option<DateTime<Utc>>,
218}
219
220pub async fn get_course_enrollments_info_for_user(
222 conn: &mut PgConnection,
223 user_id: Uuid,
224) -> ModelResult<CourseEnrollmentsInfo> {
225 let rows = sqlx::query_as!(
226 CourseEnrollmentRow,
227 "
228SELECT course_id, MIN(created_at) AS first_enrolled_at
229FROM course_instance_enrollments
230WHERE user_id = $1 AND deleted_at IS NULL
231GROUP BY course_id
232ORDER BY first_enrolled_at
233 ",
234 user_id
235 )
236 .fetch_all(&mut *conn)
237 .await?;
238
239 let course_ids: Vec<Uuid> = rows.iter().map(|r| r.course_id).collect();
240
241 let course_instance_enrollments = get_by_user_id(&mut *conn, user_id).await?;
242 let all_course_module_completions =
243 crate::course_module_completions::get_all_by_user_id(conn, user_id).await?;
244 let user_course_settings =
245 crate::user_course_settings::get_all_by_user_id(conn, user_id).await?;
246 let courses = crate::courses::get_by_ids(conn, &course_ids).await?;
247 let course_instance_ids: Vec<Uuid> = course_instance_enrollments
248 .iter()
249 .map(|e| e.course_instance_id)
250 .collect();
251 let all_course_instances =
252 crate::course_instances::get_by_ids(conn, &course_instance_ids).await?;
253
254 let mut course_enrollments = Vec::with_capacity(rows.len());
255 for row in rows {
256 let course = courses
257 .iter()
258 .find(|c| c.id == row.course_id)
259 .cloned()
260 .ok_or_else(|| {
261 crate::ModelError::new(
262 crate::error::ModelErrorType::NotFound,
263 "Course not found for enrollment".to_string(),
264 None,
265 )
266 })?;
267 let course_instances: Vec<_> = all_course_instances
268 .iter()
269 .filter(|ci| ci.course_id == row.course_id)
270 .cloned()
271 .collect();
272 let user_course_settings_for_course = user_course_settings
273 .iter()
274 .find(|ucs| ucs.course_language_group_id == course.course_language_group_id)
275 .cloned();
276 let course_module_completions = all_course_module_completions
277 .iter()
278 .filter(|cmc| cmc.course_id == row.course_id)
279 .cloned()
280 .collect();
281 let is_current = user_course_settings_for_course
282 .as_ref()
283 .map(|ucs| ucs.current_course_id == row.course_id)
284 .unwrap_or(false);
285
286 let first_enrolled_at = row.first_enrolled_at.ok_or_else(|| {
287 crate::ModelError::new(
288 crate::error::ModelErrorType::Generic,
289 "first_enrolled_at missing for grouped enrollment row".to_string(),
290 None,
291 )
292 })?;
293
294 course_enrollments.push(CourseEnrollmentInfo {
295 course_id: row.course_id,
296 course,
297 course_instances,
298 user_course_settings: user_course_settings_for_course,
299 course_module_completions,
300 first_enrolled_at,
301 is_current,
302 });
303 }
304
305 Ok(CourseEnrollmentsInfo { course_enrollments })
306}