From c3ec87657df009a603ace6e97828eec27db6e2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Fri, 6 Dec 2024 10:02:44 +0800 Subject: [PATCH 1/5] feat(contest): support list contest --- .changes/contest.md | 0 Cargo.lock | 2 +- src/models/account.rs | 3 ++- src/models/contest.rs | 48 ++++++++++++++++++++++++++++++++++++++++++- src/models/problem.rs | 2 ++ src/routes/account.rs | 20 ++++++------------ src/routes/contest.rs | 44 +++++++++++++++++++++------------------ src/routes/problem.rs | 10 ++++----- src/utils/contest.rs | 24 ++++++++-------------- 9 files changed, 94 insertions(+), 59 deletions(-) create mode 100644 .changes/contest.md diff --git a/.changes/contest.md b/.changes/contest.md new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.lock b/Cargo.lock index 13552c9..893d82d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "algohub-server" -version = "0.1.10" +version = "0.1.12" dependencies = [ "anyhow", "chrono", diff --git a/src/models/account.rs b/src/models/account.rs index 6dd64bf..fc5fdd1 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -43,6 +43,7 @@ pub struct Login<'r> { } #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(crate = "rocket::serde")] pub struct Profile { #[serde(skip_serializing_if = "Option::is_none")] pub username: Option, @@ -61,7 +62,7 @@ pub struct Profile { #[serde(skip_serializing_if = "Option::is_none")] pub sex: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub birthday: Option, + pub birthday: Option, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, diff --git a/src/models/contest.rs b/src/models/contest.rs index 51d81f3..2015ef7 100644 --- a/src/models/contest.rs +++ b/src/models/contest.rs @@ -30,7 +30,6 @@ pub struct Contest { pub start_time: chrono::NaiveDateTime, pub end_time: chrono::NaiveDateTime, - pub problems: Vec, pub owner: Thing, pub creator: Thing, @@ -71,3 +70,50 @@ pub struct RemoveProblem { pub contest_id: Thing, pub problem_id: Thing, } + +#[derive(Serialize, Deserialize)] +pub struct UserContest { + pub id: String, + + pub name: String, + pub mode: Mode, + pub visibility: Visibility, + pub description: String, + pub announcement: Option, + + pub start_time: chrono::NaiveDateTime, + pub end_time: chrono::NaiveDateTime, + + pub owner: UserRecordId, + pub creator: String, + pub updaters: Vec, + pub participants: Vec, + + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, +} + +impl From for UserContest { + fn from(value: Contest) -> Self { + UserContest { + id: value.id.unwrap().id.to_string(), + name: value.name, + mode: value.mode, + visibility: value.visibility, + description: value.description, + announcement: value.announcement, + start_time: value.start_time, + end_time: value.end_time, + owner: value.owner.into(), + creator: value.creator.to_string(), + updaters: value.updaters.iter().map(|x| x.id.to_string()).collect(), + participants: value + .participants + .iter() + .map(|x| x.id.to_string()) + .collect(), + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} diff --git a/src/models/problem.rs b/src/models/problem.rs index b788b00..53ff36d 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -54,6 +54,7 @@ pub struct Problem { pub tags: Vec, pub visibility: ProblemVisibility, + pub contest: Option, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, @@ -110,6 +111,7 @@ impl From> for Problem { categories: val.categories, tags: val.tags, visibility: val.visibility, + contest: None, created_at: chrono::Local::now().naive_local(), updated_at: chrono::Local::now().naive_local(), } diff --git a/src/routes/account.rs b/src/routes/account.rs index 2abc36c..f407f8d 100644 --- a/src/routes/account.rs +++ b/src/routes/account.rs @@ -55,14 +55,11 @@ pub async fn profile( profile: Json>, ) -> Result { account::get_by_id::(db, profile.id) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .await? .ok_or(Error::NotFound(Json("Account not found".into())))?; if session::verify(db, profile.id, profile.token).await { - account::merge_profile(db, profile.id, profile.profile.clone()) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))?; + account::merge_profile(db, profile.id, profile.profile.clone()).await?; Ok(Response { success: true, message: "Profile updated successfully".into(), @@ -70,15 +67,14 @@ pub async fn profile( } .into()) } else { - Err(Error::Unauthorized(Json("Invalid token".into()))) + Err(Error::Unauthorized(Json("Invalid credentials".into()))) } } #[get("/profile/")] pub async fn get_profile(db: &State>, id: &str) -> Result { let profile = account::get_by_identity::(db, id) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .await? .ok_or(Error::NotFound(Json("Account not found".into())))?; Ok(Response { @@ -97,13 +93,9 @@ pub async fn delete(db: &State>, id: &str, auth: Json> ))); } - account::delete(db, id) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))?; + account::delete(db, id).await?; - remove_dir_all(Path::new("content/").join(id)) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))?; + remove_dir_all(Path::new("content/").join(id)).await?; Ok(Response { success: true, diff --git a/src/routes/contest.rs b/src/routes/contest.rs index 706f0bc..7e86d7e 100644 --- a/src/routes/contest.rs +++ b/src/routes/contest.rs @@ -3,10 +3,10 @@ use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; use crate::{ models::{ - contest::{AddProblems, CreateContest}, + contest::{AddProblems, CreateContest, UserContest}, error::Error, response::{Empty, Response}, - OwnedId, + Credentials, OwnedId, }, utils::{contest, session}, Result, @@ -15,13 +15,12 @@ use crate::{ #[post("/create", data = "")] pub async fn create(db: &State>, contest: Json) -> Result { if !session::verify(db, &contest.auth.id, &contest.auth.token).await { - return Err(Error::Unauthorized(Json("Invalid session".into()))); + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); } let contest = contest.into_inner(); let contest = contest::create(db, &contest.auth.id, contest.data) - .await - .map_err(|e| Error::ServerError(Json(e.into())))? + .await? .ok_or(Error::ServerError(Json("Failed to create contest".into())))?; Ok(Json(Response { @@ -34,27 +33,15 @@ pub async fn create(db: &State>, contest: Json) - } #[post("/problems/add", data = "")] -pub async fn add_problem( +pub async fn add_problems( db: &State>, data: Json>, ) -> Result { if !session::verify(db, &data.auth.id, &data.auth.token).await { - return Err(Error::Unauthorized(Json("Invalid session".into()))); + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); } let problem = data.into_inner(); - contest::add_problems( - db, - problem.contest_id, - &problem - .problem_ids - .iter() - .map(|&p| Thing::from(("problem", p))) - .collect::>(), - ) - .await - .map_err(|e| Error::ServerError(Json(e.into())))? - .ok_or(Error::NotFound(Json("Contest not found".into())))?; Ok(Json(Response { success: true, @@ -63,7 +50,24 @@ pub async fn add_problem( })) } +#[post("/list/all", data = "")] +pub async fn list_all( + db: &State>, + auth: Json>, +) -> Result> { + if !session::verify(db, &auth.id, &auth.token).await { + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); + } + + let contests = contest::list_all(db).await?; + Ok(Json(Response { + success: true, + message: "Contests listed successfully".into(), + data: Some(contests.into_iter().map(|c| c.into()).collect()), + })) +} + pub fn routes() -> Vec { use rocket::routes; - routes![create, add_problem] + routes![create, add_problems, list_all] } diff --git a/src/routes/problem.rs b/src/routes/problem.rs index 05a7e91..7f02913 100644 --- a/src/routes/problem.rs +++ b/src/routes/problem.rs @@ -113,7 +113,7 @@ pub async fn list( ) -> Result> { let authed_id = if let Some(auth) = &data.auth { if !session::verify(db, &auth.id, &auth.token).await { - return Err(Error::Unauthorized(Json("Invalid token".into()))); + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); }; Some(auth.id.clone()) } else { @@ -125,8 +125,7 @@ pub async fn list( let account_id = if let Some(identity) = data.identity.clone() { Some( account::get_by_identity::(db, &identity) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .await? .ok_or(Error::Unauthorized(Json("Invalid identity".into())))? .id .unwrap() @@ -137,9 +136,8 @@ pub async fn list( None }; - let problems = problem::list_for_account::(db, account_id, authed_id, data.limit) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))?; + let problems = + problem::list_for_account::(db, account_id, authed_id, data.limit).await?; Ok(Json(Response { success: true, diff --git a/src/utils/contest.rs b/src/utils/contest.rs index 749c61e..3c954b0 100644 --- a/src/utils/contest.rs +++ b/src/utils/contest.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use surrealdb::{engine::remote::ws::Client, opt::PatchOp, sql::Thing, Surreal}; +use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; use crate::models::contest::{Contest, ContestData}; @@ -19,7 +19,6 @@ pub async fn create( announcement: None, start_time: contest.start_time, end_time: contest.end_time, - problems: vec![], owner: contest.owner.clone().into(), creator: ("account", creator_id).into(), updaters: vec![("account", creator_id).into()], @@ -34,23 +33,16 @@ pub async fn get(db: &Surreal, id: &str) -> Result> { Ok(db.select(("contest", id)).await?) } -pub async fn list(db: &Surreal, id: Thing) -> Result> { - Ok(db - .query("SELECT * FROM contest WHERE owner = $id") - .bind(("id", id)) - .await? - .take(0)?) +pub async fn list_all(db: &Surreal) -> Result> { + Ok(db.query("SELECT * FROM contest").await?.take(0)?) } -pub async fn add_problems( - db: &Surreal, - id: &str, - problems: &[Thing], -) -> Result> { +pub async fn list_by_owner(db: &Surreal, id: &str) -> Result> { Ok(db - .update(("contest", id)) - .patch(PatchOp::add("/problems", problems)) - .await?) + .query("SELECT * FROM contest WHERE record::id(owner) = $id") + .bind(("id", id.to_string())) + .await? + .take(0)?) } const REMOVE_PROBLEM: &str = From d85f66efb7cd02a2940eccf3238a3650fee932bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Fri, 6 Dec 2024 10:03:53 +0800 Subject: [PATCH 2/5] chore: bump version --- .changes/contest.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.changes/contest.md b/.changes/contest.md index e69de29..e18d029 100644 --- a/.changes/contest.md +++ b/.changes/contest.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Support create and list all contests. From 08b04be9f2f9e64c7d2087597d545f093ab4b9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 7 Dec 2024 03:13:19 +0800 Subject: [PATCH 3/5] feat(contest): support for contest --- .changes/create-contest.md | 5 ++++ src/models/contest.rs | 11 ++++++++ src/models/problem.rs | 2 -- src/routes/contest.rs | 56 ++++++++++++++++++++++++++++++++++++-- src/routes/index.rs | 11 ++------ src/utils/contest.rs | 50 +++++++++++++++++++++++++++++++++- 6 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 .changes/create-contest.md diff --git a/.changes/create-contest.md b/.changes/create-contest.md new file mode 100644 index 0000000..7ae0fc9 --- /dev/null +++ b/.changes/create-contest.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Support `ContestProblem` model to display submits and acceptations. diff --git a/src/models/contest.rs b/src/models/contest.rs index 2015ef7..fa25d9b 100644 --- a/src/models/contest.rs +++ b/src/models/contest.rs @@ -30,6 +30,7 @@ pub struct Contest { pub start_time: chrono::NaiveDateTime, pub end_time: chrono::NaiveDateTime, + pub problems: Vec, pub owner: Thing, pub creator: Thing, @@ -117,3 +118,13 @@ impl From for UserContest { } } } + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ContestProblem { + pub id: String, + pub title: String, + pub solved: bool, + pub submitted_count: u32, + pub accepted_count: u32, +} diff --git a/src/models/problem.rs b/src/models/problem.rs index 53ff36d..b788b00 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -54,7 +54,6 @@ pub struct Problem { pub tags: Vec, pub visibility: ProblemVisibility, - pub contest: Option, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, @@ -111,7 +110,6 @@ impl From> for Problem { categories: val.categories, tags: val.tags, visibility: val.visibility, - contest: None, created_at: chrono::Local::now().naive_local(), updated_at: chrono::Local::now().naive_local(), } diff --git a/src/routes/contest.rs b/src/routes/contest.rs index 7e86d7e..b093f01 100644 --- a/src/routes/contest.rs +++ b/src/routes/contest.rs @@ -3,7 +3,7 @@ use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; use crate::{ models::{ - contest::{AddProblems, CreateContest, UserContest}, + contest::{AddProblems, ContestProblem, CreateContest, UserContest}, error::Error, response::{Empty, Response}, Credentials, OwnedId, @@ -42,6 +42,16 @@ pub async fn add_problems( } let problem = data.into_inner(); + contest::add_problems( + db, + problem.contest_id.to_string(), + problem + .problem_ids + .iter() + .map(|&id| Thing::from(("problem", id))) + .collect(), + ) + .await?; Ok(Json(Response { success: true, @@ -67,7 +77,49 @@ pub async fn list_all( })) } +#[post("/list//problems", data = "")] +pub async fn list_problems( + db: &State>, + id: &str, + auth: Json>, +) -> Result> { + if !session::verify(db, &auth.id, &auth.token).await { + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); + } + + let problems = contest::list_problems(db, id, auth.id).await?; + + dbg!(&problems); + + Ok(Json(Response { + success: true, + message: "Problems listed successfully".into(), + data: Some(problems), + })) +} + +#[post("/get/", data = "")] +pub async fn get( + db: &State>, + id: &str, + auth: Json>, +) -> Result { + if !session::verify(db, &auth.id, &auth.token).await { + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); + } + + let contest = contest::get(db, id) + .await? + .ok_or(Error::NotFound(Json("Contest not found".into())))?; + + Ok(Json(Response { + success: true, + message: "Contest retrieved successfully".into(), + data: Some(contest.into()), + })) +} + pub fn routes() -> Vec { use rocket::routes; - routes![create, add_problems, list_all] + routes![create, get, add_problems, list_problems, list_all] } diff --git a/src/routes/index.rs b/src/routes/index.rs index 1687b9f..ac03cdb 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -12,13 +12,8 @@ use rocket::fs::NamedFile; use surrealdb::engine::remote::ws::Client; use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; -#[get("/")] -async fn index() -> Result { - NamedFile::open("dist/index.html").await -} - -#[get("/assets/")] -async fn files(file: PathBuf) -> Option { +#[get("/", rank = 1)] +async fn index(file: PathBuf) -> Option { NamedFile::open(Path::new("dist/").join(file)).await.ok() } @@ -44,7 +39,7 @@ pub async fn init_db(db_addr: &str) -> Result> { pub async fn rocket(db: Surreal) -> rocket::Rocket { rocket::build() .attach(CORS) - .mount("/", routes![index, files]) + .mount("/", routes![index]) .mount("/account", account::routes()) .mount("/asset", asset::routes()) .mount("/problem", problem::routes()) diff --git a/src/utils/contest.rs b/src/utils/contest.rs index 3c954b0..e1b6aef 100644 --- a/src/utils/contest.rs +++ b/src/utils/contest.rs @@ -1,7 +1,7 @@ use anyhow::Result; use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; -use crate::models::contest::{Contest, ContestData}; +use crate::models::contest::{Contest, ContestData, ContestProblem}; pub async fn create( db: &Surreal, @@ -19,6 +19,7 @@ pub async fn create( announcement: None, start_time: contest.start_time, end_time: contest.end_time, + problems: vec![], owner: contest.owner.clone().into(), creator: ("account", creator_id).into(), updaters: vec![("account", creator_id).into()], @@ -45,6 +46,53 @@ pub async fn list_by_owner(db: &Surreal, id: &str) -> Result, + id: String, + problems: Vec, +) -> Result> { + Ok(db + .query(ADD_PROBLEM) + .bind(("id", id)) + .bind(("problems", problems)) + .await? + .take(0)?) +} + +const LIST_PROBLEMS: &str = r#" +SELECT title, record::id(id) AS id, count( + SELECT VALUE true + FROM submission WHERE record::id(creator) == $account_id AND problem == $parent.id + AND judge_result.status.type == "accepted" +) > 0 AS solved, +count( + SELECT record::id(creator) + FROM submission WHERE record::id(creator) == $account_id AND problem == $parent.id +) AS submittedCount, +count( + SELECT record::id(creator) + FROM submission WHERE record::id(creator) == $account_id AND problem == $parent.id + AND judge_result.status.type == "accepted" +) AS acceptedCount +FROM type::thing("contest", $id).problems; +"#; +pub async fn list_problems( + db: &Surreal, + id: &str, + account_id: &str, +) -> Result> { + Ok(db + .query(LIST_PROBLEMS) + .bind(("id", id.to_string())) + .bind(("account_id", account_id.to_string())) + .await? + .take(0)?) +} + const REMOVE_PROBLEM: &str = "UPDATE contest SET problems -= type::thing(\"problem\", $problem) WHERE record::id(id) = $id"; pub async fn remove_problem( From 41cfe39fdb7a6d29921351c228ceb515b2aa69e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 7 Dec 2024 03:41:31 +0800 Subject: [PATCH 4/5] chore: fix code lint --- Cargo.lock | 2 +- src/routes/account.rs | 15 ++++----------- src/routes/contest.rs | 6 +++--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 893d82d..8950848 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "algohub-server" -version = "0.1.12" +version = "0.1.13" dependencies = [ "anyhow", "chrono", diff --git a/src/routes/account.rs b/src/routes/account.rs index f407f8d..6f9510d 100644 --- a/src/routes/account.rs +++ b/src/routes/account.rs @@ -6,7 +6,7 @@ use surrealdb::{engine::remote::ws::Client, Surreal}; use crate::{ models::{ account::{Login, MergeProfile, Profile, Register}, - error::{Error, ErrorResponse}, + error::Error, response::{Empty, Response}, OwnedCredentials, Record, Token, }, @@ -19,8 +19,8 @@ pub async fn register( db: &State>, register: Json, ) -> Result { - match account::create(db, register.into_inner()).await { - Ok(Some(account)) => { + match account::create(db, register.into_inner()).await? { + Some(account) => { let token = match session::create(db, account.id.clone().unwrap()).await { Ok(session) => session.unwrap().token, Err(e) => return Err(Error::ServerError(Json(e.to_string().into()))), @@ -33,19 +33,12 @@ pub async fn register( } .into()) } - Ok(None) => Ok(Response { + None => Ok(Response { success: false, message: "Specified username or email already exists".to_string(), data: None, } .into()), - Err(e) => Err(Error::ServerError( - ErrorResponse { - success: false, - message: e.to_string(), - } - .into(), - )), } } diff --git a/src/routes/contest.rs b/src/routes/contest.rs index b093f01..143ff0a 100644 --- a/src/routes/contest.rs +++ b/src/routes/contest.rs @@ -65,7 +65,7 @@ pub async fn list_all( db: &State>, auth: Json>, ) -> Result> { - if !session::verify(db, &auth.id, &auth.token).await { + if !session::verify(db, auth.id, auth.token).await { return Err(Error::Unauthorized(Json("Invalid credentials".into()))); } @@ -83,7 +83,7 @@ pub async fn list_problems( id: &str, auth: Json>, ) -> Result> { - if !session::verify(db, &auth.id, &auth.token).await { + if !session::verify(db, auth.id, auth.token).await { return Err(Error::Unauthorized(Json("Invalid credentials".into()))); } @@ -104,7 +104,7 @@ pub async fn get( id: &str, auth: Json>, ) -> Result { - if !session::verify(db, &auth.id, &auth.token).await { + if !session::verify(db, auth.id, auth.token).await { return Err(Error::Unauthorized(Json("Invalid credentials".into()))); } From 8aacda7e3d2cf01e6edf107856a9d1979998619c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 7 Dec 2024 04:56:56 +0800 Subject: [PATCH 5/5] feat(rank): support rank --- .changes/index.md | 5 +++++ .changes/rank.md | 5 +++++ .gitignore | 3 +++ src/models/contest.rs | 9 +++++++++ src/routes/contest.rs | 21 ++++++++++++++++++++- src/routes/index.rs | 9 +++++++-- src/utils/contest.rs | 34 +++++++++++++++++++++++++++++++++- 7 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 .changes/index.md create mode 100644 .changes/rank.md diff --git a/.changes/index.md b/.changes/index.md new file mode 100644 index 0000000..3ec940b --- /dev/null +++ b/.changes/index.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:fix +--- + +Fixed index pages cannot be displayed correctly. diff --git a/.changes/rank.md b/.changes/rank.md new file mode 100644 index 0000000..3b82eca --- /dev/null +++ b/.changes/rank.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Support enpoints for getting rank list. diff --git a/.gitignore b/.gitignore index 045c110..faf75ff 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ # Node.js /node_modules + +# Assets +/dist diff --git a/src/models/contest.rs b/src/models/contest.rs index c447ced..d5c50e0 100644 --- a/src/models/contest.rs +++ b/src/models/contest.rs @@ -127,3 +127,12 @@ pub struct ContestProblem { pub submitted_count: u32, pub accepted_count: u32, } + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ContestRank { + name: String, + problem_id: String, + accepted: bool, + wrongs: u32, +} diff --git a/src/routes/contest.rs b/src/routes/contest.rs index 143ff0a..40554ba 100644 --- a/src/routes/contest.rs +++ b/src/routes/contest.rs @@ -3,7 +3,7 @@ use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; use crate::{ models::{ - contest::{AddProblems, ContestProblem, CreateContest, UserContest}, + contest::{AddProblems, ContestProblem, ContestRank, CreateContest, UserContest}, error::Error, response::{Empty, Response}, Credentials, OwnedId, @@ -119,6 +119,25 @@ pub async fn get( })) } +#[post("/rank/", data = "")] +pub async fn rank( + db: &State>, + id: &str, + auth: Json>, +) -> Result> { + if !session::verify(db, auth.id, auth.token).await { + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); + } + + let rank = contest::rank(db, id).await?; + + Ok(Json(Response { + success: true, + message: "Contest rank retrieved successfully".into(), + data: Some(rank), + })) +} + pub fn routes() -> Vec { use rocket::routes; routes![create, get, add_problems, list_problems, list_all] diff --git a/src/routes/index.rs b/src/routes/index.rs index ac03cdb..ee7638b 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -12,8 +12,13 @@ use rocket::fs::NamedFile; use surrealdb::engine::remote::ws::Client; use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; +#[get("/")] +async fn index() -> Result { + NamedFile::open("dist/index.html").await +} + #[get("/", rank = 1)] -async fn index(file: PathBuf) -> Option { +async fn files(file: PathBuf) -> Option { NamedFile::open(Path::new("dist/").join(file)).await.ok() } @@ -39,7 +44,7 @@ pub async fn init_db(db_addr: &str) -> Result> { pub async fn rocket(db: Surreal) -> rocket::Rocket { rocket::build() .attach(CORS) - .mount("/", routes![index]) + .mount("/", routes![index, files]) .mount("/account", account::routes()) .mount("/asset", asset::routes()) .mount("/problem", problem::routes()) diff --git a/src/utils/contest.rs b/src/utils/contest.rs index ed5897c..5025cfc 100644 --- a/src/utils/contest.rs +++ b/src/utils/contest.rs @@ -1,7 +1,7 @@ use anyhow::Result; use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; -use crate::models::contest::{Contest, ContestData, ContestProblem}; +use crate::models::contest::{Contest, ContestData, ContestProblem, ContestRank}; pub async fn create( db: &Surreal, @@ -106,3 +106,35 @@ pub async fn remove_problem( .await? .take(0)?) } + +const RANK_QUERY: &str = r#" +SELECT VALUE array::map((SELECT VALUE id FROM type::thing("contest", $id).problems), |$problem| { + LET $submissions = SELECT judge_result.status.type AS status, created_at + FROM submission WHERE problem.id == $problem AND $parent.id == creator ORDER BY created_at ASC; + LET $first_accepted = array::find_index($submissions, |$submission| { + RETURN $submission.status == "accepted" + }); + RETURN IF $first_accepted { + RETURN { + name: $problem.title, + problem_id: record::id($problem), + accepted: true, + wrongs: count($submissions) - $first_accepted - 1, + } + } ELSE { + RETURN { + name: $problem.title, + problem_id: record::id($problem), + accepted: false, + wrongs: count($submissions), + } + } +}) FROM array::distinct(SELECT VALUE creator FROM submission) +"#; +pub async fn rank(db: &Surreal, id: &str) -> Result> { + Ok(db + .query(RANK_QUERY) + .bind(("id", id.to_string())) + .await? + .take(0)?) +}