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 = payload;
270 let res = models::research_forms::upsert_research_form(
271 &mut conn,
272 PKeyPolicy::Generate,
273 &new_research_form,
274 )
275 .await?;
276
277 token.authorized_ok(web::Json(res))
278}
279
280#[instrument(skip(pool))]
284#[utoipa::path(
285 get,
286 path = "/{course_id}/research-consent-form",
287 operation_id = "getCmsCourseResearchForm",
288 tag = "cms_courses",
289 params(
290 ("course_id" = Uuid, Path, description = "Course id")
291 ),
292 responses(
293 (status = 200, description = "Research form", body = Option<ResearchForm>)
294 )
295)]
296async fn get_research_form_with_course_id(
297 course_id: web::Path<Uuid>,
298 user: AuthUser,
299 pool: web::Data<PgPool>,
300) -> ControllerResult<web::Json<Option<ResearchForm>>> {
301 let mut conn = pool.acquire().await?;
302
303 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
304 let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
305 .await
306 .optional()?;
307
308 token.authorized_ok(web::Json(res))
309}
310
311#[instrument(skip(pool, payload))]
316#[utoipa::path(
317 put,
318 path = "/{course_id}/research-consent-form-questions",
319 operation_id = "upsertCmsCourseResearchFormQuestions",
320 tag = "cms_courses",
321 params(
322 ("course_id" = Uuid, Path, description = "Course id")
323 ),
324 request_body = Vec<NewResearchFormQuestion>,
325 responses(
326 (status = 200, description = "Research form questions", body = Vec<ResearchFormQuestion>)
327 )
328)]
329async fn upsert_course_research_form_questions(
330 payload: web::Json<Vec<NewResearchFormQuestion>>,
331 pool: web::Data<PgPool>,
332 course_id: web::Path<Uuid>,
333 user: AuthUser,
334) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
335 let mut conn = pool.acquire().await?;
336
337 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
338
339 let res = models::research_forms::upsert_research_form_questions(&mut conn, &payload).await?;
340
341 token.authorized_ok(web::Json(res))
342}
343
344#[instrument(skip(pool))]
349#[utoipa::path(
350 get,
351 path = "/{course_id}/modules",
352 operation_id = "getCmsCourseModules",
353 tag = "cms_courses",
354 params(
355 ("course_id" = Uuid, Path, description = "Course id")
356 ),
357 responses(
358 (status = 200, description = "Course modules", body = Vec<CourseModule>)
359 )
360)]
361async fn get_course_modules(
362 course_id: web::Path<Uuid>,
363 user: AuthUser,
364 pool: web::Data<PgPool>,
365) -> ControllerResult<web::Json<Vec<CourseModule>>> {
366 let mut conn = pool.acquire().await?;
367 let course_modules = models::course_modules::get_by_course_id(&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(course_modules))
370}
371
372#[instrument(skip(pool))]
376#[utoipa::path(
377 get,
378 path = "/{course_id}/course-instances",
379 operation_id = "getCmsCourseInstances",
380 tag = "cms_courses",
381 params(
382 ("course_id" = Uuid, Path, description = "Course id")
383 ),
384 responses(
385 (status = 200, description = "Course instances", body = Vec<CourseInstance>)
386 )
387)]
388async fn get_course_instances(
389 course_id: web::Path<Uuid>,
390 user: AuthUser,
391 pool: web::Data<PgPool>,
392) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
393 let mut conn = pool.acquire().await?;
394 let instances =
395 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
396 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
397 token.authorized_ok(web::Json(instances))
398}
399
400#[instrument(skip(payload, pool))]
404#[utoipa::path(
405 post,
406 path = "/{course_id}/partners-block",
407 operation_id = "upsertCmsCoursePartnersBlock",
408 tag = "cms_courses",
409 params(
410 ("course_id" = Uuid, Path, description = "Course id")
411 ),
412 request_body = Option<serde_json::Value>,
413 responses(
414 (status = 200, description = "Partners block upserted")
415 )
416)]
417async fn post_partners_block(
418 path: web::Path<Uuid>,
419 payload: web::Json<Option<serde_json::Value>>,
420 pool: web::Data<PgPool>,
421 user: AuthUser,
422) -> ControllerResult<web::Json<()>> {
423 let course_id = path.into_inner();
424
425 let content = payload.into_inner();
426 let mut conn = pool.acquire().await?;
427 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
428
429 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
430
431 token.authorized_ok(web::Json(()))
432}
433
434#[instrument(skip(pool))]
438#[utoipa::path(
439 get,
440 path = "/{course_id}/partners-block",
441 operation_id = "getCmsCoursePartnersBlock",
442 tag = "cms_courses",
443 params(
444 ("course_id" = Uuid, Path, description = "Course id")
445 ),
446 responses(
447 (status = 200, description = "Partners block", body = PartnersBlock)
448 )
449)]
450async fn get_partners_block(
451 path: web::Path<Uuid>,
452 user: AuthUser,
453 pool: web::Data<PgPool>,
454) -> ControllerResult<web::Json<PartnersBlock>> {
455 let course_id = path.into_inner();
456 let mut conn = pool.acquire().await?;
457 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
458
459 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
461
462 let partner_block = if course_exists {
463 models::partner_block::get_partner_block(&mut conn, course_id).await?
465 } else {
466 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
468
469 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
471 };
472
473 token.authorized_ok(web::Json(partner_block))
474}
475
476#[instrument(skip(pool))]
480#[utoipa::path(
481 delete,
482 path = "/{course_id}/partners-block",
483 operation_id = "deleteCmsCoursePartnersBlock",
484 tag = "cms_courses",
485 params(
486 ("course_id" = Uuid, Path, description = "Course id")
487 ),
488 responses(
489 (status = 200, description = "Deleted partners block", body = PartnersBlock)
490 )
491)]
492async fn delete_partners_block(
493 path: web::Path<Uuid>,
494 pool: web::Data<PgPool>,
495 user: AuthUser,
496) -> ControllerResult<web::Json<PartnersBlock>> {
497 let course_id = path.into_inner();
498 let mut conn = pool.acquire().await?;
499 let token = authorize(
500 &mut conn,
501 Act::UsuallyUnacceptableDeletion,
502 Some(user.id),
503 Res::Course(course_id),
504 )
505 .await?;
506 let deleted_partners_block =
507 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
508
509 token.authorized_ok(web::Json(deleted_partners_block))
510}
511
512#[instrument(skip(pool))]
516#[utoipa::path(
517 get,
518 path = "/{course_id}/nondefault-chatbot-configurations",
519 operation_id = "getCmsCourseNondefaultChatbotConfigurations",
520 tag = "cms_courses",
521 params(
522 ("course_id" = Uuid, Path, description = "Course id")
523 ),
524 responses(
525 (status = 200, description = "Chatbot configurations", body = Vec<ChatbotConfiguration>)
526 )
527)]
528async fn get_course_nondefault_chatbot_configurations(
529 path: web::Path<Uuid>,
530 pool: web::Data<PgPool>,
531 user: AuthUser,
532) -> ControllerResult<web::Json<Vec<ChatbotConfiguration>>> {
533 let course_id = path.into_inner();
534 let mut conn = pool.acquire().await?;
535 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
536 let course_chatbot_configurations =
537 models::chatbot_configurations::get_enabled_nondefault_for_course(&mut conn, course_id)
538 .await?;
539 token.authorized_ok(web::Json(course_chatbot_configurations))
540}
541
542pub fn _add_routes(cfg: &mut ServiceConfig) {
550 cfg.route("/{course_id}", web::get().to(get_course_by_id))
551 .route("/{course_id}/upload", web::post().to(add_media))
552 .route(
553 "/{course_id}/default-peer-review",
554 web::get().to(get_course_default_peer_or_self_review_configuration),
555 )
556 .route(
557 "/{course_id}/default-peer-review",
558 web::put().to(put_course_default_peer_or_self_review_configuration),
559 )
560 .route("/{course_id}/pages", web::get().to(get_all_pages))
561 .route(
562 "/{courseId}/research-consent-form-questions",
563 web::put().to(upsert_course_research_form_questions),
564 )
565 .route(
566 "/{course_id}/research-consent-form",
567 web::get().to(get_research_form_with_course_id),
568 )
569 .route(
570 "/{course_id}/research-consent-form",
571 web::put().to(upsert_course_research_form),
572 )
573 .route(
574 "/{course_id}/partners-block",
575 web::post().to(post_partners_block),
576 )
577 .route(
578 "/{course_id}/partners-block",
579 web::get().to(get_partners_block),
580 )
581 .route(
582 "/{course_id}/partners-block",
583 web::delete().to(delete_partners_block),
584 )
585 .route("/{course_id}/modules", web::get().to(get_course_modules))
586 .route(
587 "/{course_id}/course-instances",
588 web::get().to(get_course_instances),
589 )
590 .route(
591 "/{course_id}/nondefault-chatbot-configurations",
592 web::get().to(get_course_nondefault_chatbot_configurations),
593 );
594}