headless_lms_server/controllers/main_frontend/courses/
students.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/courses/{course_id}/students`.
2use 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/// GET `/api/v0/main-frontend/courses/{course_id}/students/progress`
35#[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/// GET `/api/v0/main-frontend/courses/{course_id}/students/{user_id}/chapter-locking-statuses`
68#[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/// GET `/api/v0/main-frontend/courses/{course_id}/students/users`
115#[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/// GET `/api/v0/main-frontend/courses/{course_id}/students/completions`
148#[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/// GET `/api/v0/main-frontend/courses/{course_id}/students/certificates`
183#[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/// POST `/api/v0/main-frontend/courses/{course_id}/students/{user_id}/chapters/{chapter_id}/lock`
218#[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/// POST `/api/v0/main-frontend/courses/{course_id}/students/{user_id}/chapters/{chapter_id}/unlock`
289#[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/// POST `/api/v0/main-frontend/courses/{course_id}/students/{user_id}/chapters/{chapter_id}/status`
360#[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}