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