Skip to main content

headless_lms_server/controllers/cms/
pages.rs

1//! Controllers for requests starting with `/api/v0/cms/pages`.
2
3use models::{
4    CourseOrExamId,
5    page_history::HistoryChangeReason,
6    pages::{
7        CmsPageUpdate, ContentManagementPage, PageInfo, PageNavigationInformation, PageUpdateArgs,
8        PageVisibility,
9    },
10};
11
12use crate::{
13    domain::{
14        models_requests::{self, JwtKey},
15        request_id::RequestId,
16    },
17    prelude::*,
18};
19use utoipa::OpenApi;
20
21#[derive(OpenApi)]
22#[openapi(paths(
23    get_page,
24    get_page_info,
25    update_page,
26    get_page_navigation,
27    get_exercises_with_submissions
28))]
29pub(crate) struct CmsPagesApiDoc;
30
31/**
32GET `/api/v0/cms/pages/:page_id` - Get a page with exercises and exercise tasks by id.
33
34Request: `GET /api/v0/cms/pages/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02`
35*/
36#[instrument(skip(pool))]
37#[utoipa::path(
38    get,
39    path = "/{page_id}",
40    operation_id = "getCmsPage",
41    tag = "cms_pages",
42    params(
43        ("page_id" = Uuid, Path, description = "Page id")
44    ),
45    responses(
46        (status = 200, description = "CMS page with exercises and peer review data", body = ContentManagementPage)
47    )
48)]
49async fn get_page(
50    page_id: web::Path<Uuid>,
51    pool: web::Data<PgPool>,
52    user: AuthUser,
53) -> ControllerResult<web::Json<ContentManagementPage>> {
54    let mut conn = pool.acquire().await?;
55    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
56
57    let cms_page = models::pages::get_page_with_exercises(&mut conn, *page_id).await?;
58    token.authorized_ok(web::Json(cms_page))
59}
60
61/**
62GET `/api/v0/cms/pages/:page_id/info` - Get a pages's course id, course name, organization slug
63
64Request: `GET /api/v0/cms/pages/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02/info`
65*/
66#[utoipa::path(
67    get,
68    path = "/{page_id}/info",
69    operation_id = "getCmsPageInfo",
70    tag = "cms_pages",
71    params(
72        ("page_id" = Uuid, Path, description = "Page id")
73    ),
74    responses(
75        (status = 200, description = "Page info", body = PageInfo)
76    )
77)]
78async fn get_page_info(
79    page_id: web::Path<Uuid>,
80    pool: web::Data<PgPool>,
81    user: AuthUser,
82) -> ControllerResult<web::Json<PageInfo>> {
83    let mut conn = pool.acquire().await?;
84    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
85
86    let cms_page_info = models::pages::get_page_info(&mut conn, *page_id).await?;
87    token.authorized_ok(web::Json(cms_page_info))
88}
89
90/**
91PUT `/api/v0/cms/pages/:page_id` - Update a page by id.
92
93Please 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.
94
95If optional property front_page_of_chapter_id is set, this page will become the front page of the specified course part.
96
97# Example: OUTDATED
98
99Request:
100
101```http
102PUT /api/v0/cms/pages/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02 HTTP/1.1
103Content-Type: application/json
104
105{
106  "content": [{"type": "x"}],
107  "url_path": "/part-1/hello-world",
108  "title": "Hello world!",
109  "chapter_id": "2495ffa3-7ea9-4615-baa5-828023688c79"
110}
111```
112*/
113
114#[instrument(skip(pool, app_conf))]
115#[utoipa::path(
116    put,
117    path = "/{page_id}",
118    operation_id = "updateCmsPage",
119    tag = "cms_pages",
120    params(
121        ("page_id" = Uuid, Path, description = "Page id")
122    ),
123    request_body = CmsPageUpdate,
124    responses(
125        (status = 200, description = "Updated CMS page", body = ContentManagementPage)
126    )
127)]
128async fn update_page(
129    request_id: RequestId,
130    payload: web::Json<CmsPageUpdate>,
131    page_id: web::Path<Uuid>,
132    pool: web::Data<PgPool>,
133    jwt_key: web::Data<JwtKey>,
134    app_conf: web::Data<ApplicationConfiguration>,
135    user: AuthUser,
136) -> ControllerResult<web::Json<ContentManagementPage>> {
137    let mut conn = pool.acquire().await?;
138    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
139
140    let cms_page_update = payload.0;
141    let course_or_exam_id = models::pages::get_course_and_exam_id(&mut conn, *page_id).await?;
142    let is_exam_page = matches!(course_or_exam_id, CourseOrExamId::Exam(_));
143    let (expected_course_id, expected_exam_id) = match course_or_exam_id {
144        CourseOrExamId::Course(course_id) => (Some(course_id), None),
145        CourseOrExamId::Exam(exam_id) => (None, Some(exam_id)),
146    };
147    let saved = models::pages::update_by_id_in_parent_context(
148        &mut conn,
149        PageUpdateArgs {
150            page_id: *page_id,
151            author: user.id,
152            cms_page_update,
153            retain_ids: false,
154            history_change_reason: HistoryChangeReason::PageSaved,
155            is_exam_page,
156        },
157        expected_course_id,
158        expected_exam_id,
159        models_requests::make_spec_fetcher(
160            app_conf.base_url.clone(),
161            request_id.0,
162            jwt_key.into_inner(),
163        ),
164        models_requests::fetch_service_info,
165    )
166    .await?;
167    token.authorized_ok(web::Json(saved))
168}
169
170/**
171POST `/api/v0/cms/pages/:page_id/exercises-with-submissions` - Given a set of exercise ids, returns
172the subset that has at least one existing submission. Used by the editor to warn the teacher before
173saving a page edit that would remove one of these exercises, since that soft-deletes it and orphans
174its submissions.
175*/
176#[instrument(skip(pool))]
177#[utoipa::path(
178    post,
179    path = "/{page_id}/exercises-with-submissions",
180    operation_id = "getExercisesWithSubmissions",
181    tag = "cms_pages",
182    params(
183        ("page_id" = Uuid, Path, description = "Page id")
184    ),
185    request_body = Vec<Uuid>,
186    responses(
187        (status = 200, description = "Exercise ids, among the given ones, that have submissions", body = Vec<Uuid>)
188    )
189)]
190async fn get_exercises_with_submissions(
191    exercise_ids: web::Json<Vec<Uuid>>,
192    page_id: web::Path<Uuid>,
193    pool: web::Data<PgPool>,
194    user: AuthUser,
195) -> ControllerResult<web::Json<Vec<Uuid>>> {
196    let mut conn = pool.acquire().await?;
197    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
198
199    let exercise_ids_with_submissions =
200        models::exercise_slide_submissions::get_exercise_ids_with_any_submissions(
201            &mut conn,
202            *page_id,
203            &exercise_ids.0,
204        )
205        .await?;
206
207    token.authorized_ok(web::Json(exercise_ids_with_submissions))
208}
209
210/**
211GET /api/v0/cms/pages/:page_id/page-navigation - tells what's the next page, previous page, and the chapter front page given a page id.
212*/
213#[instrument(skip(pool))]
214#[utoipa::path(
215    get,
216    path = "/{page_id}/page-navigation",
217    operation_id = "getCmsPageNavigation",
218    tag = "cms_pages",
219    params(
220        ("page_id" = Uuid, Path, description = "Page id")
221    ),
222    responses(
223        (status = 200, description = "Page navigation", body = PageNavigationInformation)
224    )
225)]
226async fn get_page_navigation(
227    page_id: web::Path<Uuid>,
228    pool: web::Data<PgPool>,
229) -> ControllerResult<web::Json<PageNavigationInformation>> {
230    let mut conn = pool.acquire().await?;
231    let token = skip_authorize();
232    let res =
233        models::pages::get_page_navigation_data(&mut conn, *page_id, PageVisibility::Any).await?;
234
235    token.authorized_ok(web::Json(res))
236}
237/**
238Add a route for each controller in this module.
239
240The name starts with an underline in order to appear before other functions in the module documentation.
241
242We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
243*/
244pub fn _add_routes(cfg: &mut ServiceConfig) {
245    cfg.route("/{page_id}", web::get().to(get_page))
246        .route("/{page_id}/info", web::get().to(get_page_info))
247        .route(
248            "/{page_id}/page-navigation",
249            web::get().to(get_page_navigation),
250        )
251        .route("/{page_id}", web::put().to(update_page))
252        .route(
253            "/{page_id}/exercises-with-submissions",
254            web::post().to(get_exercises_with_submissions),
255        );
256}