headless_lms_server/controllers/main_frontend/
chapters.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/chapters`.
2
3use std::{path::PathBuf, str::FromStr, sync::Arc};
4
5use headless_lms_models::chapters::DatabaseChapter;
6use models::chapters::{Chapter, ChapterUpdate, NewChapter};
7
8use crate::{
9    domain::{
10        models_requests::{self, JwtKey},
11        request_id::RequestId,
12    },
13    prelude::*,
14};
15
16/**
17POST `/api/v0/main-frontend/chapters` - Create a new course part.
18# Example
19
20Request:
21```http
22POST /api/v0/main-frontend/chapters HTTP/1.1
23Content-Type: application/json
24
25{
26    "name": "The Basics",
27    "course_id": "d86cf910-4d26-40e9-8c9c-1cc35294fdbb",
28    "chapter_number": 1,
29    "front_page_id": null
30}
31```
32*/
33
34#[instrument(skip(pool, file_store, app_conf))]
35async fn post_new_chapter(
36    request_id: RequestId,
37    pool: web::Data<PgPool>,
38    payload: web::Json<NewChapter>,
39    user: AuthUser,
40    file_store: web::Data<dyn FileStore>,
41    app_conf: web::Data<ApplicationConfiguration>,
42    jwt_key: web::Data<JwtKey>,
43) -> ControllerResult<web::Json<Chapter>> {
44    let mut conn = pool.acquire().await?;
45    let token = authorize(
46        &mut conn,
47        Act::Edit,
48        Some(user.id),
49        Res::Course(payload.course_id),
50    )
51    .await?;
52    let new_chapter = payload.0;
53    let (database_chapter, ..) = models::library::content_management::create_new_chapter(
54        &mut conn,
55        PKeyPolicy::Generate,
56        &new_chapter,
57        user.id,
58        models_requests::make_spec_fetcher(
59            app_conf.base_url.clone(),
60            request_id.0,
61            Arc::clone(&jwt_key),
62        ),
63        models_requests::fetch_service_info,
64    )
65    .await?;
66    return token.authorized_ok(web::Json(Chapter::from_database_chapter(
67        &database_chapter,
68        file_store.as_ref(),
69        app_conf.as_ref(),
70    )));
71}
72
73/**
74DELETE `/api/v0/main-frontend/chapters/:chapter_id` - Delete a chapter.
75*/
76
77#[instrument(skip(pool, file_store, app_conf))]
78async fn delete_chapter(
79    chapter_id: web::Path<String>,
80    pool: web::Data<PgPool>,
81    user: AuthUser,
82    file_store: web::Data<dyn FileStore>,
83    app_conf: web::Data<ApplicationConfiguration>,
84) -> ControllerResult<web::Json<Chapter>> {
85    let mut conn = pool.acquire().await?;
86    let chapter_id = Uuid::from_str(&chapter_id)?;
87    let token = authorize(
88        &mut conn,
89        Act::Edit,
90        Some(user.id),
91        Res::Chapter(chapter_id),
92    )
93    .await?;
94    let deleted_chapter = models::chapters::delete_chapter(&mut conn, chapter_id).await?;
95    return token.authorized_ok(web::Json(Chapter::from_database_chapter(
96        &deleted_chapter,
97        file_store.as_ref(),
98        app_conf.as_ref(),
99    )));
100}
101
102/**
103PUT `/api/v0/main-frontend/chapters/:chapter_id` - Update chapter.
104# Example
105
106Request:
107```http
108PUT /api/v0/main-frontend/chapters/d332f3d9-39a5-4a18-80f4-251727693c37  HTTP/1.1
109Content-Type: application/json
110
111{
112    "name": "The Basics",
113    "chapter_image_url": null,
114    "chapter_number": 2,
115    "front_page_id": "0ebba931-b027-4154-8274-2afb00d79306"
116}
117
118```
119*/
120
121#[instrument(skip(payload, pool, file_store, app_conf))]
122async fn update_chapter(
123    payload: web::Json<ChapterUpdate>,
124    chapter_id: web::Path<String>,
125    pool: web::Data<PgPool>,
126    user: AuthUser,
127    file_store: web::Data<dyn FileStore>,
128    app_conf: web::Data<ApplicationConfiguration>,
129) -> ControllerResult<web::Json<Chapter>> {
130    let mut conn = pool.acquire().await?;
131    let chapter_id = Uuid::from_str(&chapter_id)?;
132    let course_id = models::chapters::get_course_id(&mut conn, chapter_id).await?;
133    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
134    let course_update = payload.0;
135    let chapter = models::chapters::update_chapter(&mut conn, chapter_id, course_update).await?;
136
137    let response = Chapter::from_database_chapter(&chapter, file_store.as_ref(), app_conf.as_ref());
138
139    token.authorized_ok(web::Json(response))
140}
141
142/**
143PUT `/api/v0/main-frontend/chapters/:chapter_id/image` - Sets or updates the chapter image.
144
145# Example
146
147Request:
148```http
149PUT /api/v0/main-frontend/chapters/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
150Content-Type: multipart/form-data
151
152BINARY_DATA
153```
154*/
155
156#[instrument(skip(request, payload, pool, file_store, app_conf))]
157async fn set_chapter_image(
158    request: HttpRequest,
159    payload: Multipart,
160    chapter_id: web::Path<Uuid>,
161    pool: web::Data<PgPool>,
162    user: AuthUser,
163    file_store: web::Data<dyn FileStore>,
164    app_conf: web::Data<ApplicationConfiguration>,
165) -> ControllerResult<web::Json<Chapter>> {
166    let mut conn = pool.acquire().await?;
167    let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
168    let token = authorize(
169        &mut conn,
170        Act::Edit,
171        Some(user.id),
172        Res::Course(chapter.course_id),
173    )
174    .await?;
175
176    let course = models::courses::get_course(&mut conn, chapter.course_id).await?;
177    let chapter_image = upload_file_from_cms(
178        request.headers(),
179        payload,
180        StoreKind::Course(course.id),
181        file_store.as_ref(),
182        &mut conn,
183        user,
184    )
185    .await?
186    .to_string_lossy()
187    .to_string();
188    let updated_chapter =
189        models::chapters::update_chapter_image_path(&mut conn, chapter.id, Some(chapter_image))
190            .await?;
191
192    // Remove old image if one exists.
193    if let Some(old_image_path) = chapter.chapter_image_path {
194        let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
195            ControllerError::new(
196                ControllerErrorType::InternalServerError,
197                original_error.to_string(),
198                Some(original_error.into()),
199            )
200        })?;
201        file_store.delete(&file).await.map_err(|original_error| {
202            ControllerError::new(
203                ControllerErrorType::InternalServerError,
204                original_error.to_string(),
205                Some(original_error.into()),
206            )
207        })?;
208    }
209
210    let response =
211        Chapter::from_database_chapter(&updated_chapter, file_store.as_ref(), app_conf.as_ref());
212
213    token.authorized_ok(web::Json(response))
214}
215
216/**
217DELETE `/api/v0/main-frontend/chapters/:chapter_id/image` - Removes the chapter image.
218
219# Example
220
221Request:
222```http
223DELETE /api/v0/main-frontend/chapters/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
224```
225*/
226
227#[instrument(skip(pool, file_store))]
228async fn remove_chapter_image(
229    chapter_id: web::Path<Uuid>,
230    pool: web::Data<PgPool>,
231    user: AuthUser,
232    file_store: web::Data<dyn FileStore>,
233) -> ControllerResult<web::Json<()>> {
234    let mut conn = pool.acquire().await?;
235    let chapter = models::chapters::get_chapter(&mut conn, *chapter_id).await?;
236    let token = authorize(
237        &mut conn,
238        Act::Edit,
239        Some(user.id),
240        Res::Course(chapter.course_id),
241    )
242    .await?;
243    if let Some(chapter_image_path) = chapter.chapter_image_path {
244        let file = PathBuf::from_str(&chapter_image_path).map_err(|original_error| {
245            ControllerError::new(
246                ControllerErrorType::InternalServerError,
247                original_error.to_string(),
248                Some(original_error.into()),
249            )
250        })?;
251        let _res = models::chapters::update_chapter_image_path(&mut conn, chapter.id, None).await?;
252        file_store.delete(&file).await.map_err(|original_error| {
253            ControllerError::new(
254                ControllerErrorType::InternalServerError,
255                original_error.to_string(),
256                Some(original_error.into()),
257            )
258        })?;
259    }
260    token.authorized_ok(web::Json(()))
261}
262
263/**
264GET `/api/v0/main-frontend/chapters/{course_id}/all-chapters-for-course - Gets all chapters with a course_id
265*/
266async fn get_all_chapters_by_course_id(
267    course_id: web::Path<Uuid>,
268    pool: web::Data<PgPool>,
269    user: AuthUser,
270) -> ControllerResult<web::Json<Vec<DatabaseChapter>>> {
271    let mut conn = pool.acquire().await?;
272    let token = authorize(&mut conn, Act::View, Some(user.id), Res::Course(*course_id)).await?;
273
274    let mut chapters = models::chapters::course_chapters(&mut conn, *course_id).await?;
275
276    chapters.sort_by(|a, b| a.chapter_number.cmp(&b.chapter_number));
277
278    token.authorized_ok(web::Json(chapters))
279}
280
281/**
282Add a route for each controller in this module.
283
284The name starts with an underline in order to appear before other functions in the module documentation.
285
286We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
287*/
288pub fn _add_routes(cfg: &mut ServiceConfig) {
289    cfg.route("", web::post().to(post_new_chapter))
290        .route("/{chapter_id}", web::delete().to(delete_chapter))
291        .route("/{chapter_id}", web::put().to(update_chapter))
292        .route("/{chapter_id}/image", web::put().to(set_chapter_image))
293        .route(
294            "/{chapter_id}/image",
295            web::delete().to(remove_chapter_image),
296        )
297        .route(
298            "/{course_id}/all-chapters-for-course",
299            web::get().to(get_all_chapters_by_course_id),
300        );
301}