Skip to main content

headless_lms_server/controllers/main_frontend/
pages.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/pages`.
2
3use std::sync::Arc;
4
5use models::{
6    CourseOrExamId,
7    page_history::PageHistory,
8    pages::{HistoryRestoreData, NewPage, Page, PageDetailsUpdate, PageInfo},
9};
10use utoipa::OpenApi;
11
12use crate::{
13    domain::{
14        models_requests::{self, JwtKey},
15        request_id::RequestId,
16    },
17    prelude::*,
18};
19
20#[derive(OpenApi)]
21#[openapi(paths(
22    post_new_page,
23    delete_page,
24    get_page_info,
25    update_page_details,
26    history,
27    history_count,
28    restore,
29    get_all_pages_by_course_id
30))]
31pub(crate) struct MainFrontendPagesApiDoc;
32
33/**
34POST `/api/v0/main-frontend/pages` - Create a new page.
35
36Please note that this endpoint will change all the exercise and exercise task ids you've created. Make sure the use the updated ids from the response object.
37
38If optional property front_page_of_chapter_id is set, this page will become the front page of the specified course part.
39
40# Example:
41
42Request:
43```http
44POST /api/v0/main-frontend/pages HTTP/1.1
45Content-Type: application/json
46
47{
48  "content": [
49    {
50      "type": "x",
51      "id": "2a4e517d-a7d2-4d82-89fb-a1333d8d01d1"
52    }
53  ],
54  "url_path": "/part-2/best-page",
55  "title": "Hello world!",
56  "course_id": "10363c5b-82b4-4121-8ef1-bae8fb42a5ce",
57  "chapter_id": "2495ffa3-7ea9-4615-baa5-828023688c79"
58}
59```
60*/
61
62#[instrument(skip(pool, app_conf))]
63#[utoipa::path(
64    post,
65    path = "",
66    operation_id = "createPage",
67    tag = "pages",
68    request_body = NewPage,
69    responses(
70        (status = 200, description = "Created page", body = Page)
71    )
72)]
73async fn post_new_page(
74    request_id: RequestId,
75    payload: web::Json<NewPage>,
76    pool: web::Data<PgPool>,
77    app_conf: web::Data<ApplicationConfiguration>,
78    user: AuthUser,
79    jwt_key: web::Data<JwtKey>,
80) -> ControllerResult<web::Json<Page>> {
81    let mut conn = pool.acquire().await?;
82    let new_page = payload.0;
83    let course_id = new_page.course_id.ok_or_else(|| {
84        ControllerError::new(
85            ControllerErrorType::BadRequest,
86            "Cannot create a new page without a course id".to_string(),
87            None,
88        )
89    })?;
90    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
91    domain::authorization::ensure_user_can_add_exercise_tasks(
92        &mut conn,
93        user.id,
94        None,
95        &new_page.exercise_tasks,
96    )
97    .await?;
98
99    let page = models::pages::create_for_course_id(
100        &mut conn,
101        course_id,
102        new_page,
103        user.id,
104        models_requests::make_spec_fetcher(
105            app_conf.base_url.clone(),
106            request_id.0,
107            Arc::clone(&jwt_key),
108        ),
109        models_requests::fetch_service_info,
110    )
111    .await?;
112    token.authorized_ok(web::Json(page))
113}
114
115/**
116DELETE `/api/v0/main-frontend/pages/:page_id` - Delete a page, related exercises, and related exercise tasks by id.
117
118
119# Example
120
121Request: `DELETE /api/v0/main-frontend/pages/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02`
122*/
123#[instrument(skip(pool))]
124#[utoipa::path(
125    delete,
126    path = "/{page_id}",
127    operation_id = "deletePage",
128    tag = "pages",
129    params(
130        ("page_id" = Uuid, Path, description = "Page id")
131    ),
132    responses(
133        (status = 200, description = "Deleted page", body = Page)
134    )
135)]
136async fn delete_page(
137    page_id: web::Path<Uuid>,
138    pool: web::Data<PgPool>,
139    user: AuthUser,
140) -> ControllerResult<web::Json<Page>> {
141    let mut conn = pool.acquire().await?;
142    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
143    let deleted_page = models::pages::delete_page_and_exercises(
144        &mut conn,
145        *page_id,
146        models::entity_history::HistoryAuthor::user(user.id),
147    )
148    .await?;
149
150    token.authorized_ok(web::Json(deleted_page))
151}
152
153/**
154GET /api/v0/main-frontend/pages/:page_id/history
155*/
156#[instrument(skip(pool))]
157#[utoipa::path(
158    get,
159    path = "/{page_id}/history",
160    operation_id = "getPageHistory",
161    tag = "pages",
162    params(
163        ("page_id" = Uuid, Path, description = "Page id"),
164        ("page" = Option<i64>, Query, description = "Page number"),
165        ("limit" = Option<i64>, Query, description = "Page size")
166    ),
167    responses(
168        (status = 200, description = "Page history entries", body = Vec<PageHistory>)
169    )
170)]
171async fn history(
172    pool: web::Data<PgPool>,
173    page_id: web::Path<Uuid>,
174    pagination: web::Query<Pagination>,
175    user: AuthUser,
176) -> ControllerResult<web::Json<Vec<PageHistory>>> {
177    let mut conn = pool.acquire().await?;
178    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Page(*page_id)).await?;
179
180    let res = models::page_history::history(&mut conn, *page_id, *pagination).await?;
181
182    token.authorized_ok(web::Json(res))
183}
184
185/**
186GET /api/v0/main-frontend/pages/:page_id/history_count
187*/
188#[instrument(skip(pool))]
189#[utoipa::path(
190    get,
191    path = "/{page_id}/history_count",
192    operation_id = "getPageHistoryCount",
193    tag = "pages",
194    params(
195        ("page_id" = Uuid, Path, description = "Page id")
196    ),
197    responses(
198        (status = 200, description = "Page history count", body = i64)
199    )
200)]
201async fn history_count(
202    pool: web::Data<PgPool>,
203    page_id: web::Path<Uuid>,
204    user: AuthUser,
205) -> ControllerResult<web::Json<i64>> {
206    let mut conn = pool.acquire().await?;
207    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Page(*page_id)).await?;
208    let res = models::page_history::history_count(&mut conn, *page_id).await?;
209
210    token.authorized_ok(web::Json(res))
211}
212
213/**
214POST /api/v0/main-frontend/pages/:page_id/restore
215*/
216
217#[instrument(skip(pool, app_conf))]
218#[utoipa::path(
219    post,
220    path = "/{page_id}/restore",
221    operation_id = "restorePageHistory",
222    tag = "pages",
223    params(
224        ("page_id" = Uuid, Path, description = "Page id")
225    ),
226    request_body = HistoryRestoreData,
227    responses(
228        (status = 200, description = "Restored history id", body = Uuid)
229    )
230)]
231async fn restore(
232    request_id: RequestId,
233    pool: web::Data<PgPool>,
234    page_id: web::Path<Uuid>,
235    restore_data: web::Json<HistoryRestoreData>,
236    app_conf: web::Data<ApplicationConfiguration>,
237    user: AuthUser,
238    jwt_key: web::Data<JwtKey>,
239) -> ControllerResult<web::Json<Uuid>> {
240    let mut conn = pool.acquire().await?;
241    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
242    let page = models::pages::get_page(&mut conn, *page_id).await?;
243    let (expected_course_id, expected_exam_id) =
244        match CourseOrExamId::from_course_and_exam_ids(page.course_id, page.exam_id)? {
245            CourseOrExamId::Course(course_id) => (Some(course_id), None),
246            CourseOrExamId::Exam(exam_id) => (None, Some(exam_id)),
247        };
248    let res = models::pages::restore_from_history_for_page_id(
249        &mut conn,
250        *page_id,
251        restore_data.history_id,
252        None,
253        expected_course_id,
254        expected_exam_id,
255        user.id,
256        models_requests::make_spec_fetcher(
257            app_conf.base_url.clone(),
258            request_id.0,
259            Arc::clone(&jwt_key),
260        ),
261        models_requests::fetch_service_info,
262    )
263    .await?;
264
265    token.authorized_ok(web::Json(res))
266}
267
268/**
269GET `/api/v0/main-fronted/pages/:page_id/info` - Get a pages's course id, course name, organization slug
270
271Request: `GET /api/v0/cms/pages/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02/info`
272*/
273#[utoipa::path(
274    get,
275    path = "/{page_id}/info",
276    operation_id = "getPageInfo",
277    tag = "pages",
278    params(
279        ("page_id" = Uuid, Path, description = "Page id")
280    ),
281    responses(
282        (status = 200, description = "Page info", body = PageInfo)
283    )
284)]
285async fn get_page_info(
286    page_id: web::Path<Uuid>,
287    pool: web::Data<PgPool>,
288    user: AuthUser,
289) -> ControllerResult<web::Json<PageInfo>> {
290    let mut conn = pool.acquire().await?;
291    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
292
293    let page_info = models::pages::get_page_info(&mut conn, *page_id).await?;
294
295    token.authorized_ok(web::Json(page_info))
296}
297
298/**
299POST `/api/v0/main-frontend/pages/:page_id/page-details` - Update pages title and url_path.
300*/
301#[instrument(skip(pool))]
302#[utoipa::path(
303    put,
304    path = "/{page_id}/page-details",
305    operation_id = "updatePageDetails",
306    tag = "pages",
307    params(
308        ("page_id" = Uuid, Path, description = "Page id")
309    ),
310    request_body = PageDetailsUpdate,
311    responses(
312        (status = 200, description = "Updated page details", body = bool)
313    )
314)]
315async fn update_page_details(
316    page_id: web::Path<Uuid>,
317    payload: web::Json<PageDetailsUpdate>,
318    pool: web::Data<PgPool>,
319    user: AuthUser,
320) -> ControllerResult<web::Json<bool>> {
321    let mut conn = pool.acquire().await?;
322
323    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
324
325    models::pages::update_page_details(&mut conn, *page_id, &payload).await?;
326    token.authorized_ok(web::Json(true))
327}
328
329/**
330GET `/api/v0/main-frontend/pages/:course_id/all-course-pages-for-course` - Get all pages of a course
331*/
332#[utoipa::path(
333    get,
334    path = "/{course_id}/all-course-pages-for-course",
335    operation_id = "getCoursePages",
336    tag = "pages",
337    params(
338        ("course_id" = Uuid, Path, description = "Course id")
339    ),
340    responses(
341        (status = 200, description = "Course pages", body = Vec<Page>)
342    )
343)]
344async fn get_all_pages_by_course_id(
345    course_id: web::Path<Uuid>,
346    pool: web::Data<PgPool>,
347    user: AuthUser,
348) -> ControllerResult<web::Json<Vec<Page>>> {
349    let mut conn = pool.acquire().await?;
350    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
351
352    let mut pages = models::pages::get_pages_by_course_id(&mut conn, *course_id).await?;
353
354    pages.sort_by_key(|a| a.order_number);
355
356    token.authorized_ok(web::Json(pages))
357}
358
359/**
360Add a route for each controller in this module.
361
362The name starts with an underline in order to appear before other functions in the module documentation.
363
364We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
365*/
366pub fn _add_routes(cfg: &mut ServiceConfig) {
367    cfg.route("", web::post().to(post_new_page))
368        .route("/{page_id}", web::delete().to(delete_page))
369        .route("/{page_id}/info", web::get().to(get_page_info))
370        .route(
371            "/{page_id}/page-details",
372            web::put().to(update_page_details),
373        )
374        .route("/{page_id}/history", web::get().to(history))
375        .route("/{page_id}/history_count", web::get().to(history_count))
376        .route("/{page_id}/restore", web::post().to(restore))
377        .route(
378            "/{course_id}/all-course-pages-for-course",
379            web::get().to(get_all_pages_by_course_id),
380        );
381}