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: Option<AuthUser>,
107) -> ControllerResult<web::Json<Vec<ExerciseRepository>>> {
108    let mut conn = pool.acquire().await?;
109    let token = authorize(
110        &mut conn,
111        Act::View,
112        user.map(|u| u.id),
113        Res::Course(*course_id),
114    )
115    .await?;
116    let repos = models::exercise_repositories::get_for_course_or_exam(
117        &mut conn,
118        CourseOrExamId::Course(*course_id),
119    )
120    .await?;
121    token.authorized_ok(web::Json(repos))
122}
123
124/**
125GET `/api/v0/main-frontend/exercise-repositories/exam/:id`
126*/
127#[utoipa::path(
128    get,
129    path = "/exam/{exam_id}",
130    operation_id = "getExerciseRepositoriesForExam",
131    tag = "exercise_repositories",
132    params(
133        ("exam_id" = Uuid, Path, description = "Exam id")
134    ),
135    responses(
136        (status = 200, description = "Exercise repositories for exam", body = Vec<ExerciseRepository>)
137    )
138)]
139#[instrument(skip(pool))]
140async fn get_for_exam(
141    pool: web::Data<PgPool>,
142    exam_id: web::Path<Uuid>,
143    user: Option<AuthUser>,
144) -> ControllerResult<web::Json<Vec<ExerciseRepository>>> {
145    let mut conn = pool.acquire().await?;
146    let token = authorize(
147        &mut conn,
148        Act::View,
149        user.map(|u| u.id),
150        Res::Exam(*exam_id),
151    )
152    .await?;
153    let repos = models::exercise_repositories::get_for_course_or_exam(
154        &mut conn,
155        CourseOrExamId::Exam(*exam_id),
156    )
157    .await?;
158    token.authorized_ok(web::Json(repos))
159}
160
161/**
162DELETE `/api/v0/main-frontend/exercise-repositories/:id`
163*/
164#[utoipa::path(
165    delete,
166    path = "/{id}",
167    operation_id = "deleteExerciseRepository",
168    tag = "exercise_repositories",
169    params(
170        ("id" = Uuid, Path, description = "Exercise repository id")
171    ),
172    responses(
173        (status = 200, description = "Deleted exercise repository", body = bool)
174    )
175)]
176#[instrument(skip(pool))]
177async fn delete(
178    pool: web::Data<PgPool>,
179    id: web::Path<Uuid>,
180    user: Option<AuthUser>,
181) -> ControllerResult<web::Json<bool>> {
182    let mut conn = pool.acquire().await?;
183    let repository = models::exercise_repositories::get(&mut conn, *id).await?;
184    let course_or_exam_id =
185        CourseOrExamId::from_course_and_exam_ids(repository.course_id, repository.exam_id)?;
186    let token = authorize(
187        &mut conn,
188        Act::Edit,
189        user.map(|u| u.id),
190        Res::from_course_or_exam_id(course_or_exam_id),
191    )
192    .await?;
193
194    let mut tx = conn.begin().await?;
195    models::repository_exercises::delete_from_repository(&mut tx, *id).await?;
196    models::exercise_repositories::delete(&mut tx, *id).await?;
197    tx.commit().await?;
198
199    token.authorized_ok(web::Json(true))
200}
201
202/**
203PUT `/api/v0/main-frontend/exercise-repositories/:id`
204*/
205#[utoipa::path(
206    put,
207    path = "/{id}",
208    operation_id = "updateExerciseRepository",
209    tag = "exercise_repositories",
210    params(
211        ("id" = Uuid, Path, description = "Exercise repository id")
212    ),
213    request_body = ExerciseRepositoryUpdate,
214    responses(
215        (status = 200, description = "Updated exercise repository", body = bool)
216    )
217)]
218#[instrument(skip(pool))]
219async fn update(
220    pool: web::Data<PgPool>,
221    id: web::Path<Uuid>,
222    user: Option<AuthUser>,
223    update: web::Json<ExerciseRepositoryUpdate>,
224) -> ControllerResult<web::Json<bool>> {
225    let mut conn = pool.acquire().await?;
226    let repository = models::exercise_repositories::get(&mut conn, *id).await?;
227    let course_or_exam_id =
228        CourseOrExamId::from_course_and_exam_ids(repository.course_id, repository.exam_id)?;
229    let token = authorize(
230        &mut conn,
231        Act::Edit,
232        user.map(|u| u.id),
233        Res::from_course_or_exam_id(course_or_exam_id),
234    )
235    .await?;
236
237    models::exercise_repositories::update(&mut conn, *id, update.deref()).await?;
238    token.authorized_ok(web::Json(true))
239}
240
241/**
242Add a route for each controller in this module.
243
244The name starts with an underline in order to appear before other functions in the module documentation.
245
246We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
247*/
248pub fn _add_routes(cfg: &mut ServiceConfig) {
249    cfg.route("/new", web::post().to(new))
250        .route("/course/{course_id}", web::get().to(get_for_course))
251        .route("/exam/{exam_id}", web::get().to(get_for_exam))
252        .route("/{id}", web::delete().to(delete))
253        .route("/{id}", web::put().to(update));
254}