1use 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 chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
45 let token = authorize_access_to_course_material(&mut conn, user_id, chapter.course_id).await?;
46 if !models::chapters::is_open(&mut conn, *chapter_id).await? {
47 authorize(
48 &mut conn,
49 Act::ViewMaterial,
50 user_id,
51 Res::Course(chapter.course_id),
52 )
53 .await?;
54 }
55 let chapter_pages: Vec<Page> = models::pages::get_course_pages_by_chapter_id_and_visibility(
56 &mut conn,
57 *chapter_id,
58 PageVisibility::Public,
59 )
60 .await?;
61 let chapter_pages =
62 models::pages::filter_course_material_pages(&mut conn, user_id, chapter_pages).await?;
63 token.authorized_ok(web::Json(chapter_pages))
64}
65
66#[utoipa::path(
70 get,
71 path = "/{chapter_id}/exercises",
72 operation_id = "getCourseMaterialChapterPagesWithExercises",
73 tag = "course-material-chapters",
74 params(
75 ("chapter_id" = Uuid, Path, description = "Chapter id")
76 ),
77 responses(
78 (status = 200, description = "Chapter pages with exercises", body = Vec<PageWithExercises>)
79 )
80)]
81#[instrument(skip(pool))]
82async fn get_chapters_exercises(
83 chapter_id: web::Path<Uuid>,
84 pool: web::Data<PgPool>,
85 auth: Option<AuthUser>,
86) -> ControllerResult<web::Json<Vec<PageWithExercises>>> {
87 let mut conn = pool.acquire().await?;
88 let user_id = auth.map(|u| u.id);
89 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
90 let token = authorize_access_to_course_material(&mut conn, user_id, chapter.course_id).await?;
91 if !models::chapters::is_open(&mut conn, *chapter_id).await? {
92 authorize(
93 &mut conn,
94 Act::ViewMaterial,
95 user_id,
96 Res::Course(chapter.course_id),
97 )
98 .await?;
99 }
100 let can_view_hidden_pages = if let Some(user_id) = user_id {
101 authorize(
102 &mut conn,
103 Act::ViewMaterial,
104 Some(user_id),
105 Res::Course(chapter.course_id),
106 )
107 .await
108 .is_ok()
109 } else {
110 false
111 };
112
113 let chapter_pages_with_exercises =
114 models::pages::get_chapters_pages_with_exercises(&mut conn, *chapter_id).await?;
115 let chapter_pages_with_exercises = chapter_pages_with_exercises
116 .into_iter()
117 .filter(|page_with_exercises| can_view_hidden_pages || !page_with_exercises.page.hidden)
118 .collect();
119 let chapter_pages_with_exercises = models::pages::filter_course_material_pages_with_exercises(
120 &mut conn,
121 user_id,
122 chapter_pages_with_exercises,
123 )
124 .await?;
125 token.authorized_ok(web::Json(chapter_pages_with_exercises))
126}
127
128#[utoipa::path(
132 get,
133 path = "/{chapter_id}/pages-exclude-mainfrontpage",
134 operation_id = "getCourseMaterialChapterPagesExcludingFrontPage",
135 tag = "course-material-chapters",
136 params(
137 ("chapter_id" = Uuid, Path, description = "Chapter id")
138 ),
139 responses(
140 (status = 200, description = "Visible chapter pages without main front page", body = Vec<Page>)
141 )
142)]
143#[instrument(skip(pool))]
144async fn get_chapters_pages_without_main_frontpage(
145 chapter_id: web::Path<Uuid>,
146 pool: web::Data<PgPool>,
147 auth: Option<AuthUser>,
148) -> ControllerResult<web::Json<Vec<Page>>> {
149 let mut conn = pool.acquire().await?;
150 let user_id = auth.map(|u| u.id);
151 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
152 let token = authorize_access_to_course_material(&mut conn, user_id, chapter.course_id).await?;
153 if !models::chapters::is_open(&mut conn, *chapter_id).await? {
154 authorize(
155 &mut conn,
156 Act::ViewMaterial,
157 user_id,
158 Res::Course(chapter.course_id),
159 )
160 .await?;
161 }
162 let chapter_pages =
163 models::pages::get_chapters_visible_pages_exclude_main_frontpage(&mut conn, *chapter_id)
164 .await?;
165 let chapter_pages =
166 models::pages::filter_course_material_pages(&mut conn, user_id, chapter_pages).await?;
167 token.authorized_ok(web::Json(chapter_pages))
168}
169
170#[utoipa::path(
176 get,
177 path = "/{chapter_id}/lock-preview",
178 operation_id = "getCourseMaterialChapterLockPreview",
179 tag = "course-material-chapters",
180 params(
181 ("chapter_id" = Uuid, Path, description = "Chapter id")
182 ),
183 responses(
184 (status = 200, description = "Chapter lock preview", body = ChapterLockPreview)
185 )
186)]
187#[instrument(skip(pool))]
188async fn get_chapter_lock_preview(
189 chapter_id: web::Path<Uuid>,
190 pool: web::Data<PgPool>,
191 user: AuthUser,
192) -> ControllerResult<web::Json<ChapterLockPreview>> {
193 let mut conn = pool.acquire().await?;
194 let token = authorize(
195 &mut conn,
196 Act::View,
197 Some(user.id),
198 Res::Chapter(*chapter_id),
199 )
200 .await?;
201
202 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
203 let preview = models::chapters::get_chapter_lock_preview(
204 &mut conn,
205 *chapter_id,
206 user.id,
207 chapter.course_id,
208 )
209 .await?;
210
211 token.authorized_ok(web::Json(preview))
212}
213
214#[utoipa::path(
227 post,
228 path = "/{chapter_id}/lock",
229 operation_id = "lockCourseMaterialChapter",
230 tag = "course-material-chapters",
231 params(
232 ("chapter_id" = Uuid, Path, description = "Chapter id")
233 ),
234 responses(
235 (status = 200, description = "Updated chapter locking status", body = user_chapter_locking_statuses::UserChapterLockingStatus)
236 )
237)]
238#[instrument(skip(pool))]
239async fn lock_chapter(
240 chapter_id: web::Path<Uuid>,
241 pool: web::Data<PgPool>,
242 user: AuthUser,
243) -> ControllerResult<web::Json<user_chapter_locking_statuses::UserChapterLockingStatus>> {
244 let mut conn = pool.acquire().await?;
245 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
246 let token =
247 authorize_access_to_course_material(&mut conn, Some(user.id), chapter.course_id).await?;
248
249 let course = models::courses::get_course(&mut conn, chapter.course_id).await?;
250
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 let previous_chapters =
260 models::chapters::get_previous_chapters_in_module(&mut conn, *chapter_id).await?;
261
262 let mut tx = conn.begin().await?;
263
264 let current_status = user_chapter_locking_statuses::get_or_init_status(
265 &mut tx,
266 user.id,
267 *chapter_id,
268 Some(chapter.course_id),
269 Some(course.chapter_locking_enabled),
270 )
271 .await?;
272
273 match current_status {
274 None | Some(ChapterLockingStatus::NotUnlockedYet) => {
275 return Err(ControllerError::new(
276 ControllerErrorType::BadRequest,
277 "This chapter is locked. Complete previous chapters first.".to_string(),
278 None,
279 ));
280 }
281 Some(ChapterLockingStatus::CompletedAndLocked) => {
282 return Err(ControllerError::new(
283 ControllerErrorType::BadRequest,
284 "This chapter is already completed.".to_string(),
285 None,
286 ));
287 }
288 Some(ChapterLockingStatus::Unlocked) => {
289 }
291 }
292
293 for prev_chapter in previous_chapters {
294 let prev_status = user_chapter_locking_statuses::get_or_init_status(
295 &mut tx,
296 user.id,
297 prev_chapter.id,
298 Some(chapter.course_id),
299 Some(course.chapter_locking_enabled),
300 )
301 .await?;
302
303 match prev_status {
304 None
305 | Some(ChapterLockingStatus::Unlocked)
306 | Some(ChapterLockingStatus::NotUnlockedYet) => {
307 return Err(ControllerError::new(
308 ControllerErrorType::BadRequest,
309 format!(
310 "You must complete previous chapters in order. Please complete chapter \"{}\" first.",
311 prev_chapter.name
312 ),
313 None,
314 ));
315 }
316 Some(ChapterLockingStatus::CompletedAndLocked) => {
317 }
319 }
320 }
321
322 models::chapters::move_chapter_exercises_to_manual_review(
323 &mut tx,
324 *chapter_id,
325 user.id,
326 chapter.course_id,
327 )
328 .await?;
329
330 let status = user_chapter_locking_statuses::complete_and_lock_chapter(
331 &mut tx,
332 user.id,
333 *chapter_id,
334 chapter.course_id,
335 )
336 .await?;
337
338 models::chapters::unlock_next_chapters_for_user(
339 &mut tx,
340 user.id,
341 *chapter_id,
342 chapter.course_id,
343 )
344 .await?;
345
346 tx.commit().await?;
347
348 token.authorized_ok(web::Json(status))
349}
350
351pub fn _add_routes(cfg: &mut ServiceConfig) {
359 cfg.route(
360 "/{chapter_id}/pages",
361 web::get().to(get_public_chapter_pages),
362 )
363 .route(
364 "/{chapter_id}/exercises",
365 web::get().to(get_chapters_exercises),
366 )
367 .route(
368 "/{chapter_id}/pages-exclude-mainfrontpage",
369 web::get().to(get_chapters_pages_without_main_frontpage),
370 )
371 .route(
372 "/{chapter_id}/lock-preview",
373 web::get().to(get_chapter_lock_preview),
374 )
375 .route("/{chapter_id}/lock", web::post().to(lock_chapter));
376}