Skip to main content

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::controllers::auth::is_user_global_admin;
13use crate::domain::authorization::{Action as Act, Resource as Res};
14use crate::{
15    controllers::helpers::file_uploading::upload_image_for_organization,
16    domain::authorization::{
17        Action, Resource, authorize, authorize_with_fetched_list_of_roles, skip_authorize,
18    },
19    prelude::*,
20};
21
22use actix_web::web::{self, Json};
23use utoipa::{OpenApi, ToSchema};
24
25#[derive(OpenApi)]
26#[openapi(paths(
27    get_all_organizations,
28    create_organization,
29    get_organization,
30    update_organization,
31    soft_delete_organization,
32    get_organization_courses,
33    get_organization_duplicatable_courses,
34    get_organization_course_count,
35    get_organization_active_courses,
36    get_organization_active_courses_count,
37    set_organization_image,
38    remove_organization_image,
39    get_course_exams,
40    get_org_exams,
41    get_org_exam_with_exam_id,
42    create_exam
43))]
44pub(crate) struct MainFrontendOrganizationsApiDoc;
45
46#[allow(dead_code)]
47#[derive(Debug, ToSchema)]
48struct OrganizationImageUploadPayload {
49    #[schema(content_media_type = "application/octet-stream", value_type = String, format = Binary)]
50    file: Vec<u8>,
51}
52
53/**
54GET `/api/v0/main-frontend/organizations` - Returns a list of all organizations.
55*/
56#[utoipa::path(
57    get,
58    path = "",
59    operation_id = "getOrganizations",
60    tag = "organizations",
61    responses(
62        (status = 200, description = "Organizations", body = [Organization])
63    )
64)]
65#[instrument(skip(pool, file_store, app_conf))]
66async fn get_all_organizations(
67    pool: web::Data<PgPool>,
68    file_store: web::Data<dyn FileStore>,
69    app_conf: web::Data<ApplicationConfiguration>,
70    user: Option<AuthUser>,
71) -> ControllerResult<web::Json<Vec<Organization>>> {
72    let mut conn = pool.acquire().await?;
73
74    let is_admin = if let Some(user) = user {
75        is_user_global_admin(&mut conn, user.id).await?
76    } else {
77        false
78    };
79
80    // Choose query based on admin status
81    let raw_organizations = if is_admin {
82        models::organizations::all_organizations_include_hidden(&mut conn).await?
83    } else {
84        models::organizations::all_organizations(&mut conn).await?
85    };
86
87    let organizations = raw_organizations
88        .into_iter()
89        .map(|org| Organization::from_database_organization(org, file_store.as_ref(), &app_conf))
90        .collect();
91
92    let token = skip_authorize();
93    token.authorized_ok(web::Json(organizations))
94}
95
96/**
97GET `/api/v0/main-frontend/organizations/{organization_id}/courses"` - Returns a list of all courses in a organization.
98*/
99#[utoipa::path(
100    get,
101    path = "/{organization_id}/courses",
102    operation_id = "getOrganizationCourses",
103    tag = "organizations",
104    params(
105        ("organization_id" = Uuid, Path, description = "Organization id"),
106        ("page" = Option<i64>, Query, description = "Page number"),
107        ("limit" = Option<i64>, Query, description = "Page size")
108    ),
109    responses(
110        (status = 200, description = "Organization courses", body = [Course])
111    )
112)]
113#[instrument(skip(pool))]
114async fn get_organization_courses(
115    organization_id: web::Path<Uuid>,
116    pool: web::Data<PgPool>,
117    user: Option<AuthUser>,
118    pagination: web::Query<Pagination>,
119) -> ControllerResult<web::Json<Vec<Course>>> {
120    let mut conn = pool.acquire().await?;
121
122    let user = user.map(|u| u.id);
123    let courses = models::courses::organization_courses_visible_to_user_paginated(
124        &mut conn,
125        *organization_id,
126        user,
127        *pagination,
128    )
129    .await?;
130
131    let token = skip_authorize();
132    token.authorized_ok(web::Json(courses))
133}
134
135/**
136GET `/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.
137*/
138#[utoipa::path(
139    get,
140    path = "/{organization_id}/courses/duplicatable",
141    operation_id = "getOrganizationDuplicatableCourses",
142    tag = "organizations",
143    params(
144        ("organization_id" = Uuid, Path, description = "Organization id")
145    ),
146    responses(
147        (status = 200, description = "Duplicatable organization courses", body = [Course])
148    )
149)]
150#[instrument(skip(pool))]
151async fn get_organization_duplicatable_courses(
152    organization_id: web::Path<Uuid>,
153    pool: web::Data<PgPool>,
154    user: AuthUser,
155) -> ControllerResult<web::Json<Vec<Course>>> {
156    let mut conn = pool.acquire().await?;
157    let courses = models::courses::get_by_organization_id(&mut conn, *organization_id).await?;
158
159    // We filter out the courses the user does not have permission to duplicate.
160    // Prefetch roles so that we can do multiple authorization checks without repeteadly querying the database.
161    let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
162
163    let mut duplicatable_courses = Vec::new();
164    for course in courses {
165        if authorize_with_fetched_list_of_roles(
166            &mut conn,
167            Action::Duplicate,
168            Some(user.id),
169            Resource::Course(course.id),
170            &user_roles,
171        )
172        .await
173        .is_ok()
174        {
175            duplicatable_courses.push(course);
176        }
177    }
178
179    let token = skip_authorize();
180    token.authorized_ok(web::Json(duplicatable_courses))
181}
182
183#[utoipa::path(
184    get,
185    path = "/{organization_id}/courses/count",
186    operation_id = "getOrganizationCourseCount",
187    tag = "organizations",
188    params(
189        ("organization_id" = Uuid, Path, description = "Organization id")
190    ),
191    responses(
192        (status = 200, description = "Organization course count", body = CourseCount)
193    )
194)]
195#[instrument(skip(pool))]
196async fn get_organization_course_count(
197    request_organization_id: web::Path<Uuid>,
198    pool: web::Data<PgPool>,
199) -> ControllerResult<Json<CourseCount>> {
200    let mut conn = pool.acquire().await?;
201    let result =
202        models::courses::organization_course_count(&mut conn, *request_organization_id).await?;
203
204    let token = skip_authorize();
205    token.authorized_ok(Json(result))
206}
207
208#[utoipa::path(
209    get,
210    path = "/{organization_id}/courses/active",
211    operation_id = "getOrganizationActiveCourses",
212    tag = "organizations",
213    params(
214        ("organization_id" = Uuid, Path, description = "Organization id"),
215        ("page" = Option<i64>, Query, description = "Page number"),
216        ("limit" = Option<i64>, Query, description = "Page size")
217    ),
218    responses(
219        (status = 200, description = "Active organization courses", body = [Course])
220    )
221)]
222#[instrument(skip(pool))]
223async fn get_organization_active_courses(
224    request_organization_id: web::Path<Uuid>,
225    pool: web::Data<PgPool>,
226    pagination: web::Query<Pagination>,
227) -> ControllerResult<Json<Vec<Course>>> {
228    let mut conn = pool.acquire().await?;
229    let courses = models::courses::get_active_courses_for_organization(
230        &mut conn,
231        *request_organization_id,
232        *pagination,
233    )
234    .await?;
235
236    let token = skip_authorize();
237    token.authorized_ok(Json(courses))
238}
239
240#[utoipa::path(
241    get,
242    path = "/{organization_id}/courses/active/count",
243    operation_id = "getOrganizationActiveCourseCount",
244    tag = "organizations",
245    params(
246        ("organization_id" = Uuid, Path, description = "Organization id")
247    ),
248    responses(
249        (status = 200, description = "Active organization course count", body = CourseCount)
250    )
251)]
252#[instrument(skip(pool))]
253async fn get_organization_active_courses_count(
254    request_organization_id: web::Path<Uuid>,
255    pool: web::Data<PgPool>,
256) -> ControllerResult<Json<CourseCount>> {
257    let mut conn = pool.acquire().await?;
258    let result = models::courses::get_active_courses_for_organization_count(
259        &mut conn,
260        *request_organization_id,
261    )
262    .await?;
263
264    let token = skip_authorize();
265    token.authorized_ok(Json(result))
266}
267
268/**
269PUT `/api/v0/main-frontend/organizations/:organizations_id/image` - Sets or updates the chapter image.
270
271# Example
272
273Request:
274```http
275PUT /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
276Content-Type: multipart/form-data
277
278BINARY_DATA
279```
280*/
281#[utoipa::path(
282    put,
283    path = "/{organization_id}/image",
284    operation_id = "updateOrganizationImage",
285    tag = "organizations",
286    params(
287        ("organization_id" = Uuid, Path, description = "Organization id")
288    ),
289    request_body(content = inline(OrganizationImageUploadPayload), content_type = "multipart/form-data"),
290    responses(
291        (status = 200, description = "Updated organization", body = serde_json::Value)
292    )
293)]
294#[instrument(skip(request, payload, pool, file_store, app_conf))]
295async fn set_organization_image(
296    request: HttpRequest,
297    payload: Multipart,
298    organization_id: web::Path<Uuid>,
299    pool: web::Data<PgPool>,
300    user: AuthUser,
301    file_store: web::Data<dyn FileStore>,
302    app_conf: web::Data<ApplicationConfiguration>,
303) -> ControllerResult<web::Json<Organization>> {
304    let mut conn = pool.acquire().await?;
305    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
306    let token = authorize(
307        &mut conn,
308        Act::Edit,
309        Some(user.id),
310        Res::Organization(organization.id),
311    )
312    .await?;
313    let organization_image = upload_image_for_organization(
314        request.headers(),
315        payload,
316        &organization,
317        &file_store,
318        user,
319        &mut conn,
320    )
321    .await?
322    .to_string_lossy()
323    .to_string();
324    let updated_organization = models::organizations::update_organization_image_path(
325        &mut conn,
326        organization.id,
327        Some(organization_image),
328    )
329    .await?;
330
331    // Remove old image if one exists.
332    if let Some(old_image_path) = organization.organization_image_path {
333        let file = PathBuf::from_str(&old_image_path).map_err(|original_error| {
334            ControllerError::new(
335                ControllerErrorType::InternalServerError,
336                original_error.to_string(),
337                Some(original_error.into()),
338            )
339        })?;
340        file_store.delete(&file).await.map_err(|original_error| {
341            ControllerError::new(
342                ControllerErrorType::InternalServerError,
343                original_error.to_string(),
344                Some(original_error.into()),
345            )
346        })?;
347    }
348
349    let response = Organization::from_database_organization(
350        updated_organization,
351        file_store.as_ref(),
352        app_conf.as_ref(),
353    );
354    token.authorized_ok(web::Json(response))
355}
356
357/**
358DELETE `/api/v0/main-frontend/organizations/:organizations_id/image` - Removes the organizations image.
359
360# Example
361
362Request:
363```http
364DELETE /api/v0/main-frontend/organizations/d332f3d9-39a5-4a18-80f4-251727693c37/image HTTP/1.1
365```
366*/
367#[utoipa::path(
368    delete,
369    path = "/{organization_id}/image",
370    operation_id = "deleteOrganizationImage",
371    tag = "organizations",
372    params(
373        ("organization_id" = Uuid, Path, description = "Organization id")
374    ),
375    responses(
376        (status = 200, description = "Organization image removed")
377    )
378)]
379#[instrument(skip(pool, file_store))]
380async fn remove_organization_image(
381    organization_id: web::Path<Uuid>,
382    pool: web::Data<PgPool>,
383    user: AuthUser,
384    file_store: web::Data<dyn FileStore>,
385) -> ControllerResult<web::Json<()>> {
386    let mut conn = pool.acquire().await?;
387    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
388    let token = authorize(
389        &mut conn,
390        Act::Edit,
391        Some(user.id),
392        Res::Organization(organization.id),
393    )
394    .await?;
395    if let Some(organization_image_path) = organization.organization_image_path {
396        let file = PathBuf::from_str(&organization_image_path).map_err(|original_error| {
397            ControllerError::new(
398                ControllerErrorType::InternalServerError,
399                original_error.to_string(),
400                Some(original_error.into()),
401            )
402        })?;
403        let _res =
404            models::organizations::update_organization_image_path(&mut conn, organization.id, None)
405                .await?;
406        file_store.delete(&file).await.map_err(|original_error| {
407            ControllerError::new(
408                ControllerErrorType::InternalServerError,
409                original_error.to_string(),
410                Some(original_error.into()),
411            )
412        })?;
413    }
414    token.authorized_ok(web::Json(()))
415}
416
417/**
418GET `/api/v0/main-frontend/organizations/{organization_id}` - Returns an organizations with id.
419*/
420#[utoipa::path(
421    get,
422    path = "/{organization_id}",
423    operation_id = "getOrganization",
424    tag = "organizations",
425    params(
426        ("organization_id" = Uuid, Path, description = "Organization id")
427    ),
428    responses(
429        (status = 200, description = "Organization", body = Organization)
430    )
431)]
432#[instrument(skip(pool, file_store, app_conf))]
433async fn get_organization(
434    organization_id: web::Path<Uuid>,
435    pool: web::Data<PgPool>,
436    file_store: web::Data<dyn FileStore>,
437    app_conf: web::Data<ApplicationConfiguration>,
438    user: Option<AuthUser>,
439) -> ControllerResult<web::Json<Organization>> {
440    let mut conn = pool.acquire().await?;
441    let db_organization =
442        models::organizations::get_organization(&mut conn, *organization_id).await?;
443    if db_organization.deleted_at.is_some() {
444        return Err(organization_not_found());
445    }
446    let token = if db_organization.hidden {
447        let Some(user) = user else {
448            return Err(organization_not_found());
449        };
450        match authorize(
451            &mut conn,
452            Act::Edit,
453            Some(user.id),
454            Res::Organization(db_organization.id),
455        )
456        .await
457        {
458            Ok(token) => token,
459            Err(err) if matches!(err.error_type(), ControllerErrorType::Forbidden) => {
460                return Err(organization_not_found());
461            }
462            Err(err) => return Err(err),
463        }
464    } else {
465        skip_authorize()
466    };
467    let organization =
468        Organization::from_database_organization(db_organization, file_store.as_ref(), &app_conf);
469
470    token.authorized_ok(web::Json(organization))
471}
472
473fn organization_not_found() -> ControllerError {
474    controller_err!(NotFound, "Organization not found".to_string())
475}
476
477#[derive(Debug, Deserialize, ToSchema)]
478struct OrganizationUpdatePayload {
479    name: String,
480    hidden: bool,
481    slug: String,
482}
483
484/**
485PUT `/api/v0/main-frontend/organizations/{organization_id}`
486
487Updates an organization's name, hidden status, and slug.
488*/
489#[utoipa::path(
490    put,
491    path = "/{organization_id}",
492    operation_id = "updateOrganization",
493    tag = "organizations",
494    params(
495        ("organization_id" = Uuid, Path, description = "Organization id")
496    ),
497    request_body = OrganizationUpdatePayload,
498    responses(
499        (status = 200, description = "Organization updated")
500    )
501)]
502#[instrument(skip(pool))]
503async fn update_organization(
504    organization_id: web::Path<Uuid>,
505    payload: web::Json<OrganizationUpdatePayload>,
506    pool: web::Data<PgPool>,
507    user: AuthUser,
508) -> ControllerResult<web::Json<()>> {
509    let mut conn = pool.acquire().await?;
510    let organization = models::organizations::get_organization(&mut conn, *organization_id).await?;
511
512    let token = authorize(
513        &mut conn,
514        Act::Edit,
515        Some(user.id),
516        Res::Organization(organization.id),
517    )
518    .await?;
519
520    models::organizations::update_name_and_hidden(
521        &mut conn,
522        *organization_id,
523        &payload.name,
524        payload.hidden,
525        &payload.slug,
526    )
527    .await?;
528
529    token.authorized_ok(web::Json(()))
530}
531
532#[derive(Debug, Deserialize, ToSchema)]
533struct OrganizationCreatePayload {
534    name: String,
535    slug: String,
536    hidden: bool,
537}
538
539/// POST `/api/v0/main-frontend/organizations`
540/// Creates a new organization with the given name, slug, and visibility status.
541///
542/// # Request body (JSON)
543/// {
544///     "name": "Example Organization",
545///     "slug": "example-org",
546///     "hidden": false
547/// }
548///
549/// # Response
550/// Returns the created organization.
551///
552/// # Permissions
553/// Only users with the `Admin` role can access this endpoint.
554#[utoipa::path(
555    post,
556    path = "",
557    operation_id = "createOrganization",
558    tag = "organizations",
559    request_body = OrganizationCreatePayload,
560    responses(
561        (status = 200, description = "Created organization", body = serde_json::Value)
562    )
563)]
564#[instrument(skip(pool, file_store, app_conf))]
565async fn create_organization(
566    payload: web::Json<OrganizationCreatePayload>,
567    pool: web::Data<PgPool>,
568    file_store: web::Data<dyn FileStore>,
569    app_conf: web::Data<ApplicationConfiguration>,
570    user: AuthUser,
571) -> ControllerResult<web::Json<Organization>> {
572    let mut conn = pool.acquire().await?;
573
574    let token = authorize(
575        &mut conn,
576        Action::Administrate,
577        Some(user.id),
578        Resource::GlobalPermissions,
579    )
580    .await?;
581
582    let mut tx = conn.begin().await?;
583
584    let org_id = match models::organizations::insert(
585        &mut tx,
586        PKeyPolicy::Generate,
587        &payload.name,
588        &payload.slug,
589        None,
590        payload.hidden,
591    )
592    .await
593    {
594        Ok(id) => id,
595        Err(err) => {
596            let err_str = err.to_string();
597            if err_str.contains("organizations_slug_key") {
598                return Err(ControllerError::new(
599                    ControllerErrorType::BadRequest,
600                    "An organization with this slug already exists.".to_string(),
601                    None,
602                ));
603            }
604            return Err(err.into());
605        }
606    };
607
608    tx.commit().await?;
609
610    let db_org = models::organizations::get_organization(&mut conn, org_id).await?;
611    let org =
612        Organization::from_database_organization(db_org, file_store.as_ref(), app_conf.as_ref());
613
614    token.authorized_ok(web::Json(org))
615}
616
617#[utoipa::path(
618    patch,
619    path = "/{organization_id}",
620    operation_id = "softDeleteOrganization",
621    tag = "organizations",
622    params(
623        ("organization_id" = Uuid, Path, description = "Organization id")
624    ),
625    responses(
626        (status = 200, description = "Organization soft deleted")
627    )
628)]
629#[instrument(skip(pool))]
630async fn soft_delete_organization(
631    org_id: web::Path<Uuid>,
632    pool: web::Data<PgPool>,
633    user: AuthUser,
634) -> ControllerResult<web::Json<()>> {
635    let mut conn = pool.acquire().await?;
636
637    let token = authorize(
638        &mut conn,
639        Action::Administrate,
640        Some(user.id),
641        Resource::GlobalPermissions,
642    )
643    .await?;
644
645    models::organizations::soft_delete(&mut conn, *org_id).await?;
646    token.authorized_ok(web::Json(()))
647}
648
649/**
650GET `/api/v0/main-frontend/organizations/{organization_id}/course_exams` - Returns an organizations exams in CourseExam form.
651*/
652#[utoipa::path(
653    get,
654    path = "/{organization_id}/course_exams",
655    operation_id = "getOrganizationCourseExams",
656    tag = "organizations",
657    params(
658        ("organization_id" = Uuid, Path, description = "Organization id")
659    ),
660    responses(
661        (status = 200, description = "Organization course exams", body = [CourseExam])
662    )
663)]
664#[instrument(skip(pool))]
665async fn get_course_exams(
666    pool: web::Data<PgPool>,
667    organization: web::Path<Uuid>,
668) -> ControllerResult<web::Json<Vec<CourseExam>>> {
669    let mut conn = pool.acquire().await?;
670    let exams = models::exams::get_course_exams_for_organization(&mut conn, *organization).await?;
671
672    let token = skip_authorize();
673    token.authorized_ok(web::Json(exams))
674}
675
676/**
677GET `/api/v0/main-frontend/organizations/{organization_id}/exams` - Returns an organizations exams in Exam form.
678*/
679#[utoipa::path(
680    get,
681    path = "/{organization_id}/org_exams",
682    operation_id = "getOrganizationExams",
683    tag = "organizations",
684    params(
685        ("organization_id" = Uuid, Path, description = "Organization id")
686    ),
687    responses(
688        (status = 200, description = "Organization exams", body = [OrgExam])
689    )
690)]
691#[instrument(skip(pool))]
692async fn get_org_exams(
693    pool: web::Data<PgPool>,
694    organization: web::Path<Uuid>,
695) -> ControllerResult<web::Json<Vec<OrgExam>>> {
696    let mut conn = pool.acquire().await?;
697    let exams = models::exams::get_exams_for_organization(&mut conn, *organization).await?;
698
699    let token = skip_authorize();
700    token.authorized_ok(web::Json(exams))
701}
702
703/**
704GET `/api/v0/main-frontend/organizations/{exam_id}/fetch_org_exam
705*/
706#[utoipa::path(
707    get,
708    path = "/{exam_id}/fetch_org_exam",
709    operation_id = "getOrganizationExamByExamId",
710    tag = "organizations",
711    params(
712        ("exam_id" = Uuid, Path, description = "Exam id")
713    ),
714    responses(
715        (status = 200, description = "Organization exam", body = OrgExam)
716    )
717)]
718#[instrument(skip(pool))]
719pub async fn get_org_exam_with_exam_id(
720    pool: web::Data<PgPool>,
721    exam_id: web::Path<Uuid>,
722    user: AuthUser,
723) -> ControllerResult<web::Json<OrgExam>> {
724    let mut conn = pool.acquire().await?;
725    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::Exam(*exam_id)).await?;
726
727    let exam = models::exams::get_organization_exam_with_exam_id(&mut conn, *exam_id).await?;
728
729    token.authorized_ok(web::Json(exam))
730}
731
732/**
733POST `/api/v0/main-frontend/organizations/{organization_id}/exams` - Creates new exam for the organization.
734*/
735#[utoipa::path(
736    post,
737    path = "/{organization_id}/exams",
738    operation_id = "createOrganizationExam",
739    tag = "organizations",
740    params(
741        ("organization_id" = Uuid, Path, description = "Organization id")
742    ),
743    request_body = NewExam,
744    responses(
745        (status = 200, description = "Organization exam created")
746    )
747)]
748#[instrument(skip(pool))]
749async fn create_exam(
750    pool: web::Data<PgPool>,
751    payload: web::Json<NewExam>,
752    user: AuthUser,
753) -> ControllerResult<web::Json<()>> {
754    let mut conn = pool.acquire().await?;
755    let mut tx = conn.begin().await?;
756
757    let new_exam = payload.0;
758    let token = authorize(
759        &mut tx,
760        Act::CreateCoursesOrExams,
761        Some(user.id),
762        Res::Organization(new_exam.organization_id),
763    )
764    .await?;
765
766    let new_exam_id = models::exams::insert(&mut tx, PKeyPolicy::Generate, &new_exam).await?;
767    pages::insert_exam_page(
768        &mut tx,
769        new_exam_id,
770        NewPage {
771            chapter_id: None,
772            course_id: None,
773            exam_id: Some(new_exam_id),
774            front_page_of_chapter_id: None,
775            content: vec![],
776            content_search_language: Some("simple".to_string()),
777            exercise_slides: vec![],
778            exercise_tasks: vec![],
779            exercises: vec![],
780            title: "exam page".to_string(),
781            url_path: "/".to_string(),
782        },
783        user.id,
784    )
785    .await?;
786
787    models::roles::insert(
788        &mut tx,
789        user.id,
790        models::roles::UserRole::Teacher,
791        models::roles::RoleDomain::Exam(new_exam_id),
792    )
793    .await?;
794
795    tx.commit().await?;
796
797    token.authorized_ok(web::Json(()))
798}
799
800/**
801Add a route for each controller in this module.
802
803The name starts with an underline in order to appear before other functions in the module documentation.
804
805We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
806*/
807pub fn _add_routes(cfg: &mut ServiceConfig) {
808    cfg.route("", web::get().to(get_all_organizations))
809        .route("", web::post().to(create_organization))
810        .route("/{organization_id}", web::get().to(get_organization))
811        .route("/{organization_id}", web::put().to(update_organization))
812        .route(
813            "/{organization_id}",
814            web::patch().to(soft_delete_organization),
815        )
816        .route(
817            "/{organization_id}/courses",
818            web::get().to(get_organization_courses),
819        )
820        .route(
821            "/{organization_id}/courses/duplicatable",
822            web::get().to(get_organization_duplicatable_courses),
823        )
824        .route(
825            "/{organization_id}/courses/count",
826            web::get().to(get_organization_course_count),
827        )
828        .route(
829            "/{organization_id}/courses/active",
830            web::get().to(get_organization_active_courses),
831        )
832        .route(
833            "/{organization_id}/courses/active/count",
834            web::get().to(get_organization_active_courses_count),
835        )
836        .route(
837            "/{organization_id}/image",
838            web::put().to(set_organization_image),
839        )
840        .route(
841            "/{organization_id}/image",
842            web::delete().to(remove_organization_image),
843        )
844        .route(
845            "/{organization_id}/course_exams",
846            web::get().to(get_course_exams),
847        )
848        .route("/{organization_id}/org_exams", web::get().to(get_org_exams))
849        .route(
850            "/{exam_id}/fetch_org_exam",
851            web::get().to(get_org_exam_with_exam_id),
852        )
853        .route("/{organization_id}/exams", web::post().to(create_exam));
854}