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