headless_lms_server/controllers/course_material/
course_instances.rs

1//! Controllers for requests starting with `/api/v0/course-material/course-instances`.
2
3use 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/**
30 GET /api/v0/course-material/course-instance/:course_intance_id/progress - returns user progress information.
31*/
32#[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/**
65GET `/api/v0/course-material/course-instance/:course_instance_id/chapters/:chapter_id/progress - Returns user progress for chapter in course instance.
66*/
67#[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/**
101GET /api/v0/course-material/course-instance/:course_instance_id/chapters/:chapter_id/exercises/progress - Returns user progress for an exercise in given course instance.
102*/
103#[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/**
150GET `/api/v0/course-material/course-instance/{course_instance_id}/module-completions`
151 */
152#[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    // Override individual completions in modules with insufficient prerequisites
183    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/**
199POST /api/v0/course-material/course-instance/:course_instance_id/save-course-settings - enrolls user to the course instance and save background questions.
200*/
201#[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/**
235GET /course-instances/:id/course-module-completions/:user_id - Returns a list of all course module completions for a given user for this course instance.
236*/
237#[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/**
281GET /api/v0/course-material/course-instance/:course_instance_id/background-questions-and-answers - Gets background questions and answers for an course instance.
282*/
283#[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}