Skip to main content

headless_lms_server/controllers/main_frontend/
course_modules.rs

1use headless_lms_models::course_module_completions::CourseModuleCompletion;
2use headless_lms_models::suspected_cheaters::ThresholdData;
3use models::{
4    course_modules::{self, CourseModule},
5    library::progressing::{CompletionRegistrationLink, UserCompletionInformation},
6    suspected_cheaters,
7};
8use utoipa::OpenApi;
9
10use crate::prelude::*;
11
12#[derive(OpenApi)]
13#[openapi(paths(
14    get_course_module,
15    get_course_module_completion_information_for_user,
16    get_course_module_completion_registration_link,
17    enable_or_disable_certificate_generation,
18    get_best_course_module_completion_for_user,
19    insert_threshold_for_module,
20    delete_threshold_for_module
21))]
22pub(crate) struct MainFrontendCourseModulesApiDoc;
23
24/**
25GET `/api/v0/main-frontend/course-modules/{course_module_id}`
26
27Returns information about the course module.
28*/
29#[utoipa::path(
30    get,
31    path = "/{course_module_id}",
32    operation_id = "getCourseModule",
33    tag = "course_modules",
34    params(
35        ("course_module_id" = Uuid, Path, description = "Course module id")
36    ),
37    responses(
38        (status = 200, description = "Course module", body = CourseModule)
39    )
40)]
41#[instrument(skip(pool))]
42async fn get_course_module(
43    course_module_id: web::Path<Uuid>,
44    pool: web::Data<PgPool>,
45    user: AuthUser,
46) -> ControllerResult<web::Json<CourseModule>> {
47    let mut conn = pool.acquire().await?;
48    let course_module = course_modules::get_by_id(&mut conn, *course_module_id).await?;
49    let token = authorize(
50        &mut conn,
51        Act::View,
52        Some(user.id),
53        Res::Course(course_module.course_id),
54    )
55    .await?;
56    token.authorized_ok(web::Json(course_module))
57}
58
59/**
60GET `/api/v0/main-frontend/course-modules/{course_module_id}/user-completion`
61
62Gets active users's completion for the course, if it exists.
63*/
64#[utoipa::path(
65    get,
66    path = "/{course_module_id}/user-completion",
67    operation_id = "getCourseModuleUserCompletion",
68    tag = "course_modules",
69    params(
70        ("course_module_id" = Uuid, Path, description = "Course module id")
71    ),
72    responses(
73        (status = 200, description = "User completion information", body = UserCompletionInformation)
74    )
75)]
76#[instrument(skip(pool))]
77async fn get_course_module_completion_information_for_user(
78    course_module_id: web::Path<Uuid>,
79    pool: web::Data<PgPool>,
80    user: AuthUser,
81) -> ControllerResult<web::Json<UserCompletionInformation>> {
82    let mut conn = pool.acquire().await?;
83    let course_module = course_modules::get_by_id(&mut conn, *course_module_id).await?;
84    // Proper request validation is based on whether a completion exists for the user or not.
85    let token = authorize(
86        &mut conn,
87        Act::View,
88        Some(user.id),
89        Res::Course(course_module.course_id),
90    )
91    .await?;
92    let information = models::library::progressing::get_user_completion_information(
93        &mut conn,
94        user.id,
95        &course_module,
96    )
97    .await?;
98    token.authorized_ok(web::Json(information))
99}
100
101/**
102GET `/api/v0/main-frontend/course-modules/{course_slug}/completion-registration-link`
103*/
104#[utoipa::path(
105    get,
106    path = "/{course_module_id}/completion-registration-link",
107    operation_id = "getCourseModuleCompletionRegistrationLink",
108    tag = "course_modules",
109    params(
110        ("course_module_id" = Uuid, Path, description = "Course module id")
111    ),
112    responses(
113        (status = 200, description = "Completion registration link", body = CompletionRegistrationLink)
114    )
115)]
116#[instrument(skip(pool))]
117async fn get_course_module_completion_registration_link(
118    course_module_id: web::Path<Uuid>,
119    pool: web::Data<PgPool>,
120    user: AuthUser,
121) -> ControllerResult<web::Json<CompletionRegistrationLink>> {
122    let mut conn = pool.acquire().await?;
123    let course_module = course_modules::get_by_id(&mut conn, *course_module_id).await?;
124    // Proper request validation is based on whether a completion exists for the user or not.
125    let token = authorize(
126        &mut conn,
127        Act::View,
128        Some(user.id),
129        Res::Course(course_module.course_id),
130    )
131    .await?;
132    let completion_registration_link =
133        models::library::progressing::get_completion_registration_link_and_save_attempt(
134            &mut conn,
135            user.id,
136            &course_module,
137        )
138        .await?;
139    token.authorized_ok(web::Json(completion_registration_link))
140}
141
142#[utoipa::path(
143    post,
144    path = "/{course_module_id}/set-certificate-generation/{enabled}",
145    operation_id = "setCourseModuleCertificateGeneration",
146    tag = "course_modules",
147    params(
148        ("course_module_id" = Uuid, Path, description = "Course module id"),
149        ("enabled" = bool, Path, description = "Whether certificate generation should be enabled")
150    ),
151    responses(
152        (status = 200, description = "Certificate generation updated", body = bool)
153    )
154)]
155async fn enable_or_disable_certificate_generation(
156    params: web::Path<(Uuid, bool)>,
157    pool: web::Data<PgPool>,
158    user: AuthUser,
159) -> ControllerResult<web::Json<bool>> {
160    let mut conn = pool.acquire().await?;
161    let (course_module_id, enabled) = params.into_inner();
162
163    let course_module = course_modules::get_by_id(&mut conn, course_module_id).await?;
164    // Proper request validation is based on whether a completion exists for the user or not.
165    let token = authorize(
166        &mut conn,
167        Act::Edit,
168        Some(user.id),
169        Res::Course(course_module.course_id),
170    )
171    .await?;
172    models::course_modules::update_certification_enabled(&mut conn, course_module_id, enabled)
173        .await?;
174
175    token.authorized_ok(web::Json(true))
176}
177
178/**
179GET `/api/v0/main-frontend/course-modules/{course_module_id}/course-module-completion`
180
181Gets users's best completion for the course.
182*/
183#[utoipa::path(
184    get,
185    path = "/{course_module_id}/course-module-completion",
186    operation_id = "getCourseModuleCompletion",
187    tag = "course_modules",
188    params(
189        ("course_module_id" = Uuid, Path, description = "Course module id")
190    ),
191    responses(
192        (status = 200, description = "Best course module completion for the current user", body = Option<CourseModuleCompletion>)
193    )
194)]
195#[instrument(skip(pool))]
196async fn get_best_course_module_completion_for_user(
197    course_module_id: web::Path<Uuid>,
198    pool: web::Data<PgPool>,
199    user: AuthUser,
200) -> ControllerResult<web::Json<Option<CourseModuleCompletion>>> {
201    let mut conn = pool.acquire().await?;
202    let course_module = course_modules::get_by_id(&mut conn, *course_module_id).await?;
203
204    let token = authorize(
205        &mut conn,
206        Act::View,
207        Some(user.id),
208        Res::Course(course_module.course_id),
209    )
210    .await?;
211
212    let information =
213        models::course_module_completions::get_best_completion_by_user_and_course_module_id(
214            &mut conn,
215            user.id,
216            *course_module_id,
217        )
218        .await?;
219    token.authorized_ok(web::Json(information))
220}
221
222/**
223 POST /api/v0/main-frontend/course-modules/${course_module_id}/threshold - post threshold for a specific course module.
224*/
225#[utoipa::path(
226    post,
227    path = "/{course_module_id}/threshold",
228    operation_id = "createCourseModuleThreshold",
229    tag = "course_modules",
230    params(
231        ("course_module_id" = Uuid, Path, description = "Course module id")
232    ),
233    request_body = ThresholdData,
234    responses(
235        (status = 200, description = "Threshold created")
236    )
237)]
238#[instrument(skip(pool))]
239async fn insert_threshold_for_module(
240    pool: web::Data<PgPool>,
241    params: web::Path<Uuid>,
242    payload: web::Json<ThresholdData>,
243    user: AuthUser,
244) -> ControllerResult<web::Json<()>> {
245    let mut conn = pool.acquire().await?;
246
247    let course_module_id = params.into_inner();
248    let new_threshold = payload.0;
249
250    let course_module = course_modules::get_by_id(&mut conn, course_module_id).await?;
251    let token = authorize(
252        &mut conn,
253        Act::Edit,
254        Some(user.id),
255        Res::Course(course_module.course_id),
256    )
257    .await?;
258
259    suspected_cheaters::insert_thresholds_by_module_id(
260        &mut conn,
261        course_module_id,
262        new_threshold.duration_seconds,
263    )
264    .await?;
265
266    token.authorized_ok(web::Json(()))
267}
268
269/**
270 DELETE /api/v0/main-frontend/course-modules/${course_module_id}/threshold - delete threshold for a specific course module.
271*/
272#[utoipa::path(
273    delete,
274    path = "/{course_module_id}/threshold",
275    operation_id = "deleteCourseModuleThreshold",
276    tag = "course_modules",
277    params(
278        ("course_module_id" = Uuid, Path, description = "Course module id")
279    ),
280    responses(
281        (status = 200, description = "Threshold deleted")
282    )
283)]
284#[instrument(skip(pool))]
285async fn delete_threshold_for_module(
286    pool: web::Data<PgPool>,
287    params: web::Path<Uuid>,
288    user: AuthUser,
289) -> ControllerResult<web::Json<()>> {
290    let mut conn = pool.acquire().await?;
291
292    let course_module_id = params.into_inner();
293
294    let course_module = course_modules::get_by_id(&mut conn, course_module_id).await?;
295    let token = authorize(
296        &mut conn,
297        Act::Edit,
298        Some(user.id),
299        Res::Course(course_module.course_id),
300    )
301    .await?;
302
303    suspected_cheaters::delete_threshold_for_module(&mut conn, course_module_id).await?;
304
305    token.authorized_ok(web::Json(()))
306}
307
308/**
309Add a route for each controller in this module.
310
311The name starts with an underline in order to appear before other functions in the module documentation.
312
313We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
314*/
315pub fn _add_routes(cfg: &mut ServiceConfig) {
316    cfg.route("/{course_module_id}", web::get().to(get_course_module))
317        .route(
318            "/{course_module_id}/user-completion",
319            web::get().to(get_course_module_completion_information_for_user),
320        )
321        .route(
322            "/{course_module_id}/completion-registration-link",
323            web::get().to(get_course_module_completion_registration_link),
324        )
325        .route(
326            "/{course_module_id}/set-certificate-generation/{enabled}",
327            web::post().to(enable_or_disable_certificate_generation),
328        )
329        .route(
330            "/{course_module_id}/course-module-completion",
331            web::get().to(get_best_course_module_completion_for_user),
332        )
333        .route(
334            "/{course_module_id}/threshold",
335            web::post().to(insert_threshold_for_module),
336        )
337        .route(
338            "/{course_module_id}/threshold",
339            web::delete().to(delete_threshold_for_module),
340        );
341}