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::{self, CmsPeerOrSelfReviewConfiguration},
13 peer_or_self_review_questions::normalize_cms_peer_or_self_review_questions,
14};
15
16use crate::prelude::models::course_modules::CourseModule;
17use models::research_forms::{
18 NewResearchForm, NewResearchFormQuestion, ResearchForm, ResearchFormQuestion,
19};
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 upsert_course_research_form_questions,
31 get_course_modules,
32 get_course_instances,
33 post_partners_block,
34 get_partners_block,
35 delete_partners_block,
36 get_course_nondefault_chatbot_configurations
37))]
38pub(crate) struct CmsCoursesApiDoc;
39
40#[instrument(skip(pool))]
44#[utoipa::path(
45 get,
46 path = "/{course_id}",
47 operation_id = "getCmsCourse",
48 tag = "cms_courses",
49 params(
50 ("course_id" = Uuid, Path, description = "Course id")
51 ),
52 responses(
53 (status = 200, description = "Course", body = Course)
54 )
55)]
56async fn get_course_by_id(
57 path: web::Path<Uuid>,
58 pool: web::Data<PgPool>,
59 user: AuthUser,
60) -> ControllerResult<web::Json<Course>> {
61 let course_id = path.into_inner();
62 let mut conn = pool.acquire().await?;
63 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
64 let course = models::courses::get_course(&mut conn, course_id).await?;
65 token.authorized_ok(web::Json(course))
66}
67
68#[utoipa::path(
84 post,
85 path = "/{course_id}/upload",
86 operation_id = "uploadCmsCourseMedia",
87 tag = "cms_courses",
88 params(
89 ("course_id" = Uuid, Path, description = "Course id")
90 ),
91 request_body(
92 content = String,
93 content_type = "multipart/form-data"
94 ),
95 responses(
96 (status = 200, description = "Uploaded media result", body = UploadResult)
97 )
98)]
99#[instrument(skip(payload, request, pool, file_store, app_conf))]
100async fn add_media(
101 course_id: web::Path<Uuid>,
102 payload: Multipart,
103 request: HttpRequest,
104 pool: web::Data<PgPool>,
105 user: AuthUser,
106 file_store: web::Data<dyn FileStore>,
107 app_conf: web::Data<ApplicationConfiguration>,
108) -> ControllerResult<web::Json<UploadResult>> {
109 let mut conn = pool.acquire().await?;
110 let course = models::courses::get_course(&mut conn, *course_id).await?;
111 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course.id)).await?;
112
113 let media_path = upload_file_from_cms(
114 request.headers(),
115 payload,
116 StoreKind::Course(course.id),
117 file_store.as_ref(),
118 &mut conn,
119 user,
120 )
121 .await?;
122 let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
123
124 token.authorized_ok(web::Json(UploadResult { url: download_url }))
125}
126
127#[instrument(skip(pool))]
128#[utoipa::path(
129 get,
130 path = "/{course_id}/default-peer-review",
131 operation_id = "getCmsCourseDefaultPeerReview",
132 tag = "cms_courses",
133 params(
134 ("course_id" = Uuid, Path, description = "Course id")
135 ),
136 responses(
137 (status = 200, description = "Default peer review configuration", body = CmsPeerOrSelfReviewConfiguration)
138 )
139)]
140async fn get_course_default_peer_or_self_review_configuration(
141 course_id: web::Path<Uuid>,
142 user: AuthUser,
143 pool: web::Data<PgPool>,
144) -> ControllerResult<web::Json<CmsPeerOrSelfReviewConfiguration>> {
145 let mut conn = pool.acquire().await?;
146 let token = authorize(
147 &mut conn,
148 Act::Teach,
149 Some(user.id),
150 Res::Course(*course_id),
151 )
152 .await?;
153
154 let peer_or_self_review_config =
155 models::peer_or_self_review_configs::get_course_default_cms_peer_review(
156 &mut conn, *course_id,
157 )
158 .await?;
159
160 let peer_or_self_review_questions =
161 models::peer_or_self_review_questions::get_course_default_cms_peer_or_self_review_questions(
162 &mut conn,
163 peer_or_self_review_config.id,
164 )
165 .await?;
166
167 token.authorized_ok(web::Json(CmsPeerOrSelfReviewConfiguration {
168 peer_or_self_review_config,
169 peer_or_self_review_questions,
170 }))
171}
172
173#[instrument(skip(pool))]
174#[utoipa::path(
175 put,
176 path = "/{course_id}/default-peer-review",
177 operation_id = "updateCmsCourseDefaultPeerReview",
178 tag = "cms_courses",
179 params(
180 ("course_id" = Uuid, Path, description = "Course id")
181 ),
182 request_body = CmsPeerOrSelfReviewConfiguration,
183 responses(
184 (status = 200, description = "Updated default peer review configuration", body = CmsPeerOrSelfReviewConfiguration)
185 )
186)]
187async fn put_course_default_peer_or_self_review_configuration(
188 course_id: web::Path<Uuid>,
189 user: AuthUser,
190 pool: web::Data<PgPool>,
191 payload: web::Json<CmsPeerOrSelfReviewConfiguration>,
192) -> ControllerResult<web::Json<CmsPeerOrSelfReviewConfiguration>> {
193 let mut conn = pool.acquire().await?;
194 let token = authorize(
195 &mut conn,
196 Act::Teach,
197 Some(user.id),
198 Res::Course(*course_id),
199 )
200 .await?;
201 let mut config = payload.0;
202 normalize_cms_peer_or_self_review_questions(&mut config.peer_or_self_review_questions);
203 let cms_peer_or_self_review_configuration =
204 peer_or_self_review_configs::upsert_for_course_id(&mut conn, *course_id, &config).await?;
205 token.authorized_ok(web::Json(cms_peer_or_self_review_configuration))
206}
207
208#[instrument(skip(pool))]
212#[utoipa::path(
213 get,
214 path = "/{course_id}/pages",
215 operation_id = "getCmsCoursePages",
216 tag = "cms_courses",
217 params(
218 ("course_id" = Uuid, Path, description = "Course id")
219 ),
220 responses(
221 (status = 200, description = "Pages for course", body = Vec<Page>)
222 )
223)]
224async fn get_all_pages(
225 course_id: web::Path<Uuid>,
226 pool: web::Data<PgPool>,
227 user: AuthUser,
228) -> ControllerResult<web::Json<Vec<Page>>> {
229 let mut conn = pool.acquire().await?;
230 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
231
232 let res = models::pages::get_all_by_course_id_and_visibility(
233 &mut conn,
234 *course_id,
235 PageVisibility::Any,
236 )
237 .await?;
238
239 token.authorized_ok(web::Json(res))
240}
241
242#[instrument(skip(pool, payload))]
247#[utoipa::path(
248 put,
249 path = "/{course_id}/research-consent-form",
250 operation_id = "upsertCmsCourseResearchForm",
251 tag = "cms_courses",
252 params(
253 ("course_id" = Uuid, Path, description = "Course id")
254 ),
255 request_body = NewResearchForm,
256 responses(
257 (status = 200, description = "Research form", body = ResearchForm)
258 )
259)]
260async fn upsert_course_research_form(
261 payload: web::Json<NewResearchForm>,
262 pool: web::Data<PgPool>,
263 course_id: web::Path<Uuid>,
264 user: AuthUser,
265) -> ControllerResult<web::Json<ResearchForm>> {
266 let mut conn = pool.acquire().await?;
267
268 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
269 let new_research_form = NewResearchForm {
270 course_id: *course_id,
271 ..payload.into_inner()
272 };
273 let res = models::research_forms::upsert_research_form(
274 &mut conn,
275 PKeyPolicy::Generate,
276 &new_research_form,
277 )
278 .await?;
279
280 token.authorized_ok(web::Json(res))
281}
282
283#[instrument(skip(pool))]
287#[utoipa::path(
288 get,
289 path = "/{course_id}/research-consent-form",
290 operation_id = "getCmsCourseResearchForm",
291 tag = "cms_courses",
292 params(
293 ("course_id" = Uuid, Path, description = "Course id")
294 ),
295 responses(
296 (status = 200, description = "Research form", body = Option<ResearchForm>)
297 )
298)]
299async fn get_research_form_with_course_id(
300 course_id: web::Path<Uuid>,
301 user: AuthUser,
302 pool: web::Data<PgPool>,
303) -> ControllerResult<web::Json<Option<ResearchForm>>> {
304 let mut conn = pool.acquire().await?;
305
306 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
307 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
308 .await
309 .optional()?;
310
311 token.authorized_ok(web::Json(res))
312}
313
314#[instrument(skip(pool, payload))]
319#[utoipa::path(
320 put,
321 path = "/{course_id}/research-consent-form-questions",
322 operation_id = "upsertCmsCourseResearchFormQuestions",
323 tag = "cms_courses",
324 params(
325 ("course_id" = Uuid, Path, description = "Course id")
326 ),
327 request_body = Vec<NewResearchFormQuestion>,
328 responses(
329 (status = 200, description = "Research form questions", body = Vec<ResearchFormQuestion>)
330 )
331)]
332async fn upsert_course_research_form_questions(
333 payload: web::Json<Vec<NewResearchFormQuestion>>,
334 pool: web::Data<PgPool>,
335 course_id: web::Path<Uuid>,
336 user: AuthUser,
337) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
338 let mut conn = pool.acquire().await?;
339
340 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
341
342 let mut questions = payload.into_inner();
343 if !questions.is_empty() {
344 let research_form =
345 models::research_forms::get_research_form_with_course_id(&mut conn, *course_id).await?;
346 for question in &mut questions {
347 question.course_id = *course_id;
348 question.research_consent_form_id = research_form.id;
349 }
350 }
351
352 let res = models::research_forms::upsert_research_form_questions(&mut conn, &questions).await?;
353
354 token.authorized_ok(web::Json(res))
355}
356
357#[instrument(skip(pool))]
362#[utoipa::path(
363 get,
364 path = "/{course_id}/modules",
365 operation_id = "getCmsCourseModules",
366 tag = "cms_courses",
367 params(
368 ("course_id" = Uuid, Path, description = "Course id")
369 ),
370 responses(
371 (status = 200, description = "Course modules", body = Vec<CourseModule>)
372 )
373)]
374async fn get_course_modules(
375 course_id: web::Path<Uuid>,
376 user: AuthUser,
377 pool: web::Data<PgPool>,
378) -> ControllerResult<web::Json<Vec<CourseModule>>> {
379 let mut conn = pool.acquire().await?;
380 let course_modules = models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
381 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
382 token.authorized_ok(web::Json(course_modules))
383}
384
385#[instrument(skip(pool))]
389#[utoipa::path(
390 get,
391 path = "/{course_id}/course-instances",
392 operation_id = "getCmsCourseInstances",
393 tag = "cms_courses",
394 params(
395 ("course_id" = Uuid, Path, description = "Course id")
396 ),
397 responses(
398 (status = 200, description = "Course instances", body = Vec<CourseInstance>)
399 )
400)]
401async fn get_course_instances(
402 course_id: web::Path<Uuid>,
403 user: AuthUser,
404 pool: web::Data<PgPool>,
405) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
406 let mut conn = pool.acquire().await?;
407 let instances =
408 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
409 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
410 token.authorized_ok(web::Json(instances))
411}
412
413#[instrument(skip(payload, pool))]
417#[utoipa::path(
418 post,
419 path = "/{course_id}/partners-block",
420 operation_id = "upsertCmsCoursePartnersBlock",
421 tag = "cms_courses",
422 params(
423 ("course_id" = Uuid, Path, description = "Course id")
424 ),
425 request_body = Option<serde_json::Value>,
426 responses(
427 (status = 200, description = "Partners block upserted")
428 )
429)]
430async fn post_partners_block(
431 path: web::Path<Uuid>,
432 payload: web::Json<Option<serde_json::Value>>,
433 pool: web::Data<PgPool>,
434 user: AuthUser,
435) -> ControllerResult<web::Json<()>> {
436 let course_id = path.into_inner();
437
438 let content = payload.into_inner();
439 let mut conn = pool.acquire().await?;
440 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
441
442 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
443
444 token.authorized_ok(web::Json(()))
445}
446
447#[instrument(skip(pool))]
451#[utoipa::path(
452 get,
453 path = "/{course_id}/partners-block",
454 operation_id = "getCmsCoursePartnersBlock",
455 tag = "cms_courses",
456 params(
457 ("course_id" = Uuid, Path, description = "Course id")
458 ),
459 responses(
460 (status = 200, description = "Partners block", body = PartnersBlock)
461 )
462)]
463async fn get_partners_block(
464 path: web::Path<Uuid>,
465 user: AuthUser,
466 pool: web::Data<PgPool>,
467) -> ControllerResult<web::Json<PartnersBlock>> {
468 let course_id = path.into_inner();
469 let mut conn = pool.acquire().await?;
470 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
471
472 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
474
475 let partner_block = if course_exists {
476 models::partner_block::get_partner_block(&mut conn, course_id).await?
478 } else {
479 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
481
482 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
484 };
485
486 token.authorized_ok(web::Json(partner_block))
487}
488
489#[instrument(skip(pool))]
493#[utoipa::path(
494 delete,
495 path = "/{course_id}/partners-block",
496 operation_id = "deleteCmsCoursePartnersBlock",
497 tag = "cms_courses",
498 params(
499 ("course_id" = Uuid, Path, description = "Course id")
500 ),
501 responses(
502 (status = 200, description = "Deleted partners block", body = PartnersBlock)
503 )
504)]
505async fn delete_partners_block(
506 path: web::Path<Uuid>,
507 pool: web::Data<PgPool>,
508 user: AuthUser,
509) -> ControllerResult<web::Json<PartnersBlock>> {
510 let course_id = path.into_inner();
511 let mut conn = pool.acquire().await?;
512 let token = authorize(
513 &mut conn,
514 Act::UsuallyUnacceptableDeletion,
515 Some(user.id),
516 Res::Course(course_id),
517 )
518 .await?;
519 let deleted_partners_block =
520 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
521
522 token.authorized_ok(web::Json(deleted_partners_block))
523}
524
525#[instrument(skip(pool))]
529#[utoipa::path(
530 get,
531 path = "/{course_id}/nondefault-chatbot-configurations",
532 operation_id = "getCmsCourseNondefaultChatbotConfigurations",
533 tag = "cms_courses",
534 params(
535 ("course_id" = Uuid, Path, description = "Course id")
536 ),
537 responses(
538 (status = 200, description = "Chatbot configurations", body = Vec<ChatbotConfiguration>)
539 )
540)]
541async fn get_course_nondefault_chatbot_configurations(
542 path: web::Path<Uuid>,
543 pool: web::Data<PgPool>,
544 user: AuthUser,
545) -> ControllerResult<web::Json<Vec<ChatbotConfiguration>>> {
546 let course_id = path.into_inner();
547 let mut conn = pool.acquire().await?;
548 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
549 let course_chatbot_configurations =
550 models::chatbot_configurations::get_enabled_nondefault_for_course(&mut conn, course_id)
551 .await?;
552 token.authorized_ok(web::Json(course_chatbot_configurations))
553}
554
555pub fn _add_routes(cfg: &mut ServiceConfig) {
563 cfg.route("/{course_id}", web::get().to(get_course_by_id))
564 .route("/{course_id}/upload", web::post().to(add_media))
565 .route(
566 "/{course_id}/default-peer-review",
567 web::get().to(get_course_default_peer_or_self_review_configuration),
568 )
569 .route(
570 "/{course_id}/default-peer-review",
571 web::put().to(put_course_default_peer_or_self_review_configuration),
572 )
573 .route("/{course_id}/pages", web::get().to(get_all_pages))
574 .route(
575 "/{courseId}/research-consent-form-questions",
576 web::put().to(upsert_course_research_form_questions),
577 )
578 .route(
579 "/{course_id}/research-consent-form",
580 web::get().to(get_research_form_with_course_id),
581 )
582 .route(
583 "/{course_id}/research-consent-form",
584 web::put().to(upsert_course_research_form),
585 )
586 .route(
587 "/{course_id}/partners-block",
588 web::post().to(post_partners_block),
589 )
590 .route(
591 "/{course_id}/partners-block",
592 web::get().to(get_partners_block),
593 )
594 .route(
595 "/{course_id}/partners-block",
596 web::delete().to(delete_partners_block),
597 )
598 .route("/{course_id}/modules", web::get().to(get_course_modules))
599 .route(
600 "/{course_id}/course-instances",
601 web::get().to(get_course_instances),
602 )
603 .route(
604 "/{course_id}/nondefault-chatbot-configurations",
605 web::get().to(get_course_nondefault_chatbot_configurations),
606 );
607}