diff --git a/backend/migration/src/m20231207_000001_create_table.rs b/backend/migration/src/m20231207_000001_create_table.rs index 252c6b3..b7d2519 100644 --- a/backend/migration/src/m20231207_000001_create_table.rs +++ b/backend/migration/src/m20231207_000001_create_table.rs @@ -115,6 +115,7 @@ enum Testcase { Input, Output, Score, + Order, } #[derive(Iden)] enum Token { @@ -488,6 +489,12 @@ impl MigrationTrait for Migration { .not_null() .default(0), ) + .col( + ColumnDef::new(Testcase::Order) + .float() + .not_null() + .default(0.0), + ) .to_owned(), ) .await?; diff --git a/backend/src/endpoint/mod.rs b/backend/src/endpoint/mod.rs index b299003..cc48546 100644 --- a/backend/src/endpoint/mod.rs +++ b/backend/src/endpoint/mod.rs @@ -22,7 +22,7 @@ use tonic::*; use tracing::*; use uuid::Uuid; -use crate::entity::util::filter::*; +use crate::entity::util::{filter::*, order::*}; use crate::util::with::*; use crate::util::{auth::RoleLv, bound::BoundCheck, duplicate::*, error::Error, time::*}; use crate::{fill_active_model, fill_exist_active_model, server::ArcServer, TonicStream}; diff --git a/backend/src/endpoint/problem.rs b/backend/src/endpoint/problem.rs index 64e953f..7aa5228 100644 --- a/backend/src/endpoint/problem.rs +++ b/backend/src/endpoint/problem.rs @@ -223,6 +223,7 @@ impl Problem for ArcServer { req.get_or_insert(|req| async move { let (contest, model) = tokio::try_join!( contest::Entity::write_by_id(req.contest_id, &auth)? + .into_partial_model() .one(self.db.deref()) .instrument(debug_span!("find_parent").or_current()), Entity::write_by_id(req.problem_id, &auth)? @@ -231,13 +232,20 @@ impl Problem for ArcServer { ) .map_err(Into::::into)?; - contest.ok_or(Error::NotInDB)?; + let contest: contest::IdModel = contest.ok_or(Error::NotInDB)?; let mut model = model.ok_or(Error::NotInDB)?.into_active_model(); if let ActiveValue::Set(Some(v)) = model.contest_id { return Err(Error::AlreadyExist("problem already linked")); } + let order = contest + .with_db(self.db.deref()) + .insert_last() + .await + .map_err(Into::::into)?; + model.order = ActiveValue::Set(order); + model.contest_id = ActiveValue::Set(Some(req.problem_id)); model .save(self.db.deref()) @@ -292,6 +300,51 @@ impl Problem for ArcServer { .with_grpc() .into() } + #[instrument( + skip_all, + level = "info", + name = "oj.backend.Problem/insert", + err(level = "debug", Display) + )] + async fn insert(&self, req: Request) -> Result, Status> { + let (auth, req) = self.rate_limit(req).in_current_span().await?; + auth.perm().super_user()?; + + req.get_or_insert(|req| async move { + let contest: contest::IdModel = contest::Entity::find_by_id(req.contest_id) + .with_auth(&auth) + .write()? + .into_partial_model() + .one(self.db.deref()) + .instrument(info_span!("fetch").or_current()) + .await + .map_err(Into::::into)? + .ok_or(Error::NotInDB)?; + + let order = match req.pivot_id { + None => contest.with_db(self.db.deref()).insert_front().await, + Some(id) => contest.with_db(self.db.deref()).insert_after(id).await, + } + .map_err(Into::::into)?; + + Entity::write_filter( + Entity::update(ActiveModel { + id: ActiveValue::Set(req.problem_id), + order: ActiveValue::Set(order), + ..Default::default() + }), + &auth, + )? + .exec(self.db.deref()) + .await + .map_err(Into::::into)?; + + Ok(()) + }) + .await + .with_grpc() + .into() + } #[instrument( skip_all, level = "info", diff --git a/backend/src/endpoint/testcase.rs b/backend/src/endpoint/testcase.rs index 77a2a70..77c2331 100644 --- a/backend/src/endpoint/testcase.rs +++ b/backend/src/endpoint/testcase.rs @@ -185,6 +185,7 @@ impl Testcase for ArcServer { req.get_or_insert(|req| async move { let (problem, model) = tokio::try_join!( problem::Entity::write_by_id(req.problem_id, &auth)? + .into_partial_model() .one(self.db.deref()) .instrument(debug_span!("find_parent").or_current()), Entity::write_by_id(req.testcase_id, &auth)? @@ -193,12 +194,20 @@ impl Testcase for ArcServer { ) .map_err(Into::::into)?; - problem.ok_or(Error::NotInDB)?; + let problem: problem::IdModel = problem.ok_or(Error::NotInDB)?; + let mut model = model.ok_or(Error::NotInDB)?.into_active_model(); if let ActiveValue::Set(Some(v)) = model.problem_id { return Err(Error::AlreadyExist("testcase already linked")); } + let order = problem + .with_db(self.db.deref()) + .insert_last() + .await + .map_err(Into::::into)?; + model.order = ActiveValue::Set(order); + model.problem_id = ActiveValue::Set(Some(req.problem_id)); model .update(self.db.deref()) @@ -247,6 +256,51 @@ impl Testcase for ArcServer { .with_grpc() .into() } + #[instrument( + skip_all, + level = "info", + name = "oj.backend.Testcase/insert", + err(level = "debug", Display) + )] + async fn insert(&self, req: Request) -> Result, Status> { + let (auth, req) = self.rate_limit(req).in_current_span().await?; + auth.perm().super_user()?; + + req.get_or_insert(|req| async move { + let problem: problem::IdModel = problem::Entity::find_by_id(req.problem_id) + .with_auth(&auth) + .write()? + .into_partial_model() + .one(self.db.deref()) + .instrument(info_span!("fetch").or_current()) + .await + .map_err(Into::::into)? + .ok_or(Error::NotInDB)?; + + let order = match req.pivot_id { + None => problem.with_db(self.db.deref()).insert_front().await, + Some(id) => problem.with_db(self.db.deref()).insert_after(id).await, + } + .map_err(Into::::into)?; + + Entity::write_filter( + Entity::update(ActiveModel { + id: ActiveValue::Set(req.testcase_id), + order: ActiveValue::Set(order), + ..Default::default() + }), + &auth, + )? + .exec(self.db.deref()) + .await + .map_err(Into::::into)?; + + Ok(()) + }) + .await + .with_grpc() + .into() + } #[instrument( skip_all, level = "info", diff --git a/backend/src/entity/testcase.rs b/backend/src/entity/testcase.rs index bc21136..3584978 100644 --- a/backend/src/entity/testcase.rs +++ b/backend/src/entity/testcase.rs @@ -1,7 +1,7 @@ use super::*; // FIXME: use partial model -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "testcase")] pub struct Model { #[sea_orm(primary_key)] @@ -14,6 +14,7 @@ pub struct Model { #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] pub output: Vec, pub score: u32, + pub order: f32, } #[derive(DerivePartialModel, FromQueryResult)] diff --git a/backend/src/entity/util/helper.rs b/backend/src/entity/util/helper.rs index 90b9e0b..0c3694d 100644 --- a/backend/src/entity/util/helper.rs +++ b/backend/src/entity/util/helper.rs @@ -1,4 +1,6 @@ -//! a collection of helper function +//! a collection of helper function for low level sql query +//! +//! This module use extensively of [`sea_query`], which make it extreme unsafe to use use std::ops::Deref; diff --git a/backend/src/entity/util/mod.rs b/backend/src/entity/util/mod.rs index 083e96a..b182949 100644 --- a/backend/src/entity/util/mod.rs +++ b/backend/src/entity/util/mod.rs @@ -1,3 +1,4 @@ pub mod filter; pub mod helper; +pub mod order; pub mod paginator; diff --git a/backend/src/entity/util/order.rs b/backend/src/entity/util/order.rs new file mode 100644 index 0000000..f5e02c7 --- /dev/null +++ b/backend/src/entity/util/order.rs @@ -0,0 +1,111 @@ +use crate::util::with::WithDB; +use crate::util::with::WithDBTrait; +use sea_orm::*; +use tonic::async_trait; + +#[async_trait] +pub trait ReOrder { + async fn insert_last(self) -> Result; + async fn insert_after(self, pivot: i32) -> Result; + async fn insert_front(self) -> Result; +} + +#[derive(Default, EnumIter, DeriveColumn, Clone, Copy, Debug)] +enum RetValue { + #[default] + RetValue, +} +pub mod testcase { + use super::*; + use crate::entity::problem; + use crate::entity::testcase::{Column, Entity}; + + impl WithDBTrait for problem::IdModel {} + + #[async_trait] + impl ReOrder for WithDB<'_, problem::IdModel> { + async fn insert_last(self) -> Result { + Entity::find() + .filter(Column::ProblemId.eq(self.1.id)) + .select_only() + .column_as(Column::Order.max(), RetValue::default()) + .into_values::<_, RetValue>() + .one(self.0) + .await + .map(|x: Option| x.unwrap_or_default() + 1.0) + } + async fn insert_after(self, pivot: i32) -> Result { + let vals: Vec = Entity::find() + .filter(Column::ProblemId.eq(self.1.id)) + .filter(Column::Order.gte(pivot)) + .select_only() + .column_as(Column::Order.min(), RetValue::default()) + .limit(2) + .into_values::<_, RetValue>() + .all(self.0) + .await?; + Ok(match vals.len() { + 1 => vals[0] + 1.0, + 2 => (vals[0] + vals[1]) * 0.5, + _ => 0.0, + }) + } + async fn insert_front(self) -> Result { + Entity::find() + .filter(Column::ProblemId.eq(self.1.id)) + .select_only() + .column_as(Column::Order.min(), RetValue::default()) + .into_values::<_, RetValue>() + .one(self.0) + .await + .map(|x: Option| x.unwrap_or_default() - 1.0) + } + } +} + +pub mod contest { + use super::*; + use crate::entity::contest; + use crate::entity::problem::{Column, Entity}; + + impl WithDBTrait for contest::IdModel {} + #[async_trait] + impl ReOrder for WithDB<'_, contest::IdModel> { + async fn insert_last(self) -> Result { + Entity::find() + .filter(Column::ContestId.eq(self.1.id)) + .select_only() + .column_as(Column::Order.max(), RetValue::default()) + .into_values::<_, RetValue>() + .one(self.0) + .await + .map(|x: Option| x.unwrap_or_default() + 1.0) + } + async fn insert_after(self, pivot: i32) -> Result { + let vals: Vec = Entity::find() + .filter(Column::ContestId.eq(self.1.id)) + .filter(Column::Order.gte(pivot)) + .select_only() + .column_as(Column::Order.min(), RetValue::default()) + .limit(2) + .into_values::<_, RetValue>() + .all(self.0) + .await?; + Ok(match vals.len() { + 1 => vals[0] + 1.0, + 2 => (vals[0] + vals[1]) * 0.5, + _ => 0.0, + }) + } + async fn insert_front(self) -> Result { + Entity::find() + .filter(Column::ContestId.eq(self.1.id)) + .select_only() + .column_as(Column::Order.min(), RetValue::default()) + .into_values::<_, RetValue>() + .one(self.0) + .await + .map(|x: Option| x.unwrap_or_default() - 1.0) + } + } +} diff --git a/backend/src/util/duplicate.rs b/backend/src/util/duplicate.rs index df4e807..0f47295 100644 --- a/backend/src/util/duplicate.rs +++ b/backend/src/util/duplicate.rs @@ -114,3 +114,6 @@ create_cache!(AddEducationToProblemRequest, ()); create_cache!(UploadRequest, UploadResponse); create_cache!(AddTestcaseToProblemRequest, ()); create_cache!(AddProblemToContestRequest, ()); + +create_cache!(InsertProblemRequest, ()); +create_cache!(InsertTestcaseRequest, ()); diff --git a/backend/src/util/rate_limit.rs b/backend/src/util/rate_limit.rs index bb8950c..d85d03b 100644 --- a/backend/src/util/rate_limit.rs +++ b/backend/src/util/rate_limit.rs @@ -214,3 +214,14 @@ impl RateLimit for RefreshRequest { 50 } } + +impl RateLimit for InsertProblemRequest { + fn get_cost(&self) -> u32 { + 3 + (self.pivot_id.is_some() as u32) * 2 + } +} +impl RateLimit for InsertTestcaseRequest { + fn get_cost(&self) -> u32 { + 3 + (self.pivot_id.is_some() as u32) * 2 + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile index b65de6f..c8eee02 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -27,7 +27,7 @@ RUN rustup target add ${ARCH}-unknown-linux-musl ENV LEPTOS_OUTPUT_NAME="mdoj" ENV LEPTOS_BIN_TARGET_TRIPLE=${ARCH}-unknown-linux-musl -RUN cargo leptos build -p frontend --release --precompress -vv +RUN cargo leptos build -p frontend --bin-features compress --release --precompress -vv FROM scratch WORKDIR /config diff --git a/grpc/proto/backend.proto b/grpc/proto/backend.proto index 579acef..b3c1669 100644 --- a/grpc/proto/backend.proto +++ b/grpc/proto/backend.proto @@ -207,6 +207,13 @@ message ListProblemResponse { required uint64 remain = 3; } +message InsertProblemRequest{ + optional string request_id = 1; + optional int32 pivot_id = 2; + required int32 problem_id = 3; + required int32 contest_id = 4; +} + service Problem { rpc List(ListProblemRequest) returns (ListProblemResponse); rpc FullInfo(Id) returns (ProblemFullInfo); @@ -218,6 +225,7 @@ service Problem { rpc AddToContest(AddProblemToContestRequest) returns (google.protobuf.Empty); rpc RemoveFromContest(AddProblemToContestRequest) returns (google.protobuf.Empty); + rpc Insert(InsertProblemRequest) returns (google.protobuf.Empty); rpc Publish(PublishRequest) returns (google.protobuf.Empty); rpc Unpublish(PublishRequest) returns (google.protobuf.Empty); @@ -555,6 +563,13 @@ message ListTestcaseRequest { required int64 offset = 4; } +message InsertTestcaseRequest{ + optional string request_id = 1; + optional int32 pivot_id = 2; + required int32 testcase_id = 3; + required int32 problem_id = 4; +} + // Testcase service Testcase { // list owned testcase @@ -567,6 +582,8 @@ service Testcase { rpc RemoveFromProblem(AddTestcaseToProblemRequest) returns (google.protobuf.Empty); + rpc Insert(InsertTestcaseRequest) returns (google.protobuf.Empty); + rpc FullInfoByProblem(ListTestcaseByProblemRequest) returns (TestcaseFullInfo); }