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