headless_lms_server/controllers/course_material/
chapters.rs1use models::chapters::ChapterLockPreview;
4use models::pages::{Page, PageVisibility, PageWithExercises};
5use models::user_chapter_locking_statuses::{self, ChapterLockingStatus};
6use utoipa::OpenApi;
7
8use crate::domain::authorization::authorize_access_to_course_material;
9use crate::prelude::*;
10
11#[derive(OpenApi)]
12#[openapi(paths(
13 get_public_chapter_pages,
14 get_chapters_exercises,
15 get_chapters_pages_without_main_frontpage,
16 get_chapter_lock_preview,
17 lock_chapter
18))]
19pub(crate) struct CourseMaterialChaptersApiDoc;
20
21#[utoipa::path(
25 get,
26 path = "/{chapter_id}/pages",
27 operation_id = "getCourseMaterialChapterPages",
28 tag = "course-material-chapters",
29 params(
30 ("chapter_id" = Uuid, Path, description = "Chapter id")
31 ),
32 responses(
33 (status = 200, description = "Public chapter pages", body = Vec<Page>)
34 )
35)]
36#[instrument(skip(pool))]
37async fn get_public_chapter_pages(
38 chapter_id: web::Path<Uuid>,
39 pool: web::Data<PgPool>,
40 auth: Option<AuthUser>,
41) -> ControllerResult<web::Json<Vec<Page>>> {
42 let mut conn = pool.acquire().await?;
43 let user_id = auth.map(|u| u.id);
44 let token = authorize(&mut conn, Act::View, user_id, Res::Chapter(*chapter_id)).await?;
45 let chapter_pages: Vec<Page> = models::pages::get_course_pages_by_chapter_id_and_visibility(
46 &mut conn,
47 *chapter_id,
48 PageVisibility::Public,
49 )
50 .await?;
51 let chapter_pages =
52 models::pages::filter_course_material_pages(&mut conn, user_id, chapter_pages).await?;
53 token.authorized_ok(web::Json(chapter_pages))
54}
55
56#[utoipa::path(
60 get,
61 path = "/{chapter_id}/exercises",
62 operation_id = "getCourseMaterialChapterPagesWithExercises",
63 tag = "course-material-chapters",
64 params(
65 ("chapter_id" = Uuid, Path, description = "Chapter id")
66 ),
67 responses(
68 (status = 200, description = "Chapter pages with exercises", body = Vec<PageWithExercises>)
69 )
70)]
71#[instrument(skip(pool))]
72async fn get_chapters_exercises(
73 chapter_id: web::Path<Uuid>,
74 pool: web::Data<PgPool>,
75 auth: Option<AuthUser>,
76) -> ControllerResult<web::Json<Vec<PageWithExercises>>> {
77 let mut conn = pool.acquire().await?;
78 let user_id = auth.map(|u| u.id);
79 let token = authorize(&mut conn, Act::View, user_id, Res::Chapter(*chapter_id)).await?;
80
81 let chapter_pages_with_exercises =
82 models::pages::get_chapters_pages_with_exercises(&mut conn, *chapter_id).await?;
83 let chapter_pages_with_exercises = models::pages::filter_course_material_pages_with_exercises(
84 &mut conn,
85 user_id,
86 chapter_pages_with_exercises,
87 )
88 .await?;
89 token.authorized_ok(web::Json(chapter_pages_with_exercises))
90}
91
92#[utoipa::path(
96 get,
97 path = "/{chapter_id}/pages-exclude-mainfrontpage",
98 operation_id = "getCourseMaterialChapterPagesExcludingFrontPage",
99 tag = "course-material-chapters",
100 params(
101 ("chapter_id" = Uuid, Path, description = "Chapter id")
102 ),
103 responses(
104 (status = 200, description = "Visible chapter pages without main front page", body = Vec<Page>)
105 )
106)]
107#[instrument(skip(pool))]
108async fn get_chapters_pages_without_main_frontpage(
109 chapter_id: web::Path<Uuid>,
110 pool: web::Data<PgPool>,
111 auth: Option<AuthUser>,
112) -> ControllerResult<web::Json<Vec<Page>>> {
113 let mut conn = pool.acquire().await?;
114 let user_id = auth.map(|u| u.id);
115 let token = authorize(&mut conn, Act::View, user_id, Res::Chapter(*chapter_id)).await?;
116 let chapter_pages =
117 models::pages::get_chapters_visible_pages_exclude_main_frontpage(&mut conn, *chapter_id)
118 .await?;
119 let chapter_pages =
120 models::pages::filter_course_material_pages(&mut conn, user_id, chapter_pages).await?;
121 token.authorized_ok(web::Json(chapter_pages))
122}
123
124#[utoipa::path(
130 get,
131 path = "/{chapter_id}/lock-preview",
132 operation_id = "getCourseMaterialChapterLockPreview",
133 tag = "course-material-chapters",
134 params(
135 ("chapter_id" = Uuid, Path, description = "Chapter id")
136 ),
137 responses(
138 (status = 200, description = "Chapter lock preview", body = ChapterLockPreview)
139 )
140)]
141#[instrument(skip(pool))]
142async fn get_chapter_lock_preview(
143 chapter_id: web::Path<Uuid>,
144 pool: web::Data<PgPool>,
145 user: AuthUser,
146) -> ControllerResult<web::Json<ChapterLockPreview>> {
147 let mut conn = pool.acquire().await?;
148 let token = authorize(
149 &mut conn,
150 Act::View,
151 Some(user.id),
152 Res::Chapter(*chapter_id),
153 )
154 .await?;
155
156 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
157 let preview = models::chapters::get_chapter_lock_preview(
158 &mut conn,
159 *chapter_id,
160 user.id,
161 chapter.course_id,
162 )
163 .await?;
164
165 token.authorized_ok(web::Json(preview))
166}
167
168#[utoipa::path(
181 post,
182 path = "/{chapter_id}/lock",
183 operation_id = "lockCourseMaterialChapter",
184 tag = "course-material-chapters",
185 params(
186 ("chapter_id" = Uuid, Path, description = "Chapter id")
187 ),
188 responses(
189 (status = 200, description = "Updated chapter locking status", body = user_chapter_locking_statuses::UserChapterLockingStatus)
190 )
191)]
192#[instrument(skip(pool))]
193async fn lock_chapter(
194 chapter_id: web::Path<Uuid>,
195 pool: web::Data<PgPool>,
196 user: AuthUser,
197) -> ControllerResult<web::Json<user_chapter_locking_statuses::UserChapterLockingStatus>> {
198 let mut conn = pool.acquire().await?;
199 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
200 let token =
201 authorize_access_to_course_material(&mut conn, Some(user.id), chapter.course_id).await?;
202
203 let course = models::courses::get_course(&mut conn, chapter.course_id).await?;
204
205 if !course.chapter_locking_enabled {
206 return Err(ControllerError::new(
207 ControllerErrorType::BadRequest,
208 "Chapter locking is not enabled for this course.".to_string(),
209 None,
210 ));
211 }
212
213 let previous_chapters =
214 models::chapters::get_previous_chapters_in_module(&mut conn, *chapter_id).await?;
215
216 let mut tx = conn.begin().await?;
217
218 let current_status = user_chapter_locking_statuses::get_or_init_status(
219 &mut tx,
220 user.id,
221 *chapter_id,
222 Some(chapter.course_id),
223 Some(course.chapter_locking_enabled),
224 )
225 .await?;
226
227 match current_status {
228 None | Some(ChapterLockingStatus::NotUnlockedYet) => {
229 return Err(ControllerError::new(
230 ControllerErrorType::BadRequest,
231 "This chapter is locked. Complete previous chapters first.".to_string(),
232 None,
233 ));
234 }
235 Some(ChapterLockingStatus::CompletedAndLocked) => {
236 return Err(ControllerError::new(
237 ControllerErrorType::BadRequest,
238 "This chapter is already completed.".to_string(),
239 None,
240 ));
241 }
242 Some(ChapterLockingStatus::Unlocked) => {
243 }
245 }
246
247 for prev_chapter in previous_chapters {
248 let prev_status = user_chapter_locking_statuses::get_or_init_status(
249 &mut tx,
250 user.id,
251 prev_chapter.id,
252 Some(chapter.course_id),
253 Some(course.chapter_locking_enabled),
254 )
255 .await?;
256
257 match prev_status {
258 None
259 | Some(ChapterLockingStatus::Unlocked)
260 | Some(ChapterLockingStatus::NotUnlockedYet) => {
261 return Err(ControllerError::new(
262 ControllerErrorType::BadRequest,
263 format!(
264 "You must complete previous chapters in order. Please complete chapter \"{}\" first.",
265 prev_chapter.name
266 ),
267 None,
268 ));
269 }
270 Some(ChapterLockingStatus::CompletedAndLocked) => {
271 }
273 }
274 }
275
276 models::chapters::move_chapter_exercises_to_manual_review(
277 &mut tx,
278 *chapter_id,
279 user.id,
280 chapter.course_id,
281 )
282 .await?;
283
284 let status = user_chapter_locking_statuses::complete_and_lock_chapter(
285 &mut tx,
286 user.id,
287 *chapter_id,
288 chapter.course_id,
289 )
290 .await?;
291
292 models::chapters::unlock_next_chapters_for_user(
293 &mut tx,
294 user.id,
295 *chapter_id,
296 chapter.course_id,
297 )
298 .await?;
299
300 tx.commit().await?;
301
302 token.authorized_ok(web::Json(status))
303}
304
305pub fn _add_routes(cfg: &mut ServiceConfig) {
313 cfg.route(
314 "/{chapter_id}/pages",
315 web::get().to(get_public_chapter_pages),
316 )
317 .route(
318 "/{chapter_id}/exercises",
319 web::get().to(get_chapters_exercises),
320 )
321 .route(
322 "/{chapter_id}/pages-exclude-mainfrontpage",
323 web::get().to(get_chapters_pages_without_main_frontpage),
324 )
325 .route(
326 "/{chapter_id}/lock-preview",
327 web::get().to(get_chapter_lock_preview),
328 )
329 .route("/{chapter_id}/lock", web::post().to(lock_chapter));
330}