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#[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#[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 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#[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 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 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(
173 &mut conn,
174 course_module_id,
175 enabled,
176 user.id,
177 )
178 .await?;
179
180 token.authorized_ok(web::Json(true))
181}
182
183#[utoipa::path(
189 get,
190 path = "/{course_module_id}/course-module-completion",
191 operation_id = "getCourseModuleCompletion",
192 tag = "course_modules",
193 params(
194 ("course_module_id" = Uuid, Path, description = "Course module id")
195 ),
196 responses(
197 (status = 200, description = "Best course module completion for the current user", body = Option<CourseModuleCompletion>)
198 )
199)]
200#[instrument(skip(pool))]
201async fn get_best_course_module_completion_for_user(
202 course_module_id: web::Path<Uuid>,
203 pool: web::Data<PgPool>,
204 user: AuthUser,
205) -> ControllerResult<web::Json<Option<CourseModuleCompletion>>> {
206 let mut conn = pool.acquire().await?;
207 let course_module = course_modules::get_by_id(&mut conn, *course_module_id).await?;
208
209 let token = authorize(
210 &mut conn,
211 Act::View,
212 Some(user.id),
213 Res::Course(course_module.course_id),
214 )
215 .await?;
216
217 let information =
218 models::course_module_completions::get_best_completion_by_user_and_course_module_id(
219 &mut conn,
220 user.id,
221 *course_module_id,
222 )
223 .await?;
224 token.authorized_ok(web::Json(information))
225}
226
227#[utoipa::path(
231 post,
232 path = "/{course_module_id}/threshold",
233 operation_id = "createCourseModuleThreshold",
234 tag = "course_modules",
235 params(
236 ("course_module_id" = Uuid, Path, description = "Course module id")
237 ),
238 request_body = ThresholdData,
239 responses(
240 (status = 200, description = "Threshold created")
241 )
242)]
243#[instrument(skip(pool))]
244async fn insert_threshold_for_module(
245 pool: web::Data<PgPool>,
246 params: web::Path<Uuid>,
247 payload: web::Json<ThresholdData>,
248 user: AuthUser,
249) -> ControllerResult<web::Json<()>> {
250 let mut conn = pool.acquire().await?;
251
252 let course_module_id = params.into_inner();
253 let new_threshold = payload.0;
254
255 let course_module = course_modules::get_by_id(&mut conn, course_module_id).await?;
256 let token = authorize(
257 &mut conn,
258 Act::Edit,
259 Some(user.id),
260 Res::Course(course_module.course_id),
261 )
262 .await?;
263
264 let counts =
271 course_modules::get_chapter_and_exercise_counts(&mut conn, course_module_id).await?;
272 let minimum_seconds =
273 suspected_cheaters::minimum_threshold_seconds(counts.chapters, counts.exercises);
274 if new_threshold.duration_seconds < minimum_seconds {
275 let message = if minimum_seconds > 0 {
276 format!(
277 "The threshold must be at least {} hours.",
278 minimum_seconds / 3600
279 )
280 } else {
281 "The threshold cannot be negative.".to_string()
282 };
283 return Err(ControllerError::new(
284 ControllerErrorType::BadRequest,
285 message,
286 None,
287 ));
288 }
289
290 suspected_cheaters::insert_thresholds_by_module_id(
291 &mut conn,
292 course_module_id,
293 new_threshold.duration_seconds,
294 )
295 .await?;
296
297 token.authorized_ok(web::Json(()))
298}
299
300#[utoipa::path(
304 delete,
305 path = "/{course_module_id}/threshold",
306 operation_id = "deleteCourseModuleThreshold",
307 tag = "course_modules",
308 params(
309 ("course_module_id" = Uuid, Path, description = "Course module id")
310 ),
311 responses(
312 (status = 200, description = "Threshold deleted")
313 )
314)]
315#[instrument(skip(pool))]
316async fn delete_threshold_for_module(
317 pool: web::Data<PgPool>,
318 params: web::Path<Uuid>,
319 user: AuthUser,
320) -> ControllerResult<web::Json<()>> {
321 let mut conn = pool.acquire().await?;
322
323 let course_module_id = params.into_inner();
324
325 let course_module = course_modules::get_by_id(&mut conn, course_module_id).await?;
326 let token = authorize(
327 &mut conn,
328 Act::Edit,
329 Some(user.id),
330 Res::Course(course_module.course_id),
331 )
332 .await?;
333
334 suspected_cheaters::delete_threshold_for_module(&mut conn, course_module_id).await?;
335
336 token.authorized_ok(web::Json(()))
337}
338
339pub fn _add_routes(cfg: &mut ServiceConfig) {
347 cfg.route("/{course_module_id}", web::get().to(get_course_module))
348 .route(
349 "/{course_module_id}/user-completion",
350 web::get().to(get_course_module_completion_information_for_user),
351 )
352 .route(
353 "/{course_module_id}/completion-registration-link",
354 web::get().to(get_course_module_completion_registration_link),
355 )
356 .route(
357 "/{course_module_id}/set-certificate-generation/{enabled}",
358 web::post().to(enable_or_disable_certificate_generation),
359 )
360 .route(
361 "/{course_module_id}/course-module-completion",
362 web::get().to(get_best_course_module_completion_for_user),
363 )
364 .route(
365 "/{course_module_id}/threshold",
366 web::post().to(insert_threshold_for_module),
367 )
368 .route(
369 "/{course_module_id}/threshold",
370 web::delete().to(delete_threshold_for_module),
371 );
372}