Skip to main content

headless_lms_server/controllers/cms/
courses.rs

1//! Controllers for requests starting with `/api/v0/cms/courses`.
2
3use 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/**
41GET /api/v0/cms/courses/:course_id - Get the course.
42*/
43#[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/**
69POST `/api/v0/cms/courses/:course_id/upload` - Uploads a media (image, audio, file) for the course from Gutenberg page edit.
70
71Put the the contents of the media in a form and add a content type header multipart/form-data.
72# Example
73
74Request:
75```http
76POST /api/v0/cms/pages/d86cf910-4d26-40e9-8c9c-1cc35294fdbb/upload HTTP/1.1
77Content-Type: multipart/form-data
78
79BINARY_DATA
80```
81*/
82
83#[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/**
209GET `/api/v0/cms/courses/:course_id/pages` - Gets all pages for a course.
210*/
211#[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/**
243PUT `/api/v0/cms/courses/:course_id/research-consent-form` - Upserts courses research form from Gutenberg research form edit.
244*/
245
246#[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/**
284GET `/api/v0/cms/courses/:course_id/research-consent-form` - Fetches courses research form with course id.
285*/
286#[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/**
315PUT `/api/v0/cms/courses/:course_id/research-consent-form-questions` - Upserts questions for the courses research form from Gutenberg research form edit.
316*/
317
318#[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/**
358GET `/api/v0/cms/courses/:course_id/modules`
359Returns modules in the course.
360*/
361#[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/**
386GET `/api/v0/cms/courses/:course_id/course-instances` - Returns all course instances for given course id.
387*/
388#[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/**
414 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
415*/
416#[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/**
448GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
449*/
450#[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    // Check if the course exists in the partners_blocks table
473    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
474
475    let partner_block = if course_exists {
476        // If the course exists, fetch the partner block
477        models::partner_block::get_partner_block(&mut conn, course_id).await?
478    } else {
479        // If the course does not exist, create a new partner block with an empty content array
480        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
481
482        // Upsert the partner block with the empty content
483        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/**
490DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
491*/
492#[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/**
526GET /api/v0/cms/courses/:course_id/nondefault-chatbot-configurations - Get all nondefault, enabled-to-students chatbot configurations of this course.
527*/
528#[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
555/**
556Add a route for each controller in this module.
557
558The name starts with an underline in order to appear before other functions in the module documentation.
559
560We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
561*/
562pub 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}