Skip to main content

headless_lms_server/controllers/main_frontend/
exercise_repositories.rs

1use std::ops::Deref;
2
3use models::{
4    CourseOrExamId,
5    exercise_repositories::{ExerciseRepository, ExerciseRepositoryUpdate},
6};
7use utoipa::{OpenApi, ToSchema};
8
9use crate::{domain, prelude::*};
10
11#[derive(OpenApi)]
12#[openapi(paths(new, get_for_course, get_for_exam, delete, update))]
13pub(crate) struct MainFrontendExerciseRepositoriesApiDoc;
14
15#[derive(Debug, Deserialize, ToSchema)]
16
17pub struct NewExerciseRepository {
18    course_id: Option<Uuid>,
19    exam_id: Option<Uuid>,
20    git_url: String,
21    public_key: Option<String>,
22    deploy_key: Option<String>,
23}
24
25/**
26POST `/api/v0/main-frontend/exercise-repositories/new
27*/
28#[utoipa::path(
29    post,
30    path = "/new",
31    operation_id = "createExerciseRepository",
32    tag = "exercise_repositories",
33    request_body = NewExerciseRepository,
34    responses(
35        (status = 200, description = "Created exercise repository id", body = Uuid)
36    )
37)]
38#[instrument(skip(pool, file_store, app_conf))]
39async fn new(
40    pool: web::Data<PgPool>,
41    file_store: web::Data<dyn FileStore>,
42    repository: web::Json<NewExerciseRepository>,
43    user: AuthUser,
44    app_conf: web::Data<ApplicationConfiguration>,
45) -> ControllerResult<web::Json<Uuid>> {
46    let mut conn = pool.acquire().await?;
47    let course_or_exam_id =
48        CourseOrExamId::from_course_and_exam_ids(repository.course_id, repository.exam_id)?;
49    let token = authorize(
50        &mut conn,
51        Act::Edit,
52        Some(user.id),
53        Res::from_course_or_exam_id(course_or_exam_id),
54    )
55    .await?;
56    // create pending repository
57    let new_repository_id = Uuid::new_v4();
58    models::exercise_repositories::new(
59        &mut conn,
60        new_repository_id,
61        course_or_exam_id,
62        &repository.git_url,
63        repository.public_key.as_deref(),
64        repository.deploy_key.as_deref(),
65    )
66    .await?;
67    // processing a repository may take a while, so this is done in the background
68    actix_web::rt::spawn(async move {
69        let file_store = file_store;
70        if let Err(err) = domain::exercise_repositories::process(
71            &mut conn,
72            new_repository_id,
73            &repository.git_url,
74            repository.public_key.as_deref(),
75            repository.deploy_key.as_deref(),
76            file_store.as_ref(),
77            app_conf.as_ref(),
78        )
79        .await
80        {
81            tracing::error!("Error while processing repository {new_repository_id}: {err}");
82        }
83    });
84    token.authorized_ok(web::Json(new_repository_id))
85}
86
87/**
88GET `/api/v0/main-frontend/exercise-repositories/course/:id`
89*/
90#[utoipa::path(
91    get,
92    path = "/course/{course_id}",
93    operation_id = "getExerciseRepositoriesForCourse",
94    tag = "exercise_repositories",
95    params(
96        ("course_id" = Uuid, Path, description = "Course id")
97    ),
98    responses(
99        (status = 200, description = "Exercise repositories for course", body = Vec<ExerciseRepository>)
100    )
101)]
102#[instrument(skip(pool))]
103async fn get_for_course(
104    pool: web::Data<PgPool>,
105    course_id: web::Path<Uuid>,
106    user: AuthUser,
107) -> ControllerResult<web::Json<Vec<ExerciseRepository>>> {
108    let mut conn = pool.acquire().await?;
109    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
110    let repos = models::exercise_repositories::get_for_course_or_exam(
111        &mut conn,
112        CourseOrExamId::Course(*course_id),
113    )
114    .await?;
115    token.authorized_ok(web::Json(repos))
116}
117
118/**
119GET `/api/v0/main-frontend/exercise-repositories/exam/:id`
120*/
121#[utoipa::path(
122    get,
123    path = "/exam/{exam_id}",
124    operation_id = "getExerciseRepositoriesForExam",
125    tag = "exercise_repositories",
126    params(
127        ("exam_id" = Uuid, Path, description = "Exam id")
128    ),
129    responses(
130        (status = 200, description = "Exercise repositories for exam", body = Vec<ExerciseRepository>)
131    )
132)]
133#[instrument(skip(pool))]
134async fn get_for_exam(
135    pool: web::Data<PgPool>,
136    exam_id: web::Path<Uuid>,
137    user: AuthUser,
138) -> ControllerResult<web::Json<Vec<ExerciseRepository>>> {
139    let mut conn = pool.acquire().await?;
140    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Exam(*exam_id)).await?;
141    let repos = models::exercise_repositories::get_for_course_or_exam(
142        &mut conn,
143        CourseOrExamId::Exam(*exam_id),
144    )
145    .await?;
146    token.authorized_ok(web::Json(repos))
147}
148
149/**
150DELETE `/api/v0/main-frontend/exercise-repositories/:id`
151*/
152#[utoipa::path(
153    delete,
154    path = "/{id}",
155    operation_id = "deleteExerciseRepository",
156    tag = "exercise_repositories",
157    params(
158        ("id" = Uuid, Path, description = "Exercise repository id")
159    ),
160    responses(
161        (status = 200, description = "Deleted exercise repository", body = bool)
162    )
163)]
164#[instrument(skip(pool))]
165async fn delete(
166    pool: web::Data<PgPool>,
167    id: web::Path<Uuid>,
168    user: Option<AuthUser>,
169) -> ControllerResult<web::Json<bool>> {
170    let mut conn = pool.acquire().await?;
171    let repository = models::exercise_repositories::get(&mut conn, *id).await?;
172    let course_or_exam_id =
173        CourseOrExamId::from_course_and_exam_ids(repository.course_id, repository.exam_id)?;
174    let token = authorize(
175        &mut conn,
176        Act::Edit,
177        user.map(|u| u.id),
178        Res::from_course_or_exam_id(course_or_exam_id),
179    )
180    .await?;
181
182    let mut tx = conn.begin().await?;
183    models::repository_exercises::delete_from_repository(&mut tx, *id).await?;
184    models::exercise_repositories::delete(&mut tx, *id).await?;
185    tx.commit().await?;
186
187    token.authorized_ok(web::Json(true))
188}
189
190/**
191PUT `/api/v0/main-frontend/exercise-repositories/:id`
192*/
193#[utoipa::path(
194    put,
195    path = "/{id}",
196    operation_id = "updateExerciseRepository",
197    tag = "exercise_repositories",
198    params(
199        ("id" = Uuid, Path, description = "Exercise repository id")
200    ),
201    request_body = ExerciseRepositoryUpdate,
202    responses(
203        (status = 200, description = "Updated exercise repository", body = bool)
204    )
205)]
206#[instrument(skip(pool))]
207async fn update(
208    pool: web::Data<PgPool>,
209    id: web::Path<Uuid>,
210    user: Option<AuthUser>,
211    update: web::Json<ExerciseRepositoryUpdate>,
212) -> ControllerResult<web::Json<bool>> {
213    let mut conn = pool.acquire().await?;
214    let repository = models::exercise_repositories::get(&mut conn, *id).await?;
215    let course_or_exam_id =
216        CourseOrExamId::from_course_and_exam_ids(repository.course_id, repository.exam_id)?;
217    let token = authorize(
218        &mut conn,
219        Act::Edit,
220        user.map(|u| u.id),
221        Res::from_course_or_exam_id(course_or_exam_id),
222    )
223    .await?;
224
225    models::exercise_repositories::update(&mut conn, *id, update.deref()).await?;
226    token.authorized_ok(web::Json(true))
227}
228
229/**
230Add a route for each controller in this module.
231
232The name starts with an underline in order to appear before other functions in the module documentation.
233
234We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
235*/
236pub fn _add_routes(cfg: &mut ServiceConfig) {
237    cfg.route("/new", web::post().to(new))
238        .route("/course/{course_id}", web::get().to(get_for_course))
239        .route("/exam/{exam_id}", web::get().to(get_for_exam))
240        .route("/{id}", web::delete().to(delete))
241        .route("/{id}", web::put().to(update));
242}