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 = 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/**
281GET `/api/v0/cms/courses/:course_id/research-consent-form` - Fetches courses research form with course id.
282*/
283#[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/**
312PUT `/api/v0/cms/courses/:course_id/research-consent-form-questions` - Upserts questions for the courses research form from Gutenberg research form edit.
313*/
314
315#[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/**
345GET `/api/v0/cms/courses/:course_id/modules`
346Returns modules in the course.
347*/
348#[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/**
373GET `/api/v0/cms/courses/:course_id/course-instances` - Returns all course instances for given course id.
374*/
375#[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/**
401 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
402*/
403#[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/**
435GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
436*/
437#[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    // Check if the course exists in the partners_blocks table
460    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
461
462    let partner_block = if course_exists {
463        // If the course exists, fetch the partner block
464        models::partner_block::get_partner_block(&mut conn, course_id).await?
465    } else {
466        // If the course does not exist, create a new partner block with an empty content array
467        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
468
469        // Upsert the partner block with the empty content
470        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/**
477DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
478*/
479#[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/**
513GET /api/v0/cms/courses/:course_id/nondefault-chatbot-configurations - Get all nondefault, enabled-to-students chatbot configurations of this course.
514*/
515#[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
542/**
543Add a route for each controller in this module.
544
545The name starts with an underline in order to appear before other functions in the module documentation.
546
547We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
548*/
549pub 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}