1use crate::prelude::*;
3
4use headless_lms_models::chapter_lock_action_logs;
5use headless_lms_models::chapters::CourseUserInfo;
6use headless_lms_models::library::students_view::{
7 CertificateGridRow, CompletionGridRow, ProgressOverview,
8};
9use headless_lms_models::user_chapter_locking_statuses::{
10 ChapterLockingStatus, UserChapterLockingStatus,
11};
12use serde::Deserialize;
13use utoipa::OpenApi;
14use utoipa::ToSchema;
15
16#[derive(OpenApi)]
17#[openapi(paths(
18 get_progress,
19 get_user_chapter_locking_statuses,
20 get_course_users,
21 get_completions,
22 get_certificates,
23 teacher_lock_student_chapter,
24 teacher_unlock_student_chapter,
25 teacher_set_student_chapter_status
26))]
27pub(crate) struct MainFrontendCourseStudentsApiDoc;
28
29#[derive(Debug, Deserialize, ToSchema)]
30struct ChapterLockStatusActionPayload {
31 status: ChapterLockingStatus,
32}
33
34#[utoipa::path(
36 get,
37 path = "/progress",
38 operation_id = "getCourseStudentsProgress",
39 tag = "course-students",
40 params(
41 ("course_id" = Uuid, Path, description = "Course id")
42 ),
43 responses(
44 (status = 200, description = "Course student progress overview", body = ProgressOverview)
45 )
46)]
47#[instrument(skip(pool))]
48async fn get_progress(
49 course_id: web::Path<Uuid>,
50 pool: web::Data<PgPool>,
51 user: AuthUser,
52) -> ControllerResult<web::Json<ProgressOverview>> {
53 let mut conn = pool.acquire().await?;
54 let token = authorize(
55 &mut conn,
56 Act::Teach,
57 Some(user.id),
58 Res::Course(*course_id),
59 )
60 .await?;
61 let res =
62 headless_lms_models::library::students_view::get_progress(&mut conn, *course_id).await?;
63
64 token.authorized_ok(web::Json(res))
65}
66
67#[utoipa::path(
69 get,
70 path = "/{user_id}/chapter-locking-statuses",
71 operation_id = "getCourseStudentChapterLockingStatuses",
72 tag = "course-students",
73 params(
74 ("course_id" = Uuid, Path, description = "Course id"),
75 ("user_id" = Uuid, Path, description = "Target student id")
76 ),
77 responses(
78 (status = 200, description = "Student chapter locking statuses", body = [UserChapterLockingStatus])
79 )
80)]
81#[instrument(skip(pool))]
82async fn get_user_chapter_locking_statuses(
83 path: web::Path<(Uuid, Uuid)>,
84 pool: web::Data<PgPool>,
85 user: AuthUser,
86) -> ControllerResult<web::Json<Vec<UserChapterLockingStatus>>> {
87 let (course_id, target_user_id) = path.into_inner();
88 let mut conn = pool.acquire().await?;
89 let token = authorize(
90 &mut conn,
91 Act::ViewUserProgressOrDetails,
92 Some(user.id),
93 Res::Course(course_id),
94 )
95 .await?;
96
97 models::user_details::get_user_details_by_user_id_for_course(
98 &mut conn,
99 target_user_id,
100 course_id,
101 )
102 .await?;
103
104 let statuses = models::user_chapter_locking_statuses::get_or_init_all_for_course(
105 &mut conn,
106 target_user_id,
107 course_id,
108 )
109 .await?;
110
111 token.authorized_ok(web::Json(statuses))
112}
113
114#[utoipa::path(
116 get,
117 path = "/users",
118 operation_id = "getCourseStudentsUsers",
119 tag = "course-students",
120 params(
121 ("course_id" = Uuid, Path, description = "Course id")
122 ),
123 responses(
124 (status = 200, description = "Course users", body = [CourseUserInfo])
125 )
126)]
127#[instrument(skip(pool))]
128async fn get_course_users(
129 course_id: web::Path<Uuid>,
130 pool: web::Data<PgPool>,
131 user: AuthUser,
132) -> ControllerResult<web::Json<Vec<CourseUserInfo>>> {
133 let mut conn = pool.acquire().await?;
134 let token = authorize(
135 &mut conn,
136 Act::Teach,
137 Some(user.id),
138 Res::Course(*course_id),
139 )
140 .await?;
141 let res = headless_lms_models::library::students_view::get_course_users(&mut conn, *course_id)
142 .await?;
143
144 token.authorized_ok(web::Json(res))
145}
146
147#[utoipa::path(
149 get,
150 path = "/completions",
151 operation_id = "getCourseStudentsCompletions",
152 tag = "course-students",
153 params(
154 ("course_id" = Uuid, Path, description = "Course id")
155 ),
156 responses(
157 (status = 200, description = "Course completions", body = [CompletionGridRow])
158 )
159)]
160#[instrument(skip(pool))]
161async fn get_completions(
162 course_id: web::Path<Uuid>,
163 pool: web::Data<PgPool>,
164 user: AuthUser,
165) -> ControllerResult<web::Json<Vec<CompletionGridRow>>> {
166 let mut conn = pool.acquire().await?;
167 let token = authorize(
168 &mut conn,
169 Act::Teach,
170 Some(user.id),
171 Res::Course(*course_id),
172 )
173 .await?;
174 let rows = headless_lms_models::library::students_view::get_completions_grid_by_course_id(
175 &mut conn, *course_id,
176 )
177 .await?;
178
179 token.authorized_ok(web::Json(rows))
180}
181
182#[utoipa::path(
184 get,
185 path = "/certificates",
186 operation_id = "getCourseStudentsCertificates",
187 tag = "course-students",
188 params(
189 ("course_id" = Uuid, Path, description = "Course id")
190 ),
191 responses(
192 (status = 200, description = "Course certificates", body = [CertificateGridRow])
193 )
194)]
195#[instrument(skip(pool))]
196async fn get_certificates(
197 course_id: web::Path<Uuid>,
198 pool: web::Data<PgPool>,
199 user: AuthUser,
200) -> ControllerResult<web::Json<Vec<CertificateGridRow>>> {
201 let mut conn = pool.acquire().await?;
202 let token = authorize(
203 &mut conn,
204 Act::Teach,
205 Some(user.id),
206 Res::Course(*course_id),
207 )
208 .await?;
209 let rows = headless_lms_models::library::students_view::get_certificates_grid_by_course_id(
210 &mut conn, *course_id,
211 )
212 .await?;
213
214 token.authorized_ok(web::Json(rows))
215}
216
217#[utoipa::path(
219 post,
220 path = "/{user_id}/chapters/{chapter_id}/lock",
221 operation_id = "teacherLockStudentChapter",
222 tag = "course-students",
223 params(
224 ("course_id" = Uuid, Path, description = "Course id"),
225 ("user_id" = Uuid, Path, description = "Target student id"),
226 ("chapter_id" = Uuid, Path, description = "Chapter id")
227 ),
228 responses(
229 (status = 200, description = "Updated chapter locking status", body = UserChapterLockingStatus)
230 )
231)]
232#[instrument(skip(pool))]
233async fn teacher_lock_student_chapter(
234 path: web::Path<(Uuid, Uuid, Uuid)>,
235 pool: web::Data<PgPool>,
236 user: AuthUser,
237) -> ControllerResult<web::Json<UserChapterLockingStatus>> {
238 let (course_id, target_user_id, chapter_id) = path.into_inner();
239 let mut conn = pool.acquire().await?;
240 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
241
242 let chapter = models::chapters::get_chapter(&mut conn, chapter_id).await?;
243 if chapter.course_id != course_id {
244 return Err(ControllerError::new(
245 ControllerErrorType::BadRequest,
246 "Chapter does not belong to the course.".to_string(),
247 None,
248 ));
249 }
250 let course = models::courses::get_course(&mut conn, course_id).await?;
251 if !course.chapter_locking_enabled {
252 return Err(ControllerError::new(
253 ControllerErrorType::BadRequest,
254 "Chapter locking is not enabled for this course.".to_string(),
255 None,
256 ));
257 }
258
259 models::user_details::get_user_details_by_user_id_for_course(
260 &mut conn,
261 target_user_id,
262 course_id,
263 )
264 .await?;
265
266 let mut tx = conn.begin().await?;
267 let status = models::user_chapter_locking_statuses::complete_and_lock_chapter(
268 &mut tx,
269 target_user_id,
270 chapter_id,
271 course_id,
272 )
273 .await?;
274 chapter_lock_action_logs::insert(
275 &mut tx,
276 Some(user.id),
277 target_user_id,
278 course_id,
279 chapter_id,
280 status.status,
281 )
282 .await?;
283 tx.commit().await?;
284
285 token.authorized_ok(web::Json(status))
286}
287
288#[utoipa::path(
290 post,
291 path = "/{user_id}/chapters/{chapter_id}/unlock",
292 operation_id = "teacherUnlockStudentChapter",
293 tag = "course-students",
294 params(
295 ("course_id" = Uuid, Path, description = "Course id"),
296 ("user_id" = Uuid, Path, description = "Target student id"),
297 ("chapter_id" = Uuid, Path, description = "Chapter id")
298 ),
299 responses(
300 (status = 200, description = "Updated chapter locking status", body = UserChapterLockingStatus)
301 )
302)]
303#[instrument(skip(pool))]
304async fn teacher_unlock_student_chapter(
305 path: web::Path<(Uuid, Uuid, Uuid)>,
306 pool: web::Data<PgPool>,
307 user: AuthUser,
308) -> ControllerResult<web::Json<UserChapterLockingStatus>> {
309 let (course_id, target_user_id, chapter_id) = path.into_inner();
310 let mut conn = pool.acquire().await?;
311 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
312
313 let chapter = models::chapters::get_chapter(&mut conn, chapter_id).await?;
314 if chapter.course_id != course_id {
315 return Err(ControllerError::new(
316 ControllerErrorType::BadRequest,
317 "Chapter does not belong to the course.".to_string(),
318 None,
319 ));
320 }
321 let course = models::courses::get_course(&mut conn, course_id).await?;
322 if !course.chapter_locking_enabled {
323 return Err(ControllerError::new(
324 ControllerErrorType::BadRequest,
325 "Chapter locking is not enabled for this course.".to_string(),
326 None,
327 ));
328 }
329
330 models::user_details::get_user_details_by_user_id_for_course(
331 &mut conn,
332 target_user_id,
333 course_id,
334 )
335 .await?;
336
337 let mut tx = conn.begin().await?;
338 let status = models::user_chapter_locking_statuses::unlock_chapter(
339 &mut tx,
340 target_user_id,
341 chapter_id,
342 course_id,
343 )
344 .await?;
345 chapter_lock_action_logs::insert(
346 &mut tx,
347 Some(user.id),
348 target_user_id,
349 course_id,
350 chapter_id,
351 status.status,
352 )
353 .await?;
354 tx.commit().await?;
355
356 token.authorized_ok(web::Json(status))
357}
358
359#[utoipa::path(
361 post,
362 path = "/{user_id}/chapters/{chapter_id}/status",
363 operation_id = "teacherSetStudentChapterStatus",
364 tag = "course-students",
365 params(
366 ("course_id" = Uuid, Path, description = "Course id"),
367 ("user_id" = Uuid, Path, description = "Target student id"),
368 ("chapter_id" = Uuid, Path, description = "Chapter id")
369 ),
370 request_body = ChapterLockStatusActionPayload,
371 responses(
372 (status = 200, description = "Updated chapter locking status", body = UserChapterLockingStatus)
373 )
374)]
375#[instrument(skip(pool))]
376async fn teacher_set_student_chapter_status(
377 path: web::Path<(Uuid, Uuid, Uuid)>,
378 payload: web::Json<ChapterLockStatusActionPayload>,
379 pool: web::Data<PgPool>,
380 user: AuthUser,
381) -> ControllerResult<web::Json<UserChapterLockingStatus>> {
382 let (course_id, target_user_id, chapter_id) = path.into_inner();
383 let mut conn = pool.acquire().await?;
384 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
385
386 let chapter = models::chapters::get_chapter(&mut conn, chapter_id).await?;
387 if chapter.course_id != course_id {
388 return Err(ControllerError::new(
389 ControllerErrorType::BadRequest,
390 "Chapter does not belong to the course.".to_string(),
391 None,
392 ));
393 }
394 let course = models::courses::get_course(&mut conn, course_id).await?;
395 if !course.chapter_locking_enabled {
396 return Err(ControllerError::new(
397 ControllerErrorType::BadRequest,
398 "Chapter locking is not enabled for this course.".to_string(),
399 None,
400 ));
401 }
402
403 models::user_details::get_user_details_by_user_id_for_course(
404 &mut conn,
405 target_user_id,
406 course_id,
407 )
408 .await?;
409
410 let mut tx = conn.begin().await?;
411 let status = models::user_chapter_locking_statuses::set_chapter_status(
412 &mut tx,
413 target_user_id,
414 chapter_id,
415 course_id,
416 payload.status,
417 )
418 .await?;
419 chapter_lock_action_logs::insert(
420 &mut tx,
421 Some(user.id),
422 target_user_id,
423 course_id,
424 chapter_id,
425 status.status,
426 )
427 .await?;
428 tx.commit().await?;
429
430 token.authorized_ok(web::Json(status))
431}
432
433pub fn _add_routes(cfg: &mut web::ServiceConfig) {
434 cfg.route("/progress", web::get().to(get_progress));
435 cfg.route(
436 "/{user_id}/chapter-locking-statuses",
437 web::get().to(get_user_chapter_locking_statuses),
438 );
439 cfg.route("/users", web::get().to(get_course_users));
440 cfg.route("/completions", web::get().to(get_completions));
441 cfg.route("/certificates", web::get().to(get_certificates));
442 cfg.route(
443 "/{user_id}/chapters/{chapter_id}/lock",
444 web::post().to(teacher_lock_student_chapter),
445 );
446 cfg.route(
447 "/{user_id}/chapters/{chapter_id}/unlock",
448 web::post().to(teacher_unlock_student_chapter),
449 );
450 cfg.route(
451 "/{user_id}/chapters/{chapter_id}/status",
452 web::post().to(teacher_set_student_chapter_status),
453 );
454}