diff --git a/.changes/contest.md b/.changes/contest.md new file mode 100644 index 0000000..e18d029 --- /dev/null +++ b/.changes/contest.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Support create and list all contests. 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 =