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_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/**
212GET `/api/v0/cms/courses/:course_id/pages` - Gets all pages for a course.
213*/
214#[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/**
246PUT `/api/v0/cms/courses/:course_id/research-consent-form` - Upserts courses research form from Gutenberg research form edit.
247*/
248
249#[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/**
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 res = models::research_forms::upsert_research_form_questions(&mut conn, &payload).await?;
343
344    token.authorized_ok(web::Json(res))
345}
346
347/**
348GET `/api/v0/cms/courses/:course_id/modules`
349Returns modules in the course.
350*/
351#[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/**
376GET `/api/v0/cms/courses/:course_id/course-instances` - Returns all course instances for given course id.
377*/
378#[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/**
404 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
405*/
406#[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/**
438GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
439*/
440#[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    // Check if the course exists in the partners_blocks table
463    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
464
465    let partner_block = if course_exists {
466        // If the course exists, fetch the partner block
467        models::partner_block::get_partner_block(&mut conn, course_id).await?
468    } else {
469        // If the course does not exist, create a new partner block with an empty content array
470        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
471
472        // Upsert the partner block with the empty content
473        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/**
480DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
481*/
482#[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/**
516GET /api/v0/cms/courses/:course_id/nondefault-chatbot-configurations - Get all nondefault, enabled-to-students chatbot configurations of this course.
517*/
518#[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
545/**
546Add a route for each controller in this module.
547
548The name starts with an underline in order to appear before other functions in the module documentation.
549
550We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
551*/
552pub 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}