1use models::{
4 CourseOrExamId,
5 agent_changesets::{self, AgentChangeset},
6 page_history::HistoryChangeReason,
7 pages::{
8 CmsPageSave, ContentManagementPage, PageInfo, PageNavigationInformation, PageUpdateArgs,
9 PageVisibility,
10 },
11};
12
13use crate::{
14 domain::{
15 models_requests::{self, JwtKey},
16 request_id::RequestId,
17 },
18 prelude::*,
19};
20use utoipa::OpenApi;
21
22#[derive(OpenApi)]
23#[openapi(paths(
24 get_page,
25 get_page_info,
26 update_page,
27 get_page_navigation,
28 get_exercises_with_submissions,
29 get_pending_agent_changes
30))]
31pub(crate) struct CmsPagesApiDoc;
32
33#[instrument(skip(pool))]
39#[utoipa::path(
40 get,
41 path = "/{page_id}",
42 operation_id = "getCmsPage",
43 tag = "cms_pages",
44 params(
45 ("page_id" = Uuid, Path, description = "Page id")
46 ),
47 responses(
48 (status = 200, description = "CMS page with exercises and peer review data", body = ContentManagementPage)
49 )
50)]
51async fn get_page(
52 page_id: web::Path<Uuid>,
53 pool: web::Data<PgPool>,
54 user: AuthUser,
55) -> ControllerResult<web::Json<ContentManagementPage>> {
56 let mut conn = pool.acquire().await?;
57 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
58
59 let cms_page = models::pages::get_page_with_exercises(&mut conn, *page_id).await?;
60 token.authorized_ok(web::Json(cms_page))
61}
62
63#[utoipa::path(
69 get,
70 path = "/{page_id}/info",
71 operation_id = "getCmsPageInfo",
72 tag = "cms_pages",
73 params(
74 ("page_id" = Uuid, Path, description = "Page id")
75 ),
76 responses(
77 (status = 200, description = "Page info", body = PageInfo)
78 )
79)]
80async fn get_page_info(
81 page_id: web::Path<Uuid>,
82 pool: web::Data<PgPool>,
83 user: AuthUser,
84) -> ControllerResult<web::Json<PageInfo>> {
85 let mut conn = pool.acquire().await?;
86 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
87
88 let cms_page_info = models::pages::get_page_info(&mut conn, *page_id).await?;
89 token.authorized_ok(web::Json(cms_page_info))
90}
91
92#[instrument(skip(pool, app_conf))]
117#[utoipa::path(
118 put,
119 path = "/{page_id}",
120 operation_id = "updateCmsPage",
121 tag = "cms_pages",
122 params(
123 ("page_id" = Uuid, Path, description = "Page id")
124 ),
125 request_body = CmsPageSave,
126 responses(
127 (status = 200, description = "Updated CMS page", body = ContentManagementPage),
128 (status = 422, description = "`stale_version`: the page changed after the editor loaded it")
129 )
130)]
131async fn update_page(
132 request_id: RequestId,
133 payload: web::Json<CmsPageSave>,
134 page_id: web::Path<Uuid>,
135 pool: web::Data<PgPool>,
136 jwt_key: web::Data<JwtKey>,
137 app_conf: web::Data<ApplicationConfiguration>,
138 user: AuthUser,
139) -> ControllerResult<web::Json<ContentManagementPage>> {
140 let mut conn = pool.acquire().await?;
141 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
142
143 let CmsPageSave {
144 page: cms_page_update,
145 expected_updated_at,
146 } = payload.0;
147 domain::authorization::ensure_user_can_add_exercise_tasks(
148 &mut conn,
149 user.id,
150 Some(*page_id),
151 &cms_page_update.exercise_tasks,
152 )
153 .await?;
154 let course_or_exam_id = models::pages::get_course_and_exam_id(&mut conn, *page_id).await?;
155 let is_exam_page = matches!(course_or_exam_id, CourseOrExamId::Exam(_));
156 let (expected_course_id, expected_exam_id) = match course_or_exam_id {
157 CourseOrExamId::Course(course_id) => (Some(course_id), None),
158 CourseOrExamId::Exam(exam_id) => (None, Some(exam_id)),
159 };
160 let saved = models::pages::update_by_id_in_parent_context(
161 &mut conn,
162 PageUpdateArgs {
163 page_id: *page_id,
164 author: models::entity_history::HistoryAuthor::user(user.id),
165 cms_page_update,
166 retain_ids: false,
167 history_change_reason: HistoryChangeReason::PageSaved,
168 is_exam_page,
169 },
170 expected_course_id,
171 expected_exam_id,
172 expected_updated_at,
173 models_requests::make_spec_fetcher(
174 app_conf.base_url.clone(),
175 request_id.0,
176 jwt_key.into_inner(),
177 ),
178 models_requests::fetch_service_info,
179 )
180 .await?;
181 token.authorized_ok(web::Json(saved))
182}
183
184#[instrument(skip(pool))]
191#[utoipa::path(
192 post,
193 path = "/{page_id}/exercises-with-submissions",
194 operation_id = "getExercisesWithSubmissions",
195 tag = "cms_pages",
196 params(
197 ("page_id" = Uuid, Path, description = "Page id")
198 ),
199 request_body = Vec<Uuid>,
200 responses(
201 (status = 200, description = "Exercise ids, among the given ones, that have submissions", body = Vec<Uuid>)
202 )
203)]
204async fn get_exercises_with_submissions(
205 exercise_ids: web::Json<Vec<Uuid>>,
206 page_id: web::Path<Uuid>,
207 pool: web::Data<PgPool>,
208 user: AuthUser,
209) -> ControllerResult<web::Json<Vec<Uuid>>> {
210 let mut conn = pool.acquire().await?;
211 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
212
213 let exercise_ids_with_submissions =
214 models::exercise_slide_submissions::get_exercise_ids_with_any_submissions(
215 &mut conn,
216 *page_id,
217 &exercise_ids.0,
218 )
219 .await?;
220
221 token.authorized_ok(web::Json(exercise_ids_with_submissions))
222}
223
224#[instrument(skip(pool))]
228#[utoipa::path(
229 get,
230 path = "/{page_id}/page-navigation",
231 operation_id = "getCmsPageNavigation",
232 tag = "cms_pages",
233 params(
234 ("page_id" = Uuid, Path, description = "Page id")
235 ),
236 responses(
237 (status = 200, description = "Page navigation", body = PageNavigationInformation)
238 )
239)]
240async fn get_page_navigation(
241 page_id: web::Path<Uuid>,
242 pool: web::Data<PgPool>,
243) -> ControllerResult<web::Json<PageNavigationInformation>> {
244 let mut conn = pool.acquire().await?;
245 let token = skip_authorize();
246 let res =
247 models::pages::get_page_navigation_data(&mut conn, *page_id, PageVisibility::Any).await?;
248
249 token.authorized_ok(web::Json(res))
250}
251
252#[instrument(skip(pool))]
257#[utoipa::path(
258 get,
259 path = "/{page_id}/pending-agent-changes",
260 operation_id = "getCmsPagePendingAgentChanges",
261 tag = "cms_pages",
262 params(
263 ("page_id" = Uuid, Path, description = "Page id")
264 ),
265 responses(
266 (status = 200, description = "The open changeset changing the page, if any", body = Option<AgentChangeset>)
267 )
268)]
269async fn get_pending_agent_changes(
270 page_id: web::Path<Uuid>,
271 pool: web::Data<PgPool>,
272 user: AuthUser,
273) -> ControllerResult<web::Json<Option<AgentChangeset>>> {
274 let mut conn = pool.acquire().await?;
275 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
276 let changeset = agent_changesets::get_open_changing_page(&mut conn, user.id, *page_id).await?;
277 token.authorized_ok(web::Json(changeset))
278}
279
280pub fn _add_routes(cfg: &mut ServiceConfig) {
288 cfg.route("/{page_id}", web::get().to(get_page))
289 .route("/{page_id}/info", web::get().to(get_page_info))
290 .route(
291 "/{page_id}/page-navigation",
292 web::get().to(get_page_navigation),
293 )
294 .route("/{page_id}", web::put().to(update_page))
295 .route(
296 "/{page_id}/exercises-with-submissions",
297 web::post().to(get_exercises_with_submissions),
298 )
299 .route(
300 "/{page_id}/pending-agent-changes",
301 web::get().to(get_pending_agent_changes),
302 );
303}