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