1use headless_lms_utils::numbers::option_f32_to_f32_two_decimals_with_none_as_zero;
4use models::{
5 chapters::UserCourseInstanceChapterProgress,
6 course_background_question_answers::NewCourseBackgroundQuestionAnswer,
7 course_background_questions::CourseBackgroundQuestionsAndAnswers,
8 course_instance_enrollments::CourseInstanceEnrollment,
9 course_module_completions::CourseModuleCompletion,
10 library::progressing::UserModuleCompletionStatus,
11 user_exercise_states::{UserCourseChapterExerciseProgress, UserCourseProgress},
12};
13use utoipa::{OpenApi, ToSchema};
14
15use crate::{
16 domain::authorization::{authorize_access_to_course_material, skip_authorize},
17 prelude::*,
18};
19
20#[derive(OpenApi)]
21#[openapi(paths(
22 get_user_progress_for_course_instance,
23 get_user_progress_for_course_instance_chapter,
24 get_user_progress_for_course_instance_chapter_exercises,
25 get_module_completions_for_course_instance,
26 save_course_settings,
27 get_all_get_all_course_module_completions_for_user_by_course_instance_id,
28 get_background_questions_and_answers
29))]
30pub(crate) struct CourseMaterialCourseInstancesApiDoc;
31
32#[utoipa::path(
36 get,
37 path = "/{course_instance_id}/progress",
38 operation_id = "getCourseMaterialUserCourseProgress",
39 tag = "course-material-course-instances",
40 params(
41 ("course_instance_id" = Uuid, Path, description = "Course instance id")
42 ),
43 responses(
44 (status = 200, description = "User course progress", body = Vec<UserCourseProgress>)
45 )
46)]
47#[instrument(skip(pool))]
48async fn get_user_progress_for_course_instance(
49 user: AuthUser,
50 course_instance_id: web::Path<Uuid>,
51 pool: web::Data<PgPool>,
52) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
53 let mut conn = pool.acquire().await?;
54 let course_instance =
55 models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
56 let user_course_progress = models::user_exercise_states::get_user_course_progress(
57 &mut conn,
58 course_instance.course_id,
59 user.id,
60 false,
61 )
62 .await?;
63 let token = skip_authorize();
64 token.authorized_ok(web::Json(user_course_progress))
65}
66
67#[utoipa::path(
71 get,
72 path = "/{course_instance_id}/chapters/{chapter_id}/progress",
73 operation_id = "getCourseMaterialChapterProgress",
74 tag = "course-material-course-instances",
75 params(
76 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
77 ("chapter_id" = Uuid, Path, description = "Chapter id")
78 ),
79 responses(
80 (status = 200, description = "Course instance chapter progress", body = UserCourseInstanceChapterProgress)
81 )
82)]
83#[instrument(skip(pool))]
84async fn get_user_progress_for_course_instance_chapter(
85 user: AuthUser,
86 params: web::Path<(Uuid, Uuid)>,
87 pool: web::Data<PgPool>,
88) -> ControllerResult<web::Json<UserCourseInstanceChapterProgress>> {
89 let mut conn = pool.acquire().await?;
90 let (course_instance_id, chapter_id) = params.into_inner();
91 let course_instance =
92 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
93 let chapter = models::chapters::get_chapter(&mut conn, chapter_id).await?;
94 if chapter.course_id != course_instance.course_id {
95 return Err(controller_err!(
96 Forbidden,
97 "Chapter does not belong to the requested course instance".to_string()
98 ));
99 }
100 let token =
101 authorize_access_to_course_material(&mut conn, Some(user.id), course_instance.course_id)
102 .await?;
103 let user_course_instance_chapter_progress =
104 models::chapters::get_user_course_instance_chapter_progress(
105 &mut conn,
106 course_instance_id,
107 chapter_id,
108 user.id,
109 )
110 .await?;
111 token.authorized_ok(web::Json(user_course_instance_chapter_progress))
112}
113
114#[utoipa::path(
118 get,
119 path = "/{course_instance_id}/chapters/{chapter_id}/exercises/progress",
120 operation_id = "getCourseMaterialChapterExerciseProgress",
121 tag = "course-material-course-instances",
122 params(
123 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
124 ("chapter_id" = Uuid, Path, description = "Chapter id")
125 ),
126 responses(
127 (status = 200, description = "Course instance chapter exercise progress", body = Vec<UserCourseChapterExerciseProgress>)
128 )
129)]
130#[instrument(skip(pool))]
131async fn get_user_progress_for_course_instance_chapter_exercises(
132 user: AuthUser,
133 params: web::Path<(Uuid, Uuid)>,
134 pool: web::Data<PgPool>,
135) -> ControllerResult<web::Json<Vec<UserCourseChapterExerciseProgress>>> {
136 let mut conn = pool.acquire().await?;
137 let (course_instance_id, chapter_id) = params.into_inner();
138 let course_instance =
139 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
140 let chapter = models::chapters::get_chapter(&mut conn, chapter_id).await?;
141 if chapter.course_id != course_instance.course_id {
142 return Err(controller_err!(
143 Forbidden,
144 "Chapter does not belong to the requested course instance".to_string()
145 ));
146 }
147 let token =
148 authorize_access_to_course_material(&mut conn, Some(user.id), course_instance.course_id)
149 .await?;
150 let chapter_exercises =
151 models::exercises::get_exercises_by_chapter_id(&mut conn, chapter_id).await?;
152 let exercise_ids: Vec<Uuid> = chapter_exercises.into_iter().map(|e| e.id).collect();
153 let user_course_instance_exercise_progress =
154 models::user_exercise_states::get_user_course_chapter_exercises_progress(
155 &mut conn,
156 course_instance.course_id,
157 &exercise_ids,
158 user.id,
159 )
160 .await?;
161 let rounded_score_given_instances: Vec<UserCourseChapterExerciseProgress> =
162 user_course_instance_exercise_progress
163 .into_iter()
164 .map(|i| UserCourseChapterExerciseProgress {
165 score_given: option_f32_to_f32_two_decimals_with_none_as_zero(i.score_given),
166 exercise_id: i.exercise_id,
167 })
168 .collect();
169 token.authorized_ok(web::Json(rounded_score_given_instances))
170}
171
172#[utoipa::path(
176 get,
177 path = "/{course_instance_id}/module-completions",
178 operation_id = "getCourseMaterialUserModuleCompletions",
179 tag = "course-material-course-instances",
180 params(
181 ("course_instance_id" = Uuid, Path, description = "Course instance id")
182 ),
183 responses(
184 (status = 200, description = "User module completion statuses", body = Vec<UserModuleCompletionStatus>)
185 )
186)]
187#[instrument(skip(pool))]
188async fn get_module_completions_for_course_instance(
189 user: AuthUser,
190 course_instance_id: web::Path<Uuid>,
191 pool: web::Data<PgPool>,
192) -> ControllerResult<web::Json<Vec<UserModuleCompletionStatus>>> {
193 let mut conn = pool.acquire().await?;
194 let token = skip_authorize();
195
196 let course_instance =
197 models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
198 let mut module_completion_statuses =
199 models::library::progressing::get_user_module_completion_statuses_for_course(
200 &mut conn,
201 user.id,
202 course_instance.course_id,
203 )
204 .await?;
205 module_completion_statuses.iter_mut().for_each(|module| {
207 if !module.prerequisite_modules_completed {
208 module.completed = false;
209 module.certificate_configuration_id = None;
210 }
211 });
212 token.authorized_ok(web::Json(module_completion_statuses))
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
216
217pub struct SaveCourseSettingsPayload {
218 pub background_question_answers: Vec<NewCourseBackgroundQuestionAnswer>,
219}
220
221#[utoipa::path(
225 post,
226 path = "/{course_instance_id}/save-course-settings",
227 operation_id = "saveCourseMaterialCourseSettings",
228 tag = "course-material-course-instances",
229 params(
230 ("course_instance_id" = Uuid, Path, description = "Course instance id")
231 ),
232 request_body = SaveCourseSettingsPayload,
233 responses(
234 (status = 200, description = "Course instance enrollment", body = CourseInstanceEnrollment)
235 )
236)]
237#[instrument(skip(pool))]
238async fn save_course_settings(
239 pool: web::Data<PgPool>,
240 course_instance_id: web::Path<Uuid>,
241 payload: web::Json<SaveCourseSettingsPayload>,
242 user: AuthUser,
243) -> ControllerResult<web::Json<CourseInstanceEnrollment>> {
244 let mut conn = pool.acquire().await?;
245
246 let enrollment = models::library::course_instances::enroll(
247 &mut conn,
248 user.id,
249 *course_instance_id,
250 payload.background_question_answers.as_slice(),
251 )
252 .await?;
253 let token = skip_authorize();
254 token.authorized_ok(web::Json(enrollment))
255}
256
257#[utoipa::path(
261 get,
262 path = "/{course_instance_id}/course-module-completions/{user_id}",
263 operation_id = "getCourseMaterialCourseModuleCompletionsForUser",
264 tag = "course-material-course-instances",
265 params(
266 ("course_instance_id" = Uuid, Path, description = "Course instance id"),
267 ("user_id" = Uuid, Path, description = "User id")
268 ),
269 responses(
270 (status = 200, description = "Course module completions for user", body = Vec<CourseModuleCompletion>)
271 )
272)]
273#[instrument(skip(pool))]
274
275async fn get_all_get_all_course_module_completions_for_user_by_course_instance_id(
276 params: web::Path<(Uuid, Uuid)>,
277 pool: web::Data<PgPool>,
278 user: AuthUser,
279) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
280 let (course_instance_id, user_id) = params.into_inner();
281 let mut conn = pool.acquire().await?;
282 let token = authorize(
283 &mut conn,
284 Act::ViewUserProgressOrDetails,
285 Some(user.id),
286 Res::CourseInstance(course_instance_id),
287 )
288 .await?;
289
290 let course_instance =
291 models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
292
293 let res = models::course_module_completions::get_all_by_course_id_and_user_id(
294 &mut conn,
295 course_instance.course_id,
296 user_id,
297 )
298 .await?;
299
300 token.authorized_ok(web::Json(res))
301}
302
303#[utoipa::path(
307 get,
308 path = "/{course_instance_id}/background-questions-and-answers",
309 operation_id = "getCourseMaterialBackgroundQuestionsAndAnswers",
310 tag = "course-material-course-instances",
311 params(
312 ("course_instance_id" = Uuid, Path, description = "Course instance id")
313 ),
314 responses(
315 (status = 200, description = "Background questions and answers", body = CourseBackgroundQuestionsAndAnswers)
316 )
317)]
318#[instrument(skip(pool))]
319async fn get_background_questions_and_answers(
320 pool: web::Data<PgPool>,
321 course_instance_id: web::Path<Uuid>,
322 user: AuthUser,
323) -> ControllerResult<web::Json<CourseBackgroundQuestionsAndAnswers>> {
324 let mut conn = pool.acquire().await?;
325
326 let instance =
327 models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
328 let res = models::course_background_questions::get_background_questions_and_answers(
329 &mut conn, &instance, user.id,
330 )
331 .await?;
332 let token = skip_authorize();
333 token.authorized_ok(web::Json(res))
334}
335
336pub fn _add_routes(cfg: &mut ServiceConfig) {
337 cfg.route(
338 "/{course_instance_id}/save-course-settings",
339 web::post().to(save_course_settings),
340 )
341 .route(
342 "/{course_instance_id}/progress",
343 web::get().to(get_user_progress_for_course_instance),
344 )
345 .route(
346 "/{course_instance_id}/chapters/{chapter_id}/exercises/progress",
347 web::get().to(get_user_progress_for_course_instance_chapter_exercises),
348 )
349 .route(
350 "/{course_instance_id}/chapters/{chapter_id}/progress",
351 web::get().to(get_user_progress_for_course_instance_chapter),
352 )
353 .route(
354 "/{course_instance_id}/module-completions",
355 web::get().to(get_module_completions_for_course_instance),
356 )
357 .route(
358 "/{course_instance_id}/course-module-completions/{user_id}",
359 web::get().to(get_all_get_all_course_module_completions_for_user_by_course_instance_id),
360 )
361 .route(
362 "/{course_instance_id}/background-questions-and-answers",
363 web::get().to(get_background_questions_and_answers),
364 );
365}