From 1bee9b59233505349c4175df359c23c1a6334c6f Mon Sep 17 00:00:00 2001 From: K0nnyaku Date: Wed, 4 Dec 2024 02:37:50 +0800 Subject: [PATCH] feat: list submission (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: list submission by user or contest or user and contest * release: bump version * fix: correct the URL and rename the var * fix: use map instead of match * Update .changes/submission.md * test: submisson * fix: correct URL * test: submission * fix: Update the deprecated test program * Modify the variable type * fix: Update the variable type * fix: change test * fix: lint --------- Co-authored-by: 苏向夜 <46275354+fu050409@users.noreply.github.com> --- .changes/submission.md | 5 + src/models/submission.rs | 5 +- src/routes/submission.rs | 81 ++++++++++- src/utils/submission.rs | 48 ++++++- tests/submission.rs | 285 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 413 insertions(+), 11 deletions(-) create mode 100644 .changes/submission.md create mode 100644 tests/submission.rs diff --git a/.changes/submission.md b/.changes/submission.md new file mode 100644 index 0000000..97727e7 --- /dev/null +++ b/.changes/submission.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Support list submissions by user or contest or user and contest \ No newline at end of file diff --git a/src/models/submission.rs b/src/models/submission.rs index d931f62..0f34b04 100644 --- a/src/models/submission.rs +++ b/src/models/submission.rs @@ -10,16 +10,17 @@ pub enum Status { Ready, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Submission { pub id: Option, pub lang: Language, - pub problem_id: String, + pub problem: Thing, pub code: String, pub status: Status, pub results: Vec, pub creator: Thing, + pub contest: Option, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, diff --git a/src/routes/submission.rs b/src/routes/submission.rs index ee25efb..eabbe6d 100644 --- a/src/routes/submission.rs +++ b/src/routes/submission.rs @@ -35,8 +35,7 @@ pub async fn submit( let data = data.into_inner(); let submission = submission::create(db, &data.auth.id, id, data.code, data.lang) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .await? .ok_or(Error::ServerError(Json( "Failed to submit, please try again later.".into(), )))?; @@ -57,8 +56,7 @@ pub async fn get( _auth: Json>, ) -> Result { let submission = submission::get_by_id(db, id) - .await - .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .await? .ok_or(Error::NotFound(Json("Submission not found".into())))?; Ok(Json(Response { @@ -68,7 +66,80 @@ pub async fn get( })) } +#[post("/list/user/", data = "<_auth>")] +pub async fn list_by_user( + db: &State>, + id: &str, + _auth: Json>, +) -> Result> { + let submissions = submission::list_by_user(db, ("account", id).into()).await?; + + Ok(Json(Response { + success: true, + message: "submission fetched successfully".to_string(), + data: Some(submissions), + })) +} + +#[post("/list/contest/", data = "<_auth>")] +pub async fn list_by_contest( + db: &State>, + id: &str, + _auth: Json>, +) -> Result> { + let submissions = submission::list_by_contest(db, ("contest", id).into()).await?; + + Ok(Json(Response { + success: true, + message: "submission fetched successfully".to_string(), + data: Some(submissions), + })) +} + +#[post("/list/problem/", data = "<_auth>")] +pub async fn list_by_problem( + db: &State>, + id: &str, + _auth: Json>, +) -> Result> { + let submissions = submission::list_by_problem(db, ("problem", id).into()).await?; + + Ok(Json(Response { + success: true, + message: "submission fetched successfully".to_string(), + data: Some(submissions), + })) +} + +#[post("/list/contest//", data = "<_auth>")] +pub async fn list_within_contest( + db: &State>, + contest_id: &str, + user_id: &str, + _auth: Json>, +) -> Result> { + let submissions = submission::list_within_contest( + db, + ("contest", contest_id).into(), + ("account", user_id).into(), + ) + .await?; + + Ok(Json(Response { + success: true, + message: "submission fetched successfully".to_string(), + data: Some(submissions), + })) +} + pub fn routes() -> Vec { use rocket::routes; - routes![submit, get] + routes![ + submit, + get, + list_by_user, + list_by_contest, + list_within_contest, + list_by_problem, + ] } diff --git a/src/utils/submission.rs b/src/utils/submission.rs index 90ed59c..02b06da 100644 --- a/src/utils/submission.rs +++ b/src/utils/submission.rs @@ -1,12 +1,13 @@ +use crate::models::submission::Status; use crate::models::submission::Submission; use anyhow::Result; use eval_stack::compile::Language; -use surrealdb::{engine::remote::ws::Client, Surreal}; +use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; pub async fn create( db: &Surreal, account_id: &str, - problem_id: &str, + problem: &str, code: String, lang: Language, ) -> Result> { @@ -16,12 +17,14 @@ pub async fn create( id: None, lang, code, - problem_id: problem_id.to_string(), - status: crate::models::submission::Status::InQueue, + problem: ("problem", problem).into(), + status: Status::InQueue, creator: ("account", account_id).into(), results: vec![], + contest: None, + created_at: chrono::Local::now().naive_local(), updated_at: chrono::Local::now().naive_local(), }) @@ -31,3 +34,40 @@ pub async fn create( pub async fn get_by_id(db: &Surreal, id: &str) -> Result> { Ok(db.select(("submission", id)).await?) } + +pub async fn list_by_user(db: &Surreal, creator: Thing) -> Result> { + Ok(db + .query("SELECT * FROM submission WHERE creator = $creator") + .bind(("creator", creator)) + .await? + .take(0)?) +} + +pub async fn list_by_contest(db: &Surreal, contest: Thing) -> Result> { + Ok(db + .query("SELECT * FROM submission WHERE contest = $contest") + .bind(("contest", contest)) + .await? + .take(0)?) +} + +pub async fn list_within_contest( + db: &Surreal, + contest: Thing, + creator: Thing, +) -> Result> { + Ok(db + .query("SELECT * FROM submission WHERE contest = $contest AND creator = $creator") + .bind(("contest", contest)) + .bind(("creator", creator)) + .await? + .take(0)?) +} + +pub async fn list_by_problem(db: &Surreal, problem: Thing) -> Result> { + Ok(db + .query("SELECT * FROM submission WHERE problem = $problem") + .bind(("problem", problem)) + .await? + .take(0)?) +} diff --git a/tests/submission.rs b/tests/submission.rs new file mode 100644 index 0000000..41031f1 --- /dev/null +++ b/tests/submission.rs @@ -0,0 +1,285 @@ +use algohub_server::{ + models::{ + account::Register, + contest::{AddProblems, ContestData, CreateContest, Mode, Visibility}, + problem::{CreateProblem, ProblemVisibility}, + response::{Empty, Response}, + submission::Submission, + Credentials, OwnedCredentials, OwnedId, Token, UserRecordId, + }, + routes::{index::init_db, problem::ProblemResponse, submission::CreateSubmission}, +}; +use anyhow::Result; +use rocket::local::asynchronous::Client; +pub mod utils; + +#[rocket::async_test] +async fn test_submission() -> Result<()> { + let db = init_db(utils::TEST_DB_ADDR) + .await + .expect("Failed to initialize database, shutting down"); + let rocket = algohub_server::rocket(db.clone()).await; + + let client = Client::tracked(rocket).await?; + + println!("Testing submission..."); + let response = client + .post("/account/create") + .json(&Register { + username: "fu050409".to_string(), + password: "password".to_string(), + email: "email@example.com".to_string(), + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: OwnedCredentials = data.unwrap(); + + let id = data.id; + let token = data.token; + + assert!(success); + + let response = client + .post("/problem/create") + .json(&CreateProblem { + id: &id, + token: &token, + title: "Test Problem #1", + description: "Test Description".to_string(), + input: Some("Test Input".to_string()), + output: Some("Test Output".to_string()), + samples: vec![], + hint: None, + owner: UserRecordId { + tb: "account".to_string(), + id: id.clone(), + }, + time_limit: 1000, + memory_limit: 128, + test_cases: vec![], + categories: vec![], + tags: vec![], + visibility: ProblemVisibility::Public, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let problem_data: ProblemResponse = data.unwrap(); + + assert!(success); + println!("Created problem: {:?}", &problem_data); + + let response = client + .post("/contest/create") + .json(&CreateContest { + auth: OwnedCredentials { + id: id.clone(), + token: token.clone(), + }, + data: ContestData { + name: "Test Contest".to_string(), + mode: Mode::ICPC, + visibility: Visibility::Public, + description: "Test Description".to_string(), + start_time: chrono::Local::now().naive_local(), + end_time: chrono::Local::now().naive_local() + chrono::Duration::hours(1), + owner: UserRecordId { + tb: "account".to_string(), + id: id.clone(), + }, + }, + }) + .dispatch() + .await; + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + + assert!(success); + let contest_data: OwnedId = data.unwrap(); + + let response = client + .post("/contest/problems/add") + .json(&AddProblems { + auth: OwnedCredentials { + id: id.clone(), + token: token.clone(), + }, + contest_id: &contest_data.id, + problem_ids: vec![&problem_data.id], + }) + .dispatch() + .await; + + response.into_json::>().await.unwrap(); + + let mut new_submission: Vec = Vec::new(); + + for _ in 0..5 { + let response = client + .post(format!("/code/submit/{}", problem_data.id)) + .json(&CreateSubmission { + auth: OwnedCredentials { + id: id.to_string(), + token: token.clone(), + }, + + code: "test".to_string(), + lang: eval_stack::compile::Language::Rust, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: OwnedId = data.unwrap(); + + assert!(success); + println!("Created submission: {}", data.id); + new_submission.push(data.id); + } + + let response = client + .post(format!("/code/get/{}", new_submission[0])) + .json(&Credentials { + id: &id, + token: &token, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: Submission = data.unwrap(); + + assert!(success); + println!("Get submissions by id: {:#?}", data); + + // let response = client + // .post(format!("/code/list/contest/{}", contest_data.id)) + // .json(&Credentials { + // id: &id, + // token: &token, + // }) + // .dispatch() + // .await; + + // assert_eq!(response.status().code, 200); + + // let Response { + // success, + // message: _, + // data, + // } = response.into_json().await.unwrap(); + // let data: Vec = data.unwrap(); + + // assert!(success); + // assert_eq!(data.len(), 5); + + // println!("Get submissions by contest: {:#?}", data); + + let response = client + .post(format!("/code/list/user/{}", id)) + .json(&Credentials { + id: &id, + token: &token, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: Vec = data.unwrap(); + + assert!(success); + assert_eq!(data.len(), 5); + + println!("Get submissions by user: {:#?}", data); + + // let response = client + // .post(format!("/code/list/contest/{}/{}", contest_data.id, id)) + // .json(&Credentials { + // id: &id, + // token: &token, + // }) + // .dispatch() + // .await; + + // assert_eq!(response.status().code, 200); + + // let Response { + // success, + // message: _, + // data, + // } = response.into_json().await.unwrap(); + // let data: Vec = data.unwrap(); + + // assert!(success); + // assert_eq!(data.len(), 5); + // println!("Get submissions by user within a contest: {:#?}", data); + + let response = client + .post(format!("/code/list/problem/{}", problem_data.id)) + .json(&Credentials { + id: &id, + token: &token, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: Vec = data.unwrap(); + + assert_eq!(data.len(), 5); + + assert!(success); + println!("Get submissions by problem: {:#?}", data); + + client + .post(format!("/account/delete/{}", id)) + .json(&Token { token: &token }) + .dispatch() + .await + .into_json::>() + .await + .unwrap(); + Ok(()) +}