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};
6
7use crate::domain::authorization::authorize_access_to_course_material;
8use crate::prelude::*;
9
10#[instrument(skip(pool))]
14async fn get_public_chapter_pages(
15 chapter_id: web::Path<Uuid>,
16 pool: web::Data<PgPool>,
17 auth: Option<AuthUser>,
18) -> ControllerResult<web::Json<Vec<Page>>> {
19 let mut conn = pool.acquire().await?;
20 let token = authorize(
21 &mut conn,
22 Act::View,
23 auth.map(|u| u.id),
24 Res::Chapter(*chapter_id),
25 )
26 .await?;
27 let chapter_pages: Vec<Page> = models::pages::get_course_pages_by_chapter_id_and_visibility(
28 &mut conn,
29 *chapter_id,
30 PageVisibility::Public,
31 )
32 .await?;
33 token.authorized_ok(web::Json(chapter_pages))
34}
35
36#[instrument(skip(pool))]
40async fn get_chapters_exercises(
41 chapter_id: web::Path<Uuid>,
42 pool: web::Data<PgPool>,
43 auth: Option<AuthUser>,
44) -> ControllerResult<web::Json<Vec<PageWithExercises>>> {
45 let mut conn = pool.acquire().await?;
46 let token = authorize(
47 &mut conn,
48 Act::View,
49 auth.map(|u| u.id),
50 Res::Chapter(*chapter_id),
51 )
52 .await?;
53
54 let chapter_pages_with_exercises =
55 models::pages::get_chapters_pages_with_exercises(&mut conn, *chapter_id).await?;
56 token.authorized_ok(web::Json(chapter_pages_with_exercises))
57}
58
59#[instrument(skip(pool))]
63async fn get_chapters_pages_without_main_frontpage(
64 chapter_id: web::Path<Uuid>,
65 pool: web::Data<PgPool>,
66 auth: Option<AuthUser>,
67) -> ControllerResult<web::Json<Vec<Page>>> {
68 let mut conn = pool.acquire().await?;
69 let token = authorize(
70 &mut conn,
71 Act::View,
72 auth.map(|u| u.id),
73 Res::Chapter(*chapter_id),
74 )
75 .await?;
76 let chapter_pages =
77 models::pages::get_chapters_visible_pages_exclude_main_frontpage(&mut conn, *chapter_id)
78 .await?
79 .into_iter()
80 .collect();
81 token.authorized_ok(web::Json(chapter_pages))
82}
83
84#[instrument(skip(pool))]
90async fn get_chapter_lock_preview(
91 chapter_id: web::Path<Uuid>,
92 pool: web::Data<PgPool>,
93 user: AuthUser,
94) -> ControllerResult<web::Json<ChapterLockPreview>> {
95 let mut conn = pool.acquire().await?;
96 let token = authorize(
97 &mut conn,
98 Act::View,
99 Some(user.id),
100 Res::Chapter(*chapter_id),
101 )
102 .await?;
103
104 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
105 let preview = models::chapters::get_chapter_lock_preview(
106 &mut conn,
107 *chapter_id,
108 user.id,
109 chapter.course_id,
110 )
111 .await?;
112
113 token.authorized_ok(web::Json(preview))
114}
115
116#[instrument(skip(pool))]
129async fn lock_chapter(
130 chapter_id: web::Path<Uuid>,
131 pool: web::Data<PgPool>,
132 user: AuthUser,
133) -> ControllerResult<web::Json<user_chapter_locking_statuses::UserChapterLockingStatus>> {
134 let mut conn = pool.acquire().await?;
135 let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
136 let token =
137 authorize_access_to_course_material(&mut conn, Some(user.id), chapter.course_id).await?;
138
139 let course = models::courses::get_course(&mut conn, chapter.course_id).await?;
140
141 if !course.chapter_locking_enabled {
142 return Err(ControllerError::new(
143 ControllerErrorType::BadRequest,
144 "Chapter locking is not enabled for this course.".to_string(),
145 None,
146 ));
147 }
148
149 let previous_chapters =
150 models::chapters::get_previous_chapters_in_module(&mut conn, *chapter_id).await?;
151
152 let mut tx = conn.begin().await?;
153
154 let current_status = user_chapter_locking_statuses::get_or_init_status(
155 &mut tx,
156 user.id,
157 *chapter_id,
158 Some(chapter.course_id),
159 Some(course.chapter_locking_enabled),
160 )
161 .await?;
162
163 match current_status {
164 None | Some(ChapterLockingStatus::NotUnlockedYet) => {
165 return Err(ControllerError::new(
166 ControllerErrorType::BadRequest,
167 "This chapter is locked. Complete previous chapters first.".to_string(),
168 None,
169 ));
170 }
171 Some(ChapterLockingStatus::CompletedAndLocked) => {
172 return Err(ControllerError::new(
173 ControllerErrorType::BadRequest,
174 "This chapter is already completed.".to_string(),
175 None,
176 ));
177 }
178 Some(ChapterLockingStatus::Unlocked) => {
179 }
181 }
182
183 for prev_chapter in previous_chapters {
184 let prev_status = user_chapter_locking_statuses::get_or_init_status(
185 &mut tx,
186 user.id,
187 prev_chapter.id,
188 Some(chapter.course_id),
189 Some(course.chapter_locking_enabled),
190 )
191 .await?;
192
193 match prev_status {
194 None
195 | Some(ChapterLockingStatus::Unlocked)
196 | Some(ChapterLockingStatus::NotUnlockedYet) => {
197 return Err(ControllerError::new(
198 ControllerErrorType::BadRequest,
199 format!(
200 "You must complete previous chapters in order. Please complete chapter \"{}\" first.",
201 prev_chapter.name
202 ),
203 None,
204 ));
205 }
206 Some(ChapterLockingStatus::CompletedAndLocked) => {
207 }
209 }
210 }
211
212 models::chapters::move_chapter_exercises_to_manual_review(
213 &mut tx,
214 *chapter_id,
215 user.id,
216 chapter.course_id,
217 )
218 .await?;
219
220 let status = user_chapter_locking_statuses::complete_chapter(
221 &mut tx,
222 user.id,
223 *chapter_id,
224 chapter.course_id,
225 )
226 .await?;
227
228 models::chapters::unlock_next_chapters_for_user(
229 &mut tx,
230 user.id,
231 *chapter_id,
232 chapter.course_id,
233 )
234 .await?;
235
236 tx.commit().await?;
237
238 token.authorized_ok(web::Json(status))
239}
240
241pub fn _add_routes(cfg: &mut ServiceConfig) {
249 cfg.route(
250 "/{chapter_id}/pages",
251 web::get().to(get_public_chapter_pages),
252 )
253 .route(
254 "/{chapter_id}/exercises",
255 web::get().to(get_chapters_exercises),
256 )
257 .route(
258 "/{chapter_id}/pages-exclude-mainfrontpage",
259 web::get().to(get_chapters_pages_without_main_frontpage),
260 )
261 .route(
262 "/{chapter_id}/lock-preview",
263 web::get().to(get_chapter_lock_preview),
264 )
265 .route("/{chapter_id}/lock", web::post().to(lock_chapter));
266}