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};
13
14use crate::{domain::authorization::skip_authorize, prelude::*};
15
16/**
17 GET /api/v0/course-material/course-instance/:course_intance_id/progress - returns user progress information.
18*/
19#[instrument(skip(pool))]
20async fn get_user_progress_for_course_instance(
21    user: AuthUser,
22    course_instance_id: web::Path<Uuid>,
23    pool: web::Data<PgPool>,
24) -> ControllerResult<web::Json<Vec<UserCourseProgress>>> {
25    let mut conn = pool.acquire().await?;
26    let course_instance =
27        models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
28    let user_course_progress = models::user_exercise_states::get_user_course_progress(
29        &mut conn,
30        course_instance.course_id,
31        user.id,
32        false,
33    )
34    .await?;
35    let token = skip_authorize();
36    token.authorized_ok(web::Json(user_course_progress))
37}
38
39/**
40GET `/api/v0/course-material/course-instance/:course_instance_id/chapters/:chapter_id/progress - Returns user progress for chapter in course instance.
41*/
42#[instrument(skip(pool))]
43async fn get_user_progress_for_course_instance_chapter(
44    user: AuthUser,
45    params: web::Path<(Uuid, Uuid)>,
46    pool: web::Data<PgPool>,
47) -> ControllerResult<web::Json<UserCourseInstanceChapterProgress>> {
48    let mut conn = pool.acquire().await?;
49    let (course_instance_id, chapter_id) = params.into_inner();
50    let user_course_instance_chapter_progress =
51        models::chapters::get_user_course_instance_chapter_progress(
52            &mut conn,
53            course_instance_id,
54            chapter_id,
55            user.id,
56        )
57        .await?;
58    let token = skip_authorize();
59    token.authorized_ok(web::Json(user_course_instance_chapter_progress))
60}
61
62/**
63GET /api/v0/course-material/course-instance/:course_instance_id/chapters/:chapter_id/exercises/progress - Returns user progress for an exercise in given course instance.
64*/
65#[instrument(skip(pool))]
66async fn get_user_progress_for_course_instance_chapter_exercises(
67    user: AuthUser,
68    params: web::Path<(Uuid, Uuid)>,
69    pool: web::Data<PgPool>,
70) -> ControllerResult<web::Json<Vec<UserCourseChapterExerciseProgress>>> {
71    let mut conn = pool.acquire().await?;
72    let (course_instance_id, chapter_id) = params.into_inner();
73    let chapter_exercises =
74        models::exercises::get_exercises_by_chapter_id(&mut conn, chapter_id).await?;
75    let exercise_ids: Vec<Uuid> = chapter_exercises.into_iter().map(|e| e.id).collect();
76    let course_instance =
77        models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
78    let user_course_instance_exercise_progress =
79        models::user_exercise_states::get_user_course_chapter_exercises_progress(
80            &mut conn,
81            course_instance.course_id,
82            &exercise_ids,
83            user.id,
84        )
85        .await?;
86    let rounded_score_given_instances: Vec<UserCourseChapterExerciseProgress> =
87        user_course_instance_exercise_progress
88            .into_iter()
89            .map(|i| UserCourseChapterExerciseProgress {
90                score_given: option_f32_to_f32_two_decimals_with_none_as_zero(i.score_given),
91                exercise_id: i.exercise_id,
92            })
93            .collect();
94    let token = skip_authorize();
95    token.authorized_ok(web::Json(rounded_score_given_instances))
96}
97
98/**
99GET `/api/v0/course-material/course-instance/{course_instance_id}/module-completions`
100 */
101#[instrument(skip(pool))]
102async fn get_module_completions_for_course_instance(
103    user: AuthUser,
104    course_instance_id: web::Path<Uuid>,
105    pool: web::Data<PgPool>,
106) -> ControllerResult<web::Json<Vec<UserModuleCompletionStatus>>> {
107    let mut conn = pool.acquire().await?;
108    let token = skip_authorize();
109
110    let course_instance =
111        models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
112    let mut module_completion_statuses =
113        models::library::progressing::get_user_module_completion_statuses_for_course(
114            &mut conn,
115            user.id,
116            course_instance.course_id,
117        )
118        .await?;
119    // Override individual completions in modules with insufficient prerequisites
120    module_completion_statuses.iter_mut().for_each(|module| {
121        if !module.prerequisite_modules_completed {
122            module.completed = false;
123            module.certificate_configuration_id = None;
124        }
125    });
126    token.authorized_ok(web::Json(module_completion_statuses))
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[cfg_attr(feature = "ts_rs", derive(TS))]
131pub struct SaveCourseSettingsPayload {
132    pub background_question_answers: Vec<NewCourseBackgroundQuestionAnswer>,
133}
134
135/**
136POST /api/v0/course-material/course-instance/:course_instance_id/save-course-settings - enrolls user to the course instance and save background questions.
137*/
138#[instrument(skip(pool))]
139async fn save_course_settings(
140    pool: web::Data<PgPool>,
141    course_instance_id: web::Path<Uuid>,
142    payload: web::Json<SaveCourseSettingsPayload>,
143    user: AuthUser,
144) -> ControllerResult<web::Json<CourseInstanceEnrollment>> {
145    let mut conn = pool.acquire().await?;
146
147    let enrollment = models::library::course_instances::enroll(
148        &mut conn,
149        user.id,
150        *course_instance_id,
151        payload.background_question_answers.as_slice(),
152    )
153    .await?;
154    let token = skip_authorize();
155    token.authorized_ok(web::Json(enrollment))
156}
157
158/**
159GET /course-instances/:id/course-module-completions/:user_id - Returns a list of all course module completions for a given user for this course instance.
160*/
161#[instrument(skip(pool))]
162
163async fn get_all_get_all_course_module_completions_for_user_by_course_instance_id(
164    params: web::Path<(Uuid, Uuid)>,
165    pool: web::Data<PgPool>,
166    user: AuthUser,
167) -> ControllerResult<web::Json<Vec<CourseModuleCompletion>>> {
168    let (course_instance_id, user_id) = params.into_inner();
169    let mut conn = pool.acquire().await?;
170    let token = authorize(
171        &mut conn,
172        Act::ViewUserProgressOrDetails,
173        Some(user.id),
174        Res::CourseInstance(course_instance_id),
175    )
176    .await?;
177
178    let course_instance =
179        models::course_instances::get_course_instance(&mut conn, course_instance_id).await?;
180
181    let res = models::course_module_completions::get_all_by_course_id_and_user_id(
182        &mut conn,
183        course_instance.course_id,
184        user_id,
185    )
186    .await?;
187
188    token.authorized_ok(web::Json(res))
189}
190
191/**
192GET /api/v0/course-material/course-instance/:course_instance_id/background-questions-and-answers - Gets background questions and answers for an course instance.
193*/
194#[instrument(skip(pool))]
195async fn get_background_questions_and_answers(
196    pool: web::Data<PgPool>,
197    course_instance_id: web::Path<Uuid>,
198    user: AuthUser,
199) -> ControllerResult<web::Json<CourseBackgroundQuestionsAndAnswers>> {
200    let mut conn = pool.acquire().await?;
201
202    let instance =
203        models::course_instances::get_course_instance(&mut conn, *course_instance_id).await?;
204    let res = models::course_background_questions::get_background_questions_and_answers(
205        &mut conn, &instance, user.id,
206    )
207    .await?;
208    let token = skip_authorize();
209    token.authorized_ok(web::Json(res))
210}
211
212pub fn _add_routes(cfg: &mut ServiceConfig) {
213    cfg.route(
214        "/{course_instance_id}/save-course-settings",
215        web::post().to(save_course_settings),
216    )
217    .route(
218        "/{course_instance_id}/progress",
219        web::get().to(get_user_progress_for_course_instance),
220    )
221    .route(
222        "/{course_instance_id}/chapters/{chapter_id}/exercises/progress",
223        web::get().to(get_user_progress_for_course_instance_chapter_exercises),
224    )
225    .route(
226        "/{course_instance_id}/chapters/{chapter_id}/progress",
227        web::get().to(get_user_progress_for_course_instance_chapter),
228    )
229    .route(
230        "/{course_instance_id}/module-completions",
231        web::get().to(get_module_completions_for_course_instance),
232    )
233    .route(
234        "/{course_instance_id}/course-module-completions/{user_id}",
235        web::get().to(get_all_get_all_course_module_completions_for_user_by_course_instance_id),
236    )
237    .route(
238        "/{course_instance_id}/background-questions-and-answers",
239        web::get().to(get_background_questions_and_answers),
240    );
241}