1use crate::prelude::*;
4use utoipa::OpenApi;
5
6use headless_lms_models::chatbot_configurations::ChatbotConfiguration;
7use models::{
8 course_instances::CourseInstance,
9 courses::Course,
10 pages::{Page, PageVisibility},
11 partner_block::PartnersBlock,
12 peer_or_self_review_configs::{
13 self, CmsPeerOrSelfReviewConfigurationSave, VersionedCmsPeerOrSelfReviewConfiguration,
14 },
15 peer_or_self_review_questions::normalize_cms_peer_or_self_review_questions,
16};
17
18use crate::prelude::models::course_modules::CourseModule;
19use models::research_forms::{NewResearchForm, ResearchForm};
20
21#[derive(OpenApi)]
22#[openapi(paths(
23 add_media,
24 get_course_by_id,
25 get_course_default_peer_or_self_review_configuration,
26 put_course_default_peer_or_self_review_configuration,
27 get_all_pages,
28 upsert_course_research_form,
29 get_research_form_with_course_id,
30 get_course_modules,
31 get_course_instances,
32 post_partners_block,
33 get_partners_block,
34 delete_partners_block,
35 get_course_nondefault_chatbot_configurations
36))]
37pub(crate) struct CmsCoursesApiDoc;
38
39#[instrument(skip(pool))]
43#[utoipa::path(
44 get,
45 path = "/{course_id}",
46 operation_id = "getCmsCourse",
47 tag = "cms_courses",
48 params(
49 ("course_id" = Uuid, Path, description = "Course id")
50 ),
51 responses(
52 (status = 200, description = "Course", body = Course)
53 )
54)]
55async fn get_course_by_id(
56 path: web::Path<Uuid>,
57 pool: web::Data<PgPool>,
58 user: AuthUser,
59) -> ControllerResult<web::Json<Course>> {
60 let course_id = path.into_inner();
61 let mut conn = pool.acquire().await?;
62 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
63 let course = models::courses::get_course(&mut conn, course_id).await?;
64 token.authorized_ok(web::Json(course))
65}
66
67#[utoipa::path(
83 post,
84 path = "/{course_id}/upload",
85 operation_id = "uploadCmsCourseMedia",
86 tag = "cms_courses",
87 params(
88 ("course_id" = Uuid, Path, description = "Course id")
89 ),
90 request_body(
91 content = String,
92 content_type = "multipart/form-data"
93 ),
94 responses(
95 (status = 200, description = "Uploaded media result", body = UploadResult)
96 )
97)]
98#[instrument(skip(payload, request, pool, file_store, app_conf))]
99async fn add_media(
100 course_id: web::Path<Uuid>,
101 payload: Multipart,
102 request: HttpRequest,
103 pool: web::Data<PgPool>,
104 user: AuthUser,
105 file_store: web::Data<dyn FileStore>,
106 app_conf: web::Data<ApplicationConfiguration>,
107) -> ControllerResult<web::Json<UploadResult>> {
108 let mut conn = pool.acquire().await?;
109 let course = models::courses::get_course(&mut conn, *course_id).await?;
110 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course.id)).await?;
111
112 let media_path = upload_file_from_cms(
113 request.headers(),
114 payload,
115 MediaOwner::Course(course.id),
116 file_store.as_ref(),
117 &mut conn,
118 user,
119 )
120 .await?;
121 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
122
123 token.authorized_ok(web::Json(UploadResult { url: download_url }))
124}
125
126#[instrument(skip(pool))]
127#[utoipa::path(
128 get,
129 path = "/{course_id}/default-peer-review",
130 operation_id = "getCmsCourseDefaultPeerReview",
131 tag = "cms_courses",
132 params(
133 ("course_id" = Uuid, Path, description = "Course id")
134 ),
135 responses(
136 (status = 200, description = "Default peer review configuration", body = VersionedCmsPeerOrSelfReviewConfiguration)
137 )
138)]
139async fn get_course_default_peer_or_self_review_configuration(
140 course_id: web::Path<Uuid>,
141 user: AuthUser,
142 pool: web::Data<PgPool>,
143) -> ControllerResult<web::Json<VersionedCmsPeerOrSelfReviewConfiguration>> {
144 let mut conn = pool.acquire().await?;
145 let token = authorize(
146 &mut conn,
147 Act::Teach,
148 Some(user.id),
149 Res::Course(*course_id),
150 )
151 .await?;
152
153 let configuration = peer_or_self_review_configs::get_course_default_versioned_cms_peer_review(
154 &mut conn, *course_id,
155 )
156 .await?;
157
158 token.authorized_ok(web::Json(configuration))
159}
160
161#[instrument(skip(pool))]
162#[utoipa::path(
163 put,
164 path = "/{course_id}/default-peer-review",
165 operation_id = "updateCmsCourseDefaultPeerReview",
166 tag = "cms_courses",
167 params(
168 ("course_id" = Uuid, Path, description = "Course id")
169 ),
170 request_body = CmsPeerOrSelfReviewConfigurationSave,
171 responses(
172 (status = 200, description = "Updated default peer review configuration", body = VersionedCmsPeerOrSelfReviewConfiguration),
173 (status = 422, description = "`stale_version`: the configuration changed after the editor loaded it")
174 )
175)]
176async fn put_course_default_peer_or_self_review_configuration(
177 course_id: web::Path<Uuid>,
178 user: AuthUser,
179 pool: web::Data<PgPool>,
180 payload: web::Json<CmsPeerOrSelfReviewConfigurationSave>,
181) -> ControllerResult<web::Json<VersionedCmsPeerOrSelfReviewConfiguration>> {
182 let mut conn = pool.acquire().await?;
183 let token = authorize(
184 &mut conn,
185 Act::Teach,
186 Some(user.id),
187 Res::Course(*course_id),
188 )
189 .await?;
190 let CmsPeerOrSelfReviewConfigurationSave {
191 configuration: mut config,
192 expected_updated_at,
193 } = payload.0;
194 normalize_cms_peer_or_self_review_questions(&mut config.peer_or_self_review_questions);
195 let cms_peer_or_self_review_configuration = models::entity_history::save_recording(
196 &mut conn,
197 models::library::entity_snapshots::ContentEntityType::CourseDefaultReviewConfig,
198 config.peer_or_self_review_config.id,
199 models::entity_history::HistoryAuthor::user(user.id),
200 async |tx| {
201 peer_or_self_review_configs::upsert_for_course_id(
202 tx,
203 *course_id,
204 &config,
205 expected_updated_at,
206 )
207 .await
208 },
209 )
210 .await?;
211 token.authorized_ok(web::Json(cms_peer_or_self_review_configuration))
212}
213
214#[instrument(skip(pool))]
218#[utoipa::path(
219 get,
220 path = "/{course_id}/pages",
221 operation_id = "getCmsCoursePages",
222 tag = "cms_courses",
223 params(
224 ("course_id" = Uuid, Path, description = "Course id")
225 ),
226 responses(
227 (status = 200, description = "Pages for course", body = Vec<Page>)
228 )
229)]
230async fn get_all_pages(
231 course_id: web::Path<Uuid>,
232 pool: web::Data<PgPool>,
233 user: AuthUser,
234) -> ControllerResult<web::Json<Vec<Page>>> {
235 let mut conn = pool.acquire().await?;
236 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
237
238 let res = models::pages::get_all_by_course_id_and_visibility(
239 &mut conn,
240 *course_id,
241 PageVisibility::Any,
242 )
243 .await?;
244
245 token.authorized_ok(web::Json(res))
246}
247
248#[instrument(skip(pool, payload))]
253#[utoipa::path(
254 put,
255 path = "/{course_id}/research-consent-form",
256 operation_id = "upsertCmsCourseResearchForm",
257 tag = "cms_courses",
258 params(
259 ("course_id" = Uuid, Path, description = "Course id")
260 ),
261 request_body = NewResearchForm,
262 responses(
263 (status = 200, description = "Research form", body = ResearchForm)
264 )
265)]
266async fn upsert_course_research_form(
267 payload: web::Json<NewResearchForm>,
268 pool: web::Data<PgPool>,
269 course_id: web::Path<Uuid>,
270 user: AuthUser,
271) -> ControllerResult<web::Json<ResearchForm>> {
272 let mut conn = pool.acquire().await?;
273
274 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
275 let res = models::research_forms::save_research_form(
276 &mut conn,
277 *course_id,
278 payload.into_inner().content,
279 )
280 .await?;
281
282 token.authorized_ok(web::Json(res))
283}
284
285#[instrument(skip(pool))]
289#[utoipa::path(
290 get,
291 path = "/{course_id}/research-consent-form",
292 operation_id = "getCmsCourseResearchForm",
293 tag = "cms_courses",
294 params(
295 ("course_id" = Uuid, Path, description = "Course id")
296 ),
297 responses(
298 (status = 200, description = "Research form", body = Option<ResearchForm>)
299 )
300)]
301async fn get_research_form_with_course_id(
302 course_id: web::Path<Uuid>,
303 user: AuthUser,
304 pool: web::Data<PgPool>,
305) -> ControllerResult<web::Json<Option<ResearchForm>>> {
306 let mut conn = pool.acquire().await?;
307
308 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
309 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
310 .await
311 .optional()?;
312
313 token.authorized_ok(web::Json(res))
314}
315
316#[instrument(skip(pool))]
321#[utoipa::path(
322 get,
323 path = "/{course_id}/modules",
324 operation_id = "getCmsCourseModules",
325 tag = "cms_courses",
326 params(
327 ("course_id" = Uuid, Path, description = "Course id")
328 ),
329 responses(
330 (status = 200, description = "Course modules", body = Vec<CourseModule>)
331 )
332)]
333async fn get_course_modules(
334 course_id: web::Path<Uuid>,
335 user: AuthUser,
336 pool: web::Data<PgPool>,
337) -> ControllerResult<web::Json<Vec<CourseModule>>> {
338 let mut conn = pool.acquire().await?;
339 let course_modules = models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
340 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
341 token.authorized_ok(web::Json(course_modules))
342}
343
344#[instrument(skip(pool))]
348#[utoipa::path(
349 get,
350 path = "/{course_id}/course-instances",
351 operation_id = "getCmsCourseInstances",
352 tag = "cms_courses",
353 params(
354 ("course_id" = Uuid, Path, description = "Course id")
355 ),
356 responses(
357 (status = 200, description = "Course instances", body = Vec<CourseInstance>)
358 )
359)]
360async fn get_course_instances(
361 course_id: web::Path<Uuid>,
362 user: AuthUser,
363 pool: web::Data<PgPool>,
364) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
365 let mut conn = pool.acquire().await?;
366 let instances =
367 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
368 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
369 token.authorized_ok(web::Json(instances))
370}
371
372#[instrument(skip(payload, pool))]
376#[utoipa::path(
377 post,
378 path = "/{course_id}/partners-block",
379 operation_id = "upsertCmsCoursePartnersBlock",
380 tag = "cms_courses",
381 params(
382 ("course_id" = Uuid, Path, description = "Course id")
383 ),
384 request_body = Option<serde_json::Value>,
385 responses(
386 (status = 200, description = "Partners block upserted")
387 )
388)]
389async fn post_partners_block(
390 path: web::Path<Uuid>,
391 payload: web::Json<Option<serde_json::Value>>,
392 pool: web::Data<PgPool>,
393 user: AuthUser,
394) -> ControllerResult<web::Json<()>> {
395 let course_id = path.into_inner();
396
397 let content = payload.into_inner();
398 let mut conn = pool.acquire().await?;
399 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
400
401 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
402
403 token.authorized_ok(web::Json(()))
404}
405
406#[instrument(skip(pool))]
410#[utoipa::path(
411 get,
412 path = "/{course_id}/partners-block",
413 operation_id = "getCmsCoursePartnersBlock",
414 tag = "cms_courses",
415 params(
416 ("course_id" = Uuid, Path, description = "Course id")
417 ),
418 responses(
419 (status = 200, description = "Partners block", body = PartnersBlock)
420 )
421)]
422async fn get_partners_block(
423 path: web::Path<Uuid>,
424 user: AuthUser,
425 pool: web::Data<PgPool>,
426) -> ControllerResult<web::Json<PartnersBlock>> {
427 let course_id = path.into_inner();
428 let mut conn = pool.acquire().await?;
429 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
430
431 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
433
434 let partner_block = if course_exists {
435 models::partner_block::get_partner_block(&mut conn, course_id).await?
437 } else {
438 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
440
441 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
443 };
444
445 token.authorized_ok(web::Json(partner_block))
446}
447
448#[instrument(skip(pool))]
452#[utoipa::path(
453 delete,
454 path = "/{course_id}/partners-block",
455 operation_id = "deleteCmsCoursePartnersBlock",
456 tag = "cms_courses",
457 params(
458 ("course_id" = Uuid, Path, description = "Course id")
459 ),
460 responses(
461 (status = 200, description = "Deleted partners block", body = PartnersBlock)
462 )
463)]
464async fn delete_partners_block(
465 path: web::Path<Uuid>,
466 pool: web::Data<PgPool>,
467 user: AuthUser,
468) -> ControllerResult<web::Json<PartnersBlock>> {
469 let course_id = path.into_inner();
470 let mut conn = pool.acquire().await?;
471 let token = authorize(
472 &mut conn,
473 Act::UsuallyUnacceptableDeletion,
474 Some(user.id),
475 Res::Course(course_id),
476 )
477 .await?;
478 let deleted_partners_block =
479 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
480
481 token.authorized_ok(web::Json(deleted_partners_block))
482}
483
484#[instrument(skip(pool))]
488#[utoipa::path(
489 get,
490 path = "/{course_id}/nondefault-chatbot-configurations",
491 operation_id = "getCmsCourseNondefaultChatbotConfigurations",
492 tag = "cms_courses",
493 params(
494 ("course_id" = Uuid, Path, description = "Course id")
495 ),
496 responses(
497 (status = 200, description = "Chatbot configurations", body = Vec<ChatbotConfiguration>)
498 )
499)]
500async fn get_course_nondefault_chatbot_configurations(
501 path: web::Path<Uuid>,
502 pool: web::Data<PgPool>,
503 user: AuthUser,
504) -> ControllerResult<web::Json<Vec<ChatbotConfiguration>>> {
505 let course_id = path.into_inner();
506 let mut conn = pool.acquire().await?;
507 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
508 let course_chatbot_configurations =
509 models::chatbot_configurations::get_enabled_nondefault_for_course(&mut conn, course_id)
510 .await?;
511 token.authorized_ok(web::Json(course_chatbot_configurations))
512}
513
514pub fn _add_routes(cfg: &mut ServiceConfig) {
522 cfg.route("/{course_id}", web::get().to(get_course_by_id))
523 .route("/{course_id}/upload", web::post().to(add_media))
524 .route(
525 "/{course_id}/default-peer-review",
526 web::get().to(get_course_default_peer_or_self_review_configuration),
527 )
528 .route(
529 "/{course_id}/default-peer-review",
530 web::put().to(put_course_default_peer_or_self_review_configuration),
531 )
532 .route("/{course_id}/pages", web::get().to(get_all_pages))
533 .route(
534 "/{course_id}/research-consent-form",
535 web::get().to(get_research_form_with_course_id),
536 )
537 .route(
538 "/{course_id}/research-consent-form",
539 web::put().to(upsert_course_research_form),
540 )
541 .route(
542 "/{course_id}/partners-block",
543 web::post().to(post_partners_block),
544 )
545 .route(
546 "/{course_id}/partners-block",
547 web::get().to(get_partners_block),
548 )
549 .route(
550 "/{course_id}/partners-block",
551 web::delete().to(delete_partners_block),
552 )
553 .route("/{course_id}/modules", web::get().to(get_course_modules))
554 .route(
555 "/{course_id}/course-instances",
556 web::get().to(get_course_instances),
557 )
558 .route(
559 "/{course_id}/nondefault-chatbot-configurations",
560 web::get().to(get_course_nondefault_chatbot_configurations),
561 );
562}