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