diff options
Diffstat (limited to 'server/src/main.rs')
| -rw-r--r-- | server/src/main.rs | 367 |
1 files changed, 353 insertions, 14 deletions
diff --git a/server/src/main.rs b/server/src/main.rs index 66faec3..8c59b23 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -8,7 +8,7 @@ use rocket::http::Status; use rocket::response::status::{Custom, NotFound}; use rocket::serde::json::Json; use rocket::{futures, Build, Rocket, State}; -use rocket_db_pools::{sqlx, Connection, Database, Pool}; +use rocket_db_pools::{sqlx, Connection, Database}; use sqlx::Acquire; use std::path::PathBuf; use utoipa::OpenApi; @@ -55,6 +55,9 @@ struct Db(sqlx::MySqlPool); user_key_get, user_key_del, user_keys, + translation_review_new, + translation_review_strings, + translation_reviews, ), modifiers(&AuthApiAddon), )] @@ -122,7 +125,7 @@ fn abbrivate_key(key: &str) -> String { } async fn get_project( - db: &mut <<Db as Database>::Pool as Pool>::Connection, + mut db: Connection<Db>, projectid: &str, ) -> Result<Json<api_model::Project>, NotFound<&'static str>> { let users = sqlx::query!( @@ -173,12 +176,11 @@ async fn get_project( )] #[get("/project/<projectid>")] async fn project( - db: &Db, + db: Connection<Db>, _session: auth::Session, projectid: &str, ) -> Result<Json<api_model::Project>, NotFound<&'static str>> { - let mut conn = db.get().await.unwrap(); - get_project(&mut conn, projectid).await + get_project(db, projectid).await } // Remove linebreaks and potential openssh wrapper @@ -206,6 +208,7 @@ fn cleanup_key(key: &str) -> String { #[post("/project/<projectid>/new", data = "<data>")] async fn project_new( db: &Db, + conn: Connection<Db>, git_roots_config: &State<git_root::Config<'_>>, roots_state: &State<git_root::Roots>, session: auth::Session, @@ -219,9 +222,8 @@ async fn project_new( }; let main_branch = data.main_branch.unwrap_or("main"); - let mut conn = db.get().await.unwrap(); { - let mut tx = conn.begin().await.unwrap(); + let mut tx = db.begin().await.unwrap(); sqlx::query!( "INSERT INTO projects (id, title, description, remote, remote_key, main_branch) VALUES (?, ?, ?, ?, ?, ?)", @@ -258,7 +260,7 @@ async fn project_new( .map_err(|e| Custom(Status::InternalServerError, format!("{e}"))) .await?; - Ok(get_project(&mut conn, projectid).await.unwrap()) + Ok(get_project(conn, projectid).await.unwrap()) } async fn project_check_maintainer( @@ -536,15 +538,14 @@ async fn reviews( })) } -async fn get_review_users( - mut db: Connection<Db>, +async fn get_project_default_review_users<'a>( + db: &mut Connection<Db>, projectid: &str, - reviewid: u64, ) -> Vec<api_model::ReviewUserEntry> { - let mut users = sqlx::query!( + sqlx::query!( "SELECT id,name,dn,project_users.default_role AS role FROM users JOIN project_users ON project_users.user=id WHERE project_users.project=? ORDER BY role,id", projectid) - .fetch(&mut **db) + .fetch(&mut ***db) .map_ok(|r| api_model::ReviewUserEntry { user: api_model::User { id: r.id, @@ -555,7 +556,15 @@ async fn get_review_users( }) .try_collect::<Vec<_>>() .await - .unwrap(); + .unwrap() +} + +async fn get_review_users( + mut db: Connection<Db>, + projectid: &str, + reviewid: u64, +) -> Vec<api_model::ReviewUserEntry> { + let mut users = get_project_default_review_users(&mut db, projectid).await; let override_users = sqlx::query!( "SELECT id,name,dn,review_users.role AS role FROM users JOIN review_users ON review_users.user=id WHERE review_users.review=? ORDER BY role,id", @@ -1116,6 +1125,333 @@ fn healthcheck() -> &'static str { "" } +async fn get_translation_review( + mut db: Connection<Db>, + projectid: &str, + translation_reviewid: u64, +) -> Result<Json<api_model::TranslationReview>, NotFound<&'static str>> { + let mut translation_review = sqlx::query!( + "SELECT title,description,state,progress,archived,base,head,users.id AS user_id,users.name AS name,users.dn AS user_dn FROM translation_reviews JOIN users ON users.id=owner WHERE project=? AND translation_reviews.id=?", + projectid, translation_reviewid) + .fetch_one(&mut **db) + .map_ok(|r| { + api_model::TranslationReview { + id: translation_reviewid, + title: r.title, + description: r.description, + owner: api_model::User { + id: r.user_id, + name: r.name, + active: r.user_dn.is_some(), + }, + users: Vec::new(), + state: api_model::ReviewState::try_from(r.state).unwrap(), + progress: r.progress, + archived: r.archived != 0, + base: r.base, + head: r.head, + } + }) + .map_err(|_| NotFound("No such review")) + .await?; + + translation_review.users = + get_translation_review_users(db, projectid, translation_reviewid).await; + + Ok(Json(translation_review)) +} + +async fn get_translation_review_users( + mut db: Connection<Db>, + projectid: &str, + translation_reviewid: u64, +) -> Vec<api_model::ReviewUserEntry> { + let mut users = get_project_default_review_users(&mut db, projectid).await; + + let override_users = sqlx::query!( + "SELECT id,name,dn,translation_review_users.role AS role FROM users JOIN translation_review_users ON translation_review_users.user=id WHERE translation_review_users.translation_review=? ORDER BY role,id", + translation_reviewid) + .fetch(&mut **db) + .map_ok(|r| api_model::ReviewUserEntry { + user: api_model::User { + id: r.id, + name: r.name, + active: r.dn.is_some(), + }, + role: api_model::UserReviewRole::try_from(r.role).unwrap(), + }) + .try_collect::<Vec<_>>() + .await + .unwrap(); + + for override_user in override_users { + if let Some(user) = users + .iter_mut() + .find(|ue| ue.user.id == override_user.user.id) + { + user.role = override_user.role; + } else { + users.push(override_user); + } + } + + users +} + +#[utoipa::path( + responses( + (status = 200, description = "Translation review created", body = api_model::TranslationReview), + ), + security( + ("session" = []), + ), +)] +#[post("/translation/<projectid>/new", data = "<data>")] +async fn translation_review_new( + db: &Db, + mut conn: Connection<Db>, + roots_state: &State<git_root::Roots>, + session: auth::Session, + projectid: &str, + data: Json<api_model::TranslationReviewData<'_>>, +) -> Result<Json<api_model::TranslationReview>, Custom<String>> { + let title = if data.title == "" { + "Unnamed" + } else { + data.title.as_str() + }; + let mut base = data.base.unwrap_or("").to_string(); + let translation_reviewid: u64; + + { + let mut tx = conn.begin().await.unwrap(); + + if base == "" { + let main_branch = + sqlx::query!("SELECT main_branch FROM projects WHERE id=?", projectid) + .fetch_one(&mut *tx) + .map_ok(|r| r.main_branch) + .map_err(|_| Custom(Status::NotFound, "No such project".to_string())) + .await?; + + base = roots_state + .fetch_branch(projectid, main_branch.as_str()) + .map_err(|_| Custom(Status::InternalServerError, "git error".to_string())) + .await?; + } + + let r = sqlx::query!( + "INSERT INTO translation_reviews (project, owner, title, description, base, head) VALUES (?, ?, ?, ?, ?, ?)", + projectid, + session.user_id, + title, + data.description, + base, + base, + ) + .execute(&mut *tx) + .map_err(|e| Custom(Status::InternalServerError, format!("Database error: {e:?}"))) + .await?; + + translation_reviewid = r.last_insert_id(); + + tx.commit().await.unwrap(); + } + + roots_state + .new_translation_review(db, projectid, translation_reviewid, base.as_str()) + .map_err(|e| Custom(Status::InternalServerError, format!("{e}"))) + .await?; + + Ok( + get_translation_review(conn, projectid, translation_reviewid) + .await + .unwrap(), + ) +} + +#[utoipa::path( + responses( + (status = 200, description = "Get all translation reviews for project", body = api_model::TranslationReviews), + (status = 404, description = "No such project"), + ), + security( + ("session" = []), + ), +)] +#[get("/project/<projectid>/translations?<limit>&<offset>")] +async fn translation_reviews( + mut db: Connection<Db>, + _session: auth::Session, + projectid: &str, + limit: Option<u32>, + offset: Option<u32>, +) -> Result<Json<api_model::TranslationReviews>, NotFound<&'static str>> { + let uw_offset = offset.unwrap_or(0); + let uw_limit = limit.unwrap_or(10); + let entries = sqlx::query!( + "SELECT translation_reviews.id AS id,title,state,progress,base,head,users.id AS user_id,users.name AS name,users.dn AS user_dn FROM translation_reviews JOIN users ON users.id=owner WHERE project=? ORDER BY id DESC LIMIT ? OFFSET ?", + projectid, uw_limit, uw_offset) + .fetch(&mut **db) + .map_ok(|r| api_model::TranslationReviewEntry { + id: r.id, + title: r.title, + owner: api_model::User { + id: r.user_id, + name: r.name, + active: r.user_dn.is_some(), + }, + state: api_model::ReviewState::try_from(r.state).unwrap(), + progress: r.progress, + base: r.base, + head: r.head, + }) + .try_collect::<Vec<_>>() + .await + .unwrap(); + + let count = sqlx::query!( + "SELECT COUNT(id) AS count FROM translation_reviews WHERE project=?", + projectid + ) + .fetch_one(&mut **db) + .map_ok(|r| r.count) + .await + .unwrap(); + + if count == 0 { + let projects = sqlx::query!( + "SELECT COUNT(id) AS count FROM projects WHERE id=?", + projectid + ) + .fetch_one(&mut **db) + .map_ok(|r| r.count) + .await + .unwrap(); + if projects == 0 { + return Err(NotFound("No such project")); + } + } + + let u32_count = u32::try_from(count).unwrap(); + + Ok(Json(api_model::TranslationReviews { + offset: uw_offset, + limit: uw_limit, + total_count: u32_count, + more: uw_offset + uw_limit < u32_count, + reviews: entries, + })) +} + +#[utoipa::path( + responses( + (status = 200, description = "Get all strings for a translation review", body = api_model::LocalizationStrings), + (status = 404, description = "No such translation review"), + ), + security( + ("session" = []), + ), +)] +#[get("/translation/<translation_reviewid>/strings?<limit>&<offset>")] +async fn translation_review_strings( + mut db: Connection<Db>, + _session: auth::Session, + translation_reviewid: u64, + limit: Option<u32>, + offset: Option<u32>, +) -> Result<Json<api_model::LocalizationStrings>, NotFound<&'static str>> { + let uw_offset = offset.unwrap_or(0); + let uw_limit = limit.unwrap_or(10); + let (ids, mut entries) = sqlx::query!( + "SELECT id,name,file,description,meaning,source,placeholder_offsets FROM localization_strings WHERE translation_review=? ORDER BY id ASC LIMIT ? OFFSET ?", + translation_reviewid, uw_limit, uw_offset) + .fetch(&mut **db) + .try_fold((Vec::new(), Vec::new()), async move |mut vecs, r| { + vecs.0.push(r.id); + vecs.1.push(api_model::LocalizationString { + id: r.name, + file: r.file, + description: r.description, + meaning: r.meaning, + source: r.source, + placeholders: Vec::new(), + placeholder_offset: r.placeholder_offsets.split_terminator(',').map(|x| x.parse::<usize>().unwrap()).collect(), + translations: Vec::new(), + }); + Ok(vecs) + }) + .await + .unwrap(); + + for (i, entry) in entries.iter_mut().enumerate() { + if !entry.placeholder_offset.is_empty() { + entry.placeholders = sqlx::query!( + "SELECT name,content,example FROM localization_placeholders WHERE localization_string=? ORDER BY id ASC", + ids[i]) + .fetch(&mut **db) + .map_ok(|r| api_model::LocalizationPlaceholder { + id: r.name, + content: r.content, + example: r.example, + }) + .try_collect::<Vec<_>>() + .await + .unwrap(); + } + + entry.translations = sqlx::query!( + "SELECT language,head_translation,head_placeholder_offsets,state,comment FROM translation_strings WHERE localization_string=? ORDER BY language ASC", + ids[i]) + .fetch(&mut **db) + .map_ok(|r| api_model::TranslationString { + language: r.language, + translation: r.head_translation, + placeholder_offset: r.head_placeholder_offsets.split_terminator(',').map(|x| x.parse::<usize>().unwrap()).collect(), + state: api_model::TranslationState::try_from(r.state).unwrap(), + comment: r.comment, + // TODO + reviewer: None, + }) + .try_collect::<Vec<_>>() + .await + .unwrap(); + } + + let count = sqlx::query!( + "SELECT COUNT(id) AS count FROM localization_strings WHERE translation_review=?", + translation_reviewid + ) + .fetch_one(&mut **db) + .map_ok(|r| r.count) + .await + .unwrap(); + + if count == 0 { + let reviews = sqlx::query!( + "SELECT COUNT(id) AS count FROM translation_reviews WHERE id=?", + translation_reviewid + ) + .fetch_one(&mut **db) + .map_ok(|r| r.count) + .await + .unwrap(); + if reviews == 0 { + return Err(NotFound("No such translation review")); + } + } + + let u32_count = u32::try_from(count).unwrap(); + + Ok(Json(api_model::LocalizationStrings { + offset: uw_offset, + limit: uw_limit, + total_count: u32_count, + more: uw_offset + uw_limit < u32_count, + strings: entries, + })) +} + async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result { match Db::fetch(&rocket) { Some(db) => match sqlx::migrate!().run(&**db).await { @@ -1158,6 +1494,9 @@ fn rocket_from_config(figment: Figment) -> Rocket<Build> { user_key_get, user_key_del, user_keys, + translation_review_new, + translation_review_strings, + translation_reviews, ], ) .attach(auth::stage(basepath)) |
