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