Skip to content

Commit

Permalink
feat(contest): support for create and list contests (#54)
Browse files Browse the repository at this point in the history
* feat(contest): support list contest

* chore: bump version

* feat(contest): support for contest

* chore: fix code lint
  • Loading branch information
fu050409 authored Dec 6, 2024
1 parent 85b5efc commit 3df5237
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changes/contest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"algohub-server": patch:feat
---

Support create and list all contests.
5 changes: 5 additions & 0 deletions .changes/create-contest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"algohub-server": patch:feat
---

Support `ContestProblem` model to display submits and acceptations.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/models/contest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,13 @@ impl From<Contest> 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,
}
15 changes: 4 additions & 11 deletions src/routes/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -19,8 +19,8 @@ pub async fn register(
db: &State<Surreal<Client>>,
register: Json<Register>,
) -> Result<OwnedCredentials> {
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()))),
Expand All @@ -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(),
)),
}
}

Expand Down
58 changes: 55 additions & 3 deletions src/routes/contest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -55,7 +65,7 @@ pub async fn list_all(
db: &State<Surreal<Client>>,
auth: Json<Credentials<'_>>,
) -> Result<Vec<UserContest>> {
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())));
}

Expand All @@ -67,7 +77,49 @@ pub async fn list_all(
}))
}

#[post("/list/<id>/problems", data = "<auth>")]
pub async fn list_problems(
db: &State<Surreal<Client>>,
id: &str,
auth: Json<Credentials<'_>>,
) -> Result<Vec<ContestProblem>> {
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/<id>", data = "<auth>")]
pub async fn get(
db: &State<Surreal<Client>>,
id: &str,
auth: Json<Credentials<'_>>,
) -> Result<UserContest> {
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<rocket::Route> {
use rocket::routes;
routes![create, add_problems, list_all]
routes![create, get, add_problems, list_problems, list_all]
}
11 changes: 3 additions & 8 deletions src/routes/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, std::io::Error> {
NamedFile::open("dist/index.html").await
}

#[get("/assets/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
#[get("/<file..>", rank = 1)]
async fn index(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("dist/").join(file)).await.ok()
}

Expand All @@ -44,7 +39,7 @@ pub async fn init_db(db_addr: &str) -> Result<Surreal<Client>> {
pub async fn rocket(db: Surreal<Client>) -> rocket::Rocket<rocket::Build> {
rocket::build()
.attach(CORS)
.mount("/", routes![index, files])
.mount("/", routes![index])
.mount("/account", account::routes())
.mount("/asset", asset::routes())
.mount("/problem", problem::routes())
Expand Down
49 changes: 48 additions & 1 deletion src/utils/contest.rs
Original file line number Diff line number Diff line change
@@ -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<Client>,
Expand Down Expand Up @@ -45,6 +45,53 @@ pub async fn list_by_owner(db: &Surreal<Client>, id: &str) -> Result<Vec<Contest
.take(0)?)
}

const ADD_PROBLEM: &str = r#"
UPDATE type::thing("contest", $id)
SET problems = array::union(problems, $problems);
"#;
pub async fn add_problems(
db: &Surreal<Client>,
id: String,
problems: Vec<Thing>,
) -> Result<Option<Contest>> {
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<Client>,
id: &str,
account_id: &str,
) -> Result<Vec<ContestProblem>> {
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(
Expand Down

0 comments on commit 3df5237

Please sign in to comment.