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_course_default_cms_peer_review_and_questions(
205 &mut conn, &config,
206 )
207 .await?;
208 token.authorized_ok(web::Json(cms_peer_or_self_review_configuration))
209}
210
211#[instrument(skip(pool))]
215#[utoipa::path(
216 get,
217 path = "/{course_id}/pages",
218 operation_id = "getCmsCoursePages",
219 tag = "cms_courses",
220 params(
221 ("course_id" = Uuid, Path, description = "Course id")
222 ),
223 responses(
224 (status = 200, description = "Pages for course", body = Vec<Page>)
225 )
226)]
227async fn get_all_pages(
228 course_id: web::Path<Uuid>,
229 pool: web::Data<PgPool>,
230 user: AuthUser,
231) -> ControllerResult<web::Json<Vec<Page>>> {
232 let mut conn = pool.acquire().await?;
233 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
234
235 let res = models::pages::get_all_by_course_id_and_visibility(
236 &mut conn,
237 *course_id,
238 PageVisibility::Any,
239 )
240 .await?;
241
242 token.authorized_ok(web::Json(res))
243}
244
245#[instrument(skip(pool, payload))]
250#[utoipa::path(
251 put,
252 path = "/{course_id}/research-consent-form",
253 operation_id = "upsertCmsCourseResearchForm",
254 tag = "cms_courses",
255 params(
256 ("course_id" = Uuid, Path, description = "Course id")
257 ),
258 request_body = NewResearchForm,
259 responses(
260 (status = 200, description = "Research form", body = ResearchForm)
261 )
262)]
263async fn upsert_course_research_form(
264 payload: web::Json<NewResearchForm>,
265 pool: web::Data<PgPool>,
266 course_id: web::Path<Uuid>,
267 user: AuthUser,
268) -> ControllerResult<web::Json<ResearchForm>> {
269 let mut conn = pool.acquire().await?;
270
271 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
272 let new_research_form = payload;
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 res = models::research_forms::upsert_research_form_questions(&mut conn, &payload).await?;
343
344 token.authorized_ok(web::Json(res))
345}
346
347#[instrument(skip(pool))]
352#[utoipa::path(
353 get,
354 path = "/{course_id}/modules",
355 operation_id = "getCmsCourseModules",
356 tag = "cms_courses",
357 params(
358 ("course_id" = Uuid, Path, description = "Course id")
359 ),
360 responses(
361 (status = 200, description = "Course modules", body = Vec<CourseModule>)
362 )
363)]
364async fn get_course_modules(
365 course_id: web::Path<Uuid>,
366 user: AuthUser,
367 pool: web::Data<PgPool>,
368) -> ControllerResult<web::Json<Vec<CourseModule>>> {
369 let mut conn = pool.acquire().await?;
370 let course_modules = models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
371 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
372 token.authorized_ok(web::Json(course_modules))
373}
374
375#[instrument(skip(pool))]
379#[utoipa::path(
380 get,
381 path = "/{course_id}/course-instances",
382 operation_id = "getCmsCourseInstances",
383 tag = "cms_courses",
384 params(
385 ("course_id" = Uuid, Path, description = "Course id")
386 ),
387 responses(
388 (status = 200, description = "Course instances", body = Vec<CourseInstance>)
389 )
390)]
391async fn get_course_instances(
392 course_id: web::Path<Uuid>,
393 user: AuthUser,
394 pool: web::Data<PgPool>,
395) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
396 let mut conn = pool.acquire().await?;
397 let instances =
398 models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
399 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
400 token.authorized_ok(web::Json(instances))
401}
402
403#[instrument(skip(payload, pool))]
407#[utoipa::path(
408 post,
409 path = "/{course_id}/partners-block",
410 operation_id = "upsertCmsCoursePartnersBlock",
411 tag = "cms_courses",
412 params(
413 ("course_id" = Uuid, Path, description = "Course id")
414 ),
415 request_body = Option<serde_json::Value>,
416 responses(
417 (status = 200, description = "Partners block upserted")
418 )
419)]
420async fn post_partners_block(
421 path: web::Path<Uuid>,
422 payload: web::Json<Option<serde_json::Value>>,
423 pool: web::Data<PgPool>,
424 user: AuthUser,
425) -> ControllerResult<web::Json<()>> {
426 let course_id = path.into_inner();
427
428 let content = payload.into_inner();
429 let mut conn = pool.acquire().await?;
430 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
431
432 models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
433
434 token.authorized_ok(web::Json(()))
435}
436
437#[instrument(skip(pool))]
441#[utoipa::path(
442 get,
443 path = "/{course_id}/partners-block",
444 operation_id = "getCmsCoursePartnersBlock",
445 tag = "cms_courses",
446 params(
447 ("course_id" = Uuid, Path, description = "Course id")
448 ),
449 responses(
450 (status = 200, description = "Partners block", body = PartnersBlock)
451 )
452)]
453async fn get_partners_block(
454 path: web::Path<Uuid>,
455 user: AuthUser,
456 pool: web::Data<PgPool>,
457) -> ControllerResult<web::Json<PartnersBlock>> {
458 let course_id = path.into_inner();
459 let mut conn = pool.acquire().await?;
460 let token = skip_authorize();
461
462 let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
464
465 let partner_block = if course_exists {
466 models::partner_block::get_partner_block(&mut conn, course_id).await?
468 } else {
469 let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
471
472 models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
474 };
475
476 token.authorized_ok(web::Json(partner_block))
477}
478
479#[instrument(skip(pool))]
483#[utoipa::path(
484 delete,
485 path = "/{course_id}/partners-block",
486 operation_id = "deleteCmsCoursePartnersBlock",
487 tag = "cms_courses",
488 params(
489 ("course_id" = Uuid, Path, description = "Course id")
490 ),
491 responses(
492 (status = 200, description = "Deleted partners block", body = PartnersBlock)
493 )
494)]
495async fn delete_partners_block(
496 path: web::Path<Uuid>,
497 pool: web::Data<PgPool>,
498 user: AuthUser,
499) -> ControllerResult<web::Json<PartnersBlock>> {
500 let course_id = path.into_inner();
501 let mut conn = pool.acquire().await?;
502 let token = authorize(
503 &mut conn,
504 Act::UsuallyUnacceptableDeletion,
505 Some(user.id),
506 Res::Course(course_id),
507 )
508 .await?;
509 let deleted_partners_block =
510 models::partner_block::delete_partner_block(&mut conn, course_id).await?;
511
512 token.authorized_ok(web::Json(deleted_partners_block))
513}
514
515#[instrument(skip(pool))]
519#[utoipa::path(
520 get,
521 path = "/{course_id}/nondefault-chatbot-configurations",
522 operation_id = "getCmsCourseNondefaultChatbotConfigurations",
523 tag = "cms_courses",
524 params(
525 ("course_id" = Uuid, Path, description = "Course id")
526 ),
527 responses(
528 (status = 200, description = "Chatbot configurations", body = Vec<ChatbotConfiguration>)
529 )
530)]
531async fn get_course_nondefault_chatbot_configurations(
532 path: web::Path<Uuid>,
533 pool: web::Data<PgPool>,
534 user: AuthUser,
535) -> ControllerResult<web::Json<Vec<ChatbotConfiguration>>> {
536 let course_id = path.into_inner();
537 let mut conn = pool.acquire().await?;
538 let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
539 let course_chatbot_configurations =
540 models::chatbot_configurations::get_enabled_nondefault_for_course(&mut conn, course_id)
541 .await?;
542 token.authorized_ok(web::Json(course_chatbot_configurations))
543}
544
545pub fn _add_routes(cfg: &mut ServiceConfig) {
553 cfg.route("/{course_id}", web::get().to(get_course_by_id))
554 .route("/{course_id}/upload", web::post().to(add_media))
555 .route(
556 "/{course_id}/default-peer-review",
557 web::get().to(get_course_default_peer_or_self_review_configuration),
558 )
559 .route(
560 "/{course_id}/default-peer-review",
561 web::put().to(put_course_default_peer_or_self_review_configuration),
562 )
563 .route("/{course_id}/pages", web::get().to(get_all_pages))
564 .route(
565 "/{courseId}/research-consent-form-questions",
566 web::put().to(upsert_course_research_form_questions),
567 )
568 .route(
569 "/{course_id}/research-consent-form",
570 web::get().to(get_research_form_with_course_id),
571 )
572 .route(
573 "/{course_id}/research-consent-form",
574 web::put().to(upsert_course_research_form),
575 )
576 .route(
577 "/{course_id}/partners-block",
578 web::post().to(post_partners_block),
579 )
580 .route(
581 "/{course_id}/partners-block",
582 web::get().to(get_partners_block),
583 )
584 .route(
585 "/{course_id}/partners-block",
586 web::delete().to(delete_partners_block),
587 )
588 .route("/{course_id}/modules", web::get().to(get_course_modules))
589 .route(
590 "/{course_id}/course-instances",
591 web::get().to(get_course_instances),
592 )
593 .route(
594 "/{course_id}/nondefault-chatbot-configurations",
595 web::get().to(get_course_nondefault_chatbot_configurations),
596 );
597}