headless_lms_server/controllers/main_frontend/
organizations.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/organizations`.
2
3use std::{path::PathBuf, str::FromStr};
4
5use models::{
6    courses::{Course, CourseCount},
7    exams::{CourseExam, NewExam, OrgExam},
8    organizations::Organization,
9    pages::{self, NewPage},
10};
11
12use crate::{
13    controllers::helpers::file_uploading::upload_image_for_organization,
14    domain::authorization::{
15        Action, Resource, authorize_with_fetched_list_of_roles, skip_authorize,
16    },
17    prelude::*,
18};
19use actix_web::web::{self, Json};
20
21/**
22GET `/api/v0/main-frontend/organizations` - Returns a list of all organizations.
23*/
24
25#[instrument(skip(pool, file_store, app_conf))]
26async fn get_all_organizations(
27    pool: web::Data<PgPool>,
28    file_store: web::Data<dyn FileStore>,
29    app_conf: web::Data<ApplicationConfiguration>,
30) -> ControllerResult<web::Json<Vec<Organization>>> {
31    let mut conn = pool.acquire().await?;
32    let organizations = models::organizations::all_organizations(&mut conn)
33        .await?
34        .into_iter()
35        .map(|org| Organization::from_database_organization(org, file_store.as_ref(), &app_conf))
36        .collect();
37
38    let token = skip_authorize();
39    token.authorized_ok(web::Json(organizations))
40}
41
42/**
43GET `/api/v0/main-frontend/organizations/{organization_id}/courses"` - Returns a list of all courses in a organization.
44*/
45#[instrument(skip(pool))]
46async fn get_organization_courses(
47    organization_id: web::Path<Uuid>,
48    pool: web::Data<PgPool>,
49    user: Option<AuthUser>,
50    pagination: web::Query<Pagination>,
51) -> ControllerResult<web::Json<Vec<Course>>> {
52    let mut conn = pool.acquire().await?;
53
54    let user = user.map(|u| u.id);
55    let courses = models::courses::organization_courses_visible_to_user_paginated(
56        &mut conn,
57        *organization_id,
58        user,
59        *pagination,
60    )
61    .await?;
62
63    let token = skip_authorize();
64    token.authorized_ok(web::Json(courses))
65}
66
67/**
68GET `/api/v0/main-frontend/organizations/{organization_id}/courses/duplicatable"` - Returns a list of all courses in a organization that the current user has permission to duplicate.
69*/
70
71#[instrument(skip(pool))]
72async fn get_organization_duplicatable_courses(
73    organization_id: web::Path<Uuid>,
74    pool: web::Data<PgPool>,
75    user: AuthUser,
76) -> ControllerResult<web::Json<Vec<Course>>> {
77    let mut conn = pool.acquire().await?;
78    let courses = models::courses::get_by_organization_id(&mut conn, *organization_id).await?;
79
80    // We filter out the courses the user does not have permission to duplicate.
81    // Prefetch roles so that we can do multiple authorization checks without repeteadly querying the database.
82    let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
83
84    let mut duplicatable_courses = Vec::new();
85    for course in courses {
86        if authorize_with_fetched_list_of_roles(
87            &mut conn,
88            Action::Duplicate,
89            Some(user.id),
90            Resource::Course(course.id),
91            &user_roles,
92        )
93        .await
94        .is_ok()
95        {
96            duplicatable_courses.push(course);
97        }
98    }
99
100    let token = skip_authorize();
101    token.authorized_ok(web::Json(duplicatable_courses))
102}
103
104#[instrument(skip(pool))]
105async fn get_organization_course_count(
106    request_organization_id: web::Path<Uuid>,
107    pool: web::Data<PgPool>,
108) -> ControllerResult<Json<CourseCount>> {
109    let mut conn = pool.acquire().await?;
110    let result =
111        models::courses::organization_course_count(&mut conn, *request_organization_id).await?;
112
113    let token = skip_authorize();
114    token.authorized_ok(Json(result))
115}
116
117#[instrument(skip(pool))]
118async fn get_organization_active_courses(
119    request_organization_id: web::Path<Uuid>,
120    pool: web::Data<PgPool>,
121    pagination: web::Query<Pagination>,
122) -> ControllerResult<Json<Vec<Course>>> {
123    let mut conn = pool.acquire().await?;
124    let courses = models::courses::get_active_courses_for_organization(
125        &mut conn,
126        *request_organization_id,
127        *pagination,
128    )
129    .await?;
130
131    let token = skip_authorize();
132    token.authorized_ok(Json(courses))
133}
134
135#[instrument(skip(pool))]
136async fn get_organization_active_courses_count(
137    request_organization_id: web::Path<Uuid>,
138    pool: web::Data<PgPool>,
139) -> ControllerResult<Json<CourseCount>> {
140    let mut conn = pool.acquire().await?;
141    let result = models::courses::get_active_courses_for_organization_count(
142        &mut conn,
143        *request_organization_id,
144    )
145    .await?;
146
147    let token = skip_authorize();
148    token.authorized_ok(Json(result))
149}
150
151/**
152PUT `/api/v0/main-frontend/organizations/:organizations_id/image` - Sets or updates the chapter image.
153
154# Example
155
156Request:
157```http
158PUT /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
159Content-Type: multipart/form-data
160
161BINARY_DATA
162```
163*/
164
165#[instrument(skip(request, payload, pool, file_store, app_conf))]
166async fn set_organization_image(
167    request: HttpRequest,
168    payload: Multipart,
169    organization_id: web::Path<Uuid>,
170    pool: web::Data<PgPool>,
171    user: AuthUser,
172    file_store: web::Data<dyn FileStore>,
173    app_conf: web::Data<ApplicationConfiguration>,
174) -> ControllerResult<web::Json<Organization>> {
175    let mut conn = pool.acquire().await?;
176    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
177    let token = authorize(
178        &mut conn,
179        Act::Edit,
180        Some(user.id),
181        Res::Organization(organization.id),
182    )
183    .await?;
184    let organization_image = upload_image_for_organization(
185        request.headers(),
186        payload,
187        &organization,
188        &file_store,
189        user,
190        &mut conn,
191    )
192    .await?
193    .to_string_lossy()
194    .to_string();
195    let updated_organization = models::organizations::update_organization_image_path(
196        &mut conn,
197        organization.id,
198        Some(organization_image),
199    )
200    .await?;
201
202    // Remove old image if one exists.
203    if let Some(old_image_path) = organization.organization_image_path {
204        let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
205            ControllerError::new(
206                ControllerErrorType::InternalServerError,
207                original_error.to_string(),
208                Some(original_error.into()),
209            )
210        })?;
211        file_store.delete(&file).await.map_err(|original_error| {
212            ControllerError::new(
213                ControllerErrorType::InternalServerError,
214                original_error.to_string(),
215                Some(original_error.into()),
216            )
217        })?;
218    }
219
220    let response = Organization::from_database_organization(
221        updated_organization,
222        file_store.as_ref(),
223        app_conf.as_ref(),
224    );
225    token.authorized_ok(web::Json(response))
226}
227
228/**
229DELETE `/api/v0/main-frontend/organizations/:organizations_id/image` - Removes the organizations image.
230
231# Example
232
233Request:
234```http
235DELETE /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
236```
237*/
238
239#[instrument(skip(pool, file_store))]
240async fn remove_organization_image(
241    organization_id: web::Path<Uuid>,
242    pool: web::Data<PgPool>,
243    user: AuthUser,
244    file_store: web::Data<dyn FileStore>,
245) -> ControllerResult<web::Json<()>> {
246    let mut conn = pool.acquire().await?;
247    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
248    let token = authorize(
249        &mut conn,
250        Act::Edit,
251        Some(user.id),
252        Res::Organization(organization.id),
253    )
254    .await?;
255    if let Some(organization_image_path) = organization.organization_image_path {
256        let file = PathBuf::from_str(&organization_image_path).map_err(|original_error| {
257            ControllerError::new(
258                ControllerErrorType::InternalServerError,
259                original_error.to_string(),
260                Some(original_error.into()),
261            )
262        })?;
263        let _res =
264            models::organizations::update_organization_image_path(&mut conn, organization.id, None)
265                .await?;
266        file_store.delete(&file).await.map_err(|original_error| {
267            ControllerError::new(
268                ControllerErrorType::InternalServerError,
269                original_error.to_string(),
270                Some(original_error.into()),
271            )
272        })?;
273    }
274    token.authorized_ok(web::Json(()))
275}
276
277/**
278GET `/api/v0/main-frontend/organizations/{organization_id}` - Returns an organizations with id.
279*/
280
281#[instrument(skip(pool, file_store, app_conf))]
282async fn get_organization(
283    organization_id: web::Path<Uuid>,
284    pool: web::Data<PgPool>,
285    file_store: web::Data<dyn FileStore>,
286    app_conf: web::Data<ApplicationConfiguration>,
287) -> ControllerResult<web::Json<Organization>> {
288    let mut conn = pool.acquire().await?;
289    let db_organization =
290        models::organizations::get_organization(&mut conn, *organization_id).await?;
291    let organization =
292        Organization::from_database_organization(db_organization, file_store.as_ref(), &app_conf);
293
294    let token = skip_authorize();
295    token.authorized_ok(web::Json(organization))
296}
297
298/**
299GET `/api/v0/main-frontend/organizations/{organization_id}/course_exams` - Returns an organizations exams in CourseExam form.
300*/
301#[instrument(skip(pool))]
302async fn get_course_exams(
303    pool: web::Data<PgPool>,
304    organization: web::Path<Uuid>,
305) -> ControllerResult<web::Json<Vec<CourseExam>>> {
306    let mut conn = pool.acquire().await?;
307    let exams = models::exams::get_course_exams_for_organization(&mut conn, *organization).await?;
308
309    let token = skip_authorize();
310    token.authorized_ok(web::Json(exams))
311}
312
313/**
314GET `/api/v0/main-frontend/organizations/{organization_id}/exams` - Returns an organizations exams in Exam form.
315*/
316#[instrument(skip(pool))]
317async fn get_org_exams(
318    pool: web::Data<PgPool>,
319    organization: web::Path<Uuid>,
320) -> ControllerResult<web::Json<Vec<OrgExam>>> {
321    let mut conn = pool.acquire().await?;
322    let exams = models::exams::get_exams_for_organization(&mut conn, *organization).await?;
323
324    let token = skip_authorize();
325    token.authorized_ok(web::Json(exams))
326}
327
328/**
329GET `/api/v0/main-frontend/organizations/{exam_id}/fetch_org_exam
330*/
331#[instrument(skip(pool))]
332pub async fn get_org_exam_with_exam_id(
333    pool: web::Data<PgPool>,
334    exam_id: web::Path<Uuid>,
335    user: AuthUser,
336) -> ControllerResult<web::Json<OrgExam>> {
337    let mut conn = pool.acquire().await?;
338    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
339
340    let exam = models::exams::get_organization_exam_with_exam_id(&mut conn, *exam_id).await?;
341
342    token.authorized_ok(web::Json(exam))
343}
344
345/**
346POST `/api/v0/main-frontend/organizations/{organization_id}/exams` - Creates new exam for the organization.
347*/
348#[instrument(skip(pool))]
349async fn create_exam(
350    pool: web::Data<PgPool>,
351    payload: web::Json<NewExam>,
352    user: AuthUser,
353) -> ControllerResult<web::Json<()>> {
354    let mut conn = pool.acquire().await?;
355    let mut tx = conn.begin().await?;
356
357    let new_exam = payload.0;
358    let token = authorize(
359        &mut tx,
360        Act::CreateCoursesOrExams,
361        Some(user.id),
362        Res::Organization(new_exam.organization_id),
363    )
364    .await?;
365
366    let new_exam_id = models::exams::insert(&mut tx, PKeyPolicy::Generate, &new_exam).await?;
367    pages::insert_exam_page(
368        &mut tx,
369        new_exam_id,
370        NewPage {
371            chapter_id: None,
372            course_id: None,
373            exam_id: Some(new_exam_id),
374            front_page_of_chapter_id: None,
375            content: serde_json::Value::Array(vec![]),
376            content_search_language: Some("simple".to_string()),
377            exercise_slides: vec![],
378            exercise_tasks: vec![],
379            exercises: vec![],
380            title: "exam page".to_string(),
381            url_path: "/".to_string(),
382        },
383        user.id,
384    )
385    .await?;
386
387    models::roles::insert(
388        &mut tx,
389        user.id,
390        models::roles::UserRole::Teacher,
391        models::roles::RoleDomain::Exam(new_exam_id),
392    )
393    .await?;
394
395    tx.commit().await?;
396
397    token.authorized_ok(web::Json(()))
398}
399
400/**
401Add a route for each controller in this module.
402
403The name starts with an underline in order to appear before other functions in the module documentation.
404
405We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
406*/
407pub fn _add_routes(cfg: &mut ServiceConfig) {
408    cfg.route("", web::get().to(get_all_organizations))
409        .route("/{organization_id}", web::get().to(get_organization))
410        .route(
411            "/{organization_id}/courses",
412            web::get().to(get_organization_courses),
413        )
414        .route(
415            "/{organization_id}/courses/duplicatable",
416            web::get().to(get_organization_duplicatable_courses),
417        )
418        .route(
419            "/{organization_id}/courses/count",
420            web::get().to(get_organization_course_count),
421        )
422        .route(
423            "/{organization_id}/courses/active",
424            web::get().to(get_organization_active_courses),
425        )
426        .route(
427            "/{organization_id}/courses/active/count",
428            web::get().to(get_organization_active_courses_count),
429        )
430        .route(
431            "/{organization_id}/image",
432            web::put().to(set_organization_image),
433        )
434        .route(
435            "/{organization_id}/image",
436            web::delete().to(remove_organization_image),
437        )
438        .route(
439            "/{organization_id}/course_exams",
440            web::get().to(get_course_exams),
441        )
442        .route("/{organization_id}/org_exams", web::get().to(get_org_exams))
443        .route(
444            "/{organization_id}/fetch_org_exam",
445            web::get().to(get_org_exam_with_exam_id),
446        )
447        .route("/{organization_id}/exams", web::post().to(create_exam));
448}