Skip to main content

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