headless_lms_server/controllers/cms/
courses.rs

1//! Controllers for requests starting with `/api/v0/cms/courses`.
2
3use crate::prelude::*;
4
5use headless_lms_models::chatbot_configurations::ChatbotConfiguration;
6use models::{
7    course_instances::CourseInstance,
8    courses::Course,
9    pages::{Page, PageVisibility},
10    partner_block::PartnersBlock,
11    peer_or_self_review_configs::{self, CmsPeerOrSelfReviewConfiguration},
12    peer_or_self_review_questions::normalize_cms_peer_or_self_review_questions,
13};
14
15use crate::prelude::models::course_modules::CourseModule;
16use models::research_forms::{
17    NewResearchForm, NewResearchFormQuestion, ResearchForm, ResearchFormQuestion,
18};
19
20/**
21GET /api/v0/cms/courses/:course_id - Get the course.
22*/
23#[instrument(skip(pool))]
24async fn get_course_by_id(
25    path: web::Path<Uuid>,
26    pool: web::Data<PgPool>,
27    user: AuthUser,
28) -> ControllerResult<web::Json<Course>> {
29    let course_id = path.into_inner();
30    let mut conn = pool.acquire().await?;
31    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
32    let course = models::courses::get_course(&mut conn, course_id).await?;
33    token.authorized_ok(web::Json(course))
34}
35
36/**
37POST `/api/v0/cms/courses/:course_id/upload` - Uploads a media (image, audio, file) for the course from Gutenberg page edit.
38
39Put the the contents of the media in a form and add a content type header multipart/form-data.
40# Example
41
42Request:
43```http
44POST /api/v0/cms/pages/d86cf910-4d26-40e9-8c9c-1cc35294fdbb/upload HTTP/1.1
45Content-Type: multipart/form-data
46
47BINARY_DATA
48```
49*/
50
51#[instrument(skip(payload, request, pool, file_store, app_conf))]
52async fn add_media(
53    course_id: web::Path<Uuid>,
54    payload: Multipart,
55    request: HttpRequest,
56    pool: web::Data<PgPool>,
57    user: AuthUser,
58    file_store: web::Data<dyn FileStore>,
59    app_conf: web::Data<ApplicationConfiguration>,
60) -> ControllerResult<web::Json<UploadResult>> {
61    let mut conn = pool.acquire().await?;
62    let course = models::courses::get_course(&mut conn, *course_id).await?;
63    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course.id)).await?;
64
65    let media_path = upload_file_from_cms(
66        request.headers(),
67        payload,
68        StoreKind::Course(course.id),
69        file_store.as_ref(),
70        &mut conn,
71        user,
72    )
73    .await?;
74    let download_url = file_store.get_download_url(media_path.as_path(), app_conf.as_ref());
75
76    token.authorized_ok(web::Json(UploadResult { url: download_url }))
77}
78
79#[instrument(skip(pool))]
80async fn get_course_default_peer_or_self_review_configuration(
81    course_id: web::Path<Uuid>,
82    user: AuthUser,
83    pool: web::Data<PgPool>,
84) -> ControllerResult<web::Json<CmsPeerOrSelfReviewConfiguration>> {
85    let mut conn = pool.acquire().await?;
86    let token = authorize(
87        &mut conn,
88        Act::Teach,
89        Some(user.id),
90        Res::Course(*course_id),
91    )
92    .await?;
93
94    let peer_or_self_review_config =
95        models::peer_or_self_review_configs::get_course_default_cms_peer_review(
96            &mut conn, *course_id,
97        )
98        .await?;
99
100    let peer_or_self_review_questions =
101        models::peer_or_self_review_questions::get_course_default_cms_peer_or_self_review_questions(
102            &mut conn,
103            peer_or_self_review_config.id,
104        )
105        .await?;
106
107    token.authorized_ok(web::Json(CmsPeerOrSelfReviewConfiguration {
108        peer_or_self_review_config,
109        peer_or_self_review_questions,
110    }))
111}
112
113#[instrument(skip(pool))]
114async fn put_course_default_peer_or_self_review_configuration(
115    course_id: web::Path<Uuid>,
116    user: AuthUser,
117    pool: web::Data<PgPool>,
118    payload: web::Json<CmsPeerOrSelfReviewConfiguration>,
119) -> ControllerResult<web::Json<CmsPeerOrSelfReviewConfiguration>> {
120    let mut conn = pool.acquire().await?;
121    let token = authorize(
122        &mut conn,
123        Act::Teach,
124        Some(user.id),
125        Res::Course(*course_id),
126    )
127    .await?;
128    let mut config = payload.0;
129    normalize_cms_peer_or_self_review_questions(&mut config.peer_or_self_review_questions);
130    let cms_peer_or_self_review_configuration =
131        peer_or_self_review_configs::upsert_course_default_cms_peer_review_and_questions(
132            &mut conn, &config,
133        )
134        .await?;
135    token.authorized_ok(web::Json(cms_peer_or_self_review_configuration))
136}
137
138/**
139GET `/api/v0/cms/courses/:course_id/pages` - Gets all pages for a course.
140*/
141#[instrument(skip(pool))]
142async fn get_all_pages(
143    course_id: web::Path<Uuid>,
144    pool: web::Data<PgPool>,
145    user: AuthUser,
146) -> ControllerResult<web::Json<Vec<Page>>> {
147    let mut conn = pool.acquire().await?;
148    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
149
150    let res = models::pages::get_all_by_course_id_and_visibility(
151        &mut conn,
152        *course_id,
153        PageVisibility::Any,
154    )
155    .await?;
156
157    token.authorized_ok(web::Json(res))
158}
159
160/**
161PUT `/api/v0/cms/courses/:course_id/research-consent-form` - Upserts courses research form from Gutenberg research form edit.
162*/
163
164#[instrument(skip(pool, payload))]
165async fn upsert_course_research_form(
166    payload: web::Json<NewResearchForm>,
167    pool: web::Data<PgPool>,
168    course_id: web::Path<Uuid>,
169    user: AuthUser,
170) -> ControllerResult<web::Json<ResearchForm>> {
171    let mut conn = pool.acquire().await?;
172
173    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
174    let new_research_form = payload;
175    let res = models::research_forms::upsert_research_form(
176        &mut conn,
177        PKeyPolicy::Generate,
178        &new_research_form,
179    )
180    .await?;
181
182    token.authorized_ok(web::Json(res))
183}
184
185/**
186GET `/api/v0/cms/courses/:course_id/research-consent-form` - Fetches courses research form with course id.
187*/
188#[instrument(skip(pool))]
189async fn get_research_form_with_course_id(
190    course_id: web::Path<Uuid>,
191    user: AuthUser,
192    pool: web::Data<PgPool>,
193) -> ControllerResult<web::Json<Option<ResearchForm>>> {
194    let mut conn = pool.acquire().await?;
195
196    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
197    let res = models::research_forms::get_research_form_with_course_id(&mut conn, *course_id)
198        .await
199        .optional()?;
200
201    token.authorized_ok(web::Json(res))
202}
203
204/**
205PUT `/api/v0/cms/courses/:course_id/research-consent-form-questions` - Upserts questions for the courses research form from Gutenberg research form edit.
206*/
207
208#[instrument(skip(pool, payload))]
209async fn upsert_course_research_form_questions(
210    payload: web::Json<Vec<NewResearchFormQuestion>>,
211    pool: web::Data<PgPool>,
212    course_id: web::Path<Uuid>,
213    user: AuthUser,
214) -> ControllerResult<web::Json<Vec<ResearchFormQuestion>>> {
215    let mut conn = pool.acquire().await?;
216
217    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?;
218
219    let res = models::research_forms::upsert_research_form_questions(&mut conn, &payload).await?;
220
221    token.authorized_ok(web::Json(res))
222}
223
224/**
225GET `/api/v0/cms/courses/:course_id/modules`
226Returns modules in the course.
227*/
228#[instrument(skip(pool))]
229async fn get_course_modules(
230    course_id: web::Path<Uuid>,
231    user: AuthUser,
232    pool: web::Data<PgPool>,
233) -> ControllerResult<web::Json<Vec<CourseModule>>> {
234    let mut conn = pool.acquire().await?;
235    let course_modules = models::course_modules::get_by_course_id(&mut conn, *course_id).await?;
236    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
237    token.authorized_ok(web::Json(course_modules))
238}
239
240/**
241GET `/api/v0/cms/courses/:course_id/course-instances` - Returns all course instances for given course id.
242*/
243#[instrument(skip(pool))]
244async fn get_course_instances(
245    course_id: web::Path<Uuid>,
246    user: AuthUser,
247    pool: web::Data<PgPool>,
248) -> ControllerResult<web::Json<Vec<CourseInstance>>> {
249    let mut conn = pool.acquire().await?;
250    let instances =
251        models::course_instances::get_course_instances_for_course(&mut conn, *course_id).await?;
252    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
253    token.authorized_ok(web::Json(instances))
254}
255
256/**
257 POST /api/v0/main-frontend/courses/:course_id/partners_block - Create or updates a partners block for a course
258*/
259#[instrument(skip(payload, pool))]
260async fn post_partners_block(
261    path: web::Path<Uuid>,
262    payload: web::Json<Option<serde_json::Value>>,
263    pool: web::Data<PgPool>,
264    user: AuthUser,
265) -> ControllerResult<web::Json<()>> {
266    let course_id = path.into_inner();
267
268    let content = payload.into_inner();
269    let mut conn = pool.acquire().await?;
270    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
271
272    models::partner_block::upsert_partner_block(&mut conn, course_id, content).await?;
273
274    token.authorized_ok(web::Json(()))
275}
276
277/**
278GET /courses/:course_id/partners_blocks - Gets a partners block related to a course
279*/
280#[instrument(skip(pool))]
281async fn get_partners_block(
282    path: web::Path<Uuid>,
283    user: AuthUser,
284    pool: web::Data<PgPool>,
285) -> ControllerResult<web::Json<PartnersBlock>> {
286    let course_id = path.into_inner();
287    let mut conn = pool.acquire().await?;
288    let token = skip_authorize();
289
290    // Check if the course exists in the partners_blocks table
291    let course_exists = models::partner_block::check_if_course_exists(&mut conn, course_id).await?;
292
293    let partner_block = if course_exists {
294        // If the course exists, fetch the partner block
295        models::partner_block::get_partner_block(&mut conn, course_id).await?
296    } else {
297        // If the course does not exist, create a new partner block with an empty content array
298        let empty_content: Option<serde_json::Value> = Some(serde_json::Value::Array(vec![]));
299
300        // Upsert the partner block with the empty content
301        models::partner_block::upsert_partner_block(&mut conn, course_id, empty_content).await?
302    };
303
304    token.authorized_ok(web::Json(partner_block))
305}
306
307/**
308DELETE `/api/v0/main-frontend/courses/:course_id` - Delete a partners block in a course.
309*/
310#[instrument(skip(pool))]
311async fn delete_partners_block(
312    path: web::Path<Uuid>,
313    pool: web::Data<PgPool>,
314    user: AuthUser,
315) -> ControllerResult<web::Json<PartnersBlock>> {
316    let course_id = path.into_inner();
317    let mut conn = pool.acquire().await?;
318    let token = authorize(
319        &mut conn,
320        Act::UsuallyUnacceptableDeletion,
321        Some(user.id),
322        Res::Course(course_id),
323    )
324    .await?;
325    let deleted_partners_block =
326        models::partner_block::delete_partner_block(&mut conn, course_id).await?;
327
328    token.authorized_ok(web::Json(deleted_partners_block))
329}
330
331/**
332GET /api/v0/cms/courses/:course_id/nondefault-chatbot-configurations - Get all nondefault, enabled-to-students chatbot configurations of this course.
333*/
334#[instrument(skip(pool))]
335async fn get_course_nondefault_chatbot_configurations(
336    path: web::Path<Uuid>,
337    pool: web::Data<PgPool>,
338    user: AuthUser,
339) -> ControllerResult<web::Json<Vec<ChatbotConfiguration>>> {
340    let course_id = path.into_inner();
341    let mut conn = pool.acquire().await?;
342    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Course(course_id)).await?;
343    let course_chatbot_configurations =
344        models::chatbot_configurations::get_enabled_nondefault_for_course(&mut conn, course_id)
345            .await?;
346    token.authorized_ok(web::Json(course_chatbot_configurations))
347}
348
349/**
350Add a route for each controller in this module.
351
352The name starts with an underline in order to appear before other functions in the module documentation.
353
354We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
355*/
356pub fn _add_routes(cfg: &mut ServiceConfig) {
357    cfg.route("/{course_id}", web::get().to(get_course_by_id))
358        .route("/{course_id}/upload", web::post().to(add_media))
359        .route(
360            "/{course_id}/default-peer-review",
361            web::get().to(get_course_default_peer_or_self_review_configuration),
362        )
363        .route(
364            "/{course_id}/default-peer-review",
365            web::put().to(put_course_default_peer_or_self_review_configuration),
366        )
367        .route("/{course_id}/pages", web::get().to(get_all_pages))
368        .route(
369            "/{courseId}/research-consent-form-questions",
370            web::put().to(upsert_course_research_form_questions),
371        )
372        .route(
373            "/{course_id}/research-consent-form",
374            web::get().to(get_research_form_with_course_id),
375        )
376        .route(
377            "/{course_id}/research-consent-form",
378            web::put().to(upsert_course_research_form),
379        )
380        .route(
381            "/{course_id}/partners-block",
382            web::post().to(post_partners_block),
383        )
384        .route(
385            "/{course_id}/partners-block",
386            web::get().to(get_partners_block),
387        )
388        .route(
389            "/{course_id}/partners-block",
390            web::delete().to(delete_partners_block),
391        )
392        .route("/{course_id}/modules", web::get().to(get_course_modules))
393        .route(
394            "/{course_id}/course-instances",
395            web::get().to(get_course_instances),
396        )
397        .route(
398            "/{course_id}/nondefault-chatbot-configurations",
399            web::get().to(get_course_nondefault_chatbot_configurations),
400        );
401}