From a1f0fc91544e78cee331ea84ecd20921bc1f4b61 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:39:37 +0800 Subject: [PATCH 01/18] chore(Frontend): :construction: Mpve navbar and footer into nest router --- frontend/src/app.rs | 3 +- frontend/src/pages/about.rs | 1 - frontend/src/pages/contest.rs | 2 - frontend/src/pages/contests.rs | 1 - frontend/src/pages/create/problem.rs | 2 - frontend/src/pages/home.rs | 1 - frontend/src/pages/login.rs | 1 - frontend/src/pages/pages.rs | 56 +++++++++++++++++++--------- frontend/src/pages/problems.rs | 2 - frontend/src/pages/rank.rs | 1 - frontend/src/pages/submission.rs | 1 - 11 files changed, 40 insertions(+), 31 deletions(-) diff --git a/frontend/src/app.rs b/frontend/src/app.rs index e96a5e72..3d046602 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -2,7 +2,7 @@ use leptos::*; use leptos_meta::*; use leptos_router::*; -use crate::{components::*, config::ProvideConfig, pages::Pages}; +use crate::{components::ProvideToast, config::ProvideConfig, pages::Pages}; // use tracing_subscriber::fmt::format::Pretty; // use tracing_subscriber::prelude::*; // use tracing_web::{performance_layer, MakeWebConsoleWriter}; @@ -20,7 +20,6 @@ pub fn App() -> impl IntoView { <div class="bg-black-950 w-full min-h-dvh flex flex-col text-text"> - <Navbar/> <Pages/> </div> </Router> diff --git a/frontend/src/pages/about.rs b/frontend/src/pages/about.rs index f34fe697..ee7f57cb 100644 --- a/frontend/src/pages/about.rs +++ b/frontend/src/pages/about.rs @@ -6,6 +6,5 @@ use crate::components::*; pub fn About() -> impl IntoView { view! { <h1>About</h1> - <Footer/> } } diff --git a/frontend/src/pages/contest.rs b/frontend/src/pages/contest.rs index d9dc5e61..e7ff3446 100644 --- a/frontend/src/pages/contest.rs +++ b/frontend/src/pages/contest.rs @@ -4,7 +4,5 @@ use leptos::*; pub fn About() -> impl IntoView { view! { <h1>About</h1> - - <Footer/> } } diff --git a/frontend/src/pages/contests.rs b/frontend/src/pages/contests.rs index 2f8637ee..d239ab2e 100644 --- a/frontend/src/pages/contests.rs +++ b/frontend/src/pages/contests.rs @@ -6,6 +6,5 @@ use crate::components::*; pub fn Contests() -> impl IntoView { view! { <h1>Contest</h1> - <Footer/> } } diff --git a/frontend/src/pages/create/problem.rs b/frontend/src/pages/create/problem.rs index 0f5e34af..5647bd41 100644 --- a/frontend/src/pages/create/problem.rs +++ b/frontend/src/pages/create/problem.rs @@ -131,7 +131,5 @@ pub fn Problem() -> impl IntoView { </form> </main> - - <Footer/> } } diff --git a/frontend/src/pages/home.rs b/frontend/src/pages/home.rs index 5b410295..61a88a2d 100644 --- a/frontend/src/pages/home.rs +++ b/frontend/src/pages/home.rs @@ -12,6 +12,5 @@ pub fn Home() -> impl IntoView { ToastVariant::Error, view! { "This is a error message............." }.into_view(), )>Click Me</Button> - <Footer/> } } diff --git a/frontend/src/pages/login.rs b/frontend/src/pages/login.rs index 42dc69ca..19b3a8e3 100644 --- a/frontend/src/pages/login.rs +++ b/frontend/src/pages/login.rs @@ -109,6 +109,5 @@ pub fn Login() -> impl IntoView { </div> </form> </main> - <Footer/> } } diff --git a/frontend/src/pages/pages.rs b/frontend/src/pages/pages.rs index 6d815a73..3386b96f 100644 --- a/frontend/src/pages/pages.rs +++ b/frontend/src/pages/pages.rs @@ -3,7 +3,7 @@ use leptos_router::*; use leptos_use::*; use problem::ProblemRouter; -use crate::{errors::NotFound, grpc, pages::*, session::*}; +use crate::{components::*, errors::NotFound, grpc, pages::*, session::*}; /// |Permission|Root|Admin|SuperUser|User|Guest /// |:-|:-:|:-:|:-:|:-:|:-:| @@ -29,26 +29,48 @@ pub fn Pages() -> impl IntoView { }) }; + let show_footer = move || { + !use_location() + .pathname + .with(|path| path.starts_with("/problem/")) + }; + let page_wrapper = move || { + view! { + <Navbar/> + <Outlet/> + <Show when=show_footer fallback=|| ()> + <Footer/> + </Show> + } + }; + view! { <Routes> - <Route path="" view=Home/> - <Route path="/problems" view=Problems/> - <Route path="/submissions" view=Submission/> - <Route path="/contests" view=Contests/> - <Route path="/about" view=About/> - <Route path="/rank" view=Rank/> - <ProblemRouter/> + <Route path="" view=page_wrapper> + <Route path="" view=Home/> + <Route path="/problems" view=Problems/> + <Route path="/submissions" view=Submission/> + <Route path="/contests" view=Contests/> + <Route path="/about" view=About/> + <Route path="/rank" view=Rank/> + <ProblemRouter/> - <ProtectedRoute path="/login" redirect_path="/" condition=is_none(token) view=Login/> - <ProtectedRoute - path="/create/problem" - redirect_path="/login" - condition=can_create_problem_or_contest - view=create::Problem - /> + <ProtectedRoute + path="/login" + redirect_path="/" + condition=is_none(token) + view=Login + /> + <ProtectedRoute + path="/create/problem" + redirect_path="/login" + condition=can_create_problem_or_contest + view=create::Problem + /> - // Fallback - <Route path="/*any" view=NotFound/> + // Fallback + <Route path="/*any" view=NotFound/> + </Route> </Routes> } } diff --git a/frontend/src/pages/problems.rs b/frontend/src/pages/problems.rs index 6104745d..62c10c3e 100644 --- a/frontend/src/pages/problems.rs +++ b/frontend/src/pages/problems.rs @@ -7,7 +7,5 @@ use crate::components::*; pub fn Problems() -> impl IntoView { view! { <div class="container flex items-center justify-between text-lg"></div> - - <Footer/> } } diff --git a/frontend/src/pages/rank.rs b/frontend/src/pages/rank.rs index 354ea4c0..49e50a4b 100644 --- a/frontend/src/pages/rank.rs +++ b/frontend/src/pages/rank.rs @@ -6,6 +6,5 @@ use crate::components::*; pub fn Rank() -> impl IntoView { view! { <h1>Rank</h1> - <Footer/> } } diff --git a/frontend/src/pages/submission.rs b/frontend/src/pages/submission.rs index f6bc8264..84abd797 100644 --- a/frontend/src/pages/submission.rs +++ b/frontend/src/pages/submission.rs @@ -6,6 +6,5 @@ use crate::components::*; pub fn Submission() -> impl IntoView { view! { <h1>Submission</h1> - <Footer/> } } From fc2f5df2a9c93a281c6df809304a5b4eca99c1f6 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:38:26 +0800 Subject: [PATCH 02/18] chore(Frontend): :heavy_plus_sign: Add leptos query --- Cargo.lock | 42 ++++++++++++++++++++++++++++++++++++++++++ frontend/Cargo.toml | 22 ++++++++++++++++++++-- frontend/src/main.rs | 4 ++-- grpc/build.rs | 8 ++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbefa136..c3ab037e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,6 +567,12 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "async_cell" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "834eee9ce518130a3b4d5af09ecc43e9d6b57ee76613f227a1ddd6b77c7a62bc" + [[package]] name = "atoi" version = "2.0.0" @@ -1887,6 +1893,7 @@ dependencies = [ "actix-web", "cfg-if", "console_error_panic_hook", + "cookie 0.18.1", "gloo", "grpc", "http 0.2.12", @@ -1898,6 +1905,8 @@ dependencies = [ "leptos_animated_for", "leptos_icons", "leptos_meta", + "leptos_query", + "leptos_query_devtools", "leptos_router", "lol_alloc", "num-traits", @@ -3390,6 +3399,39 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos_query" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210f99102170bc3d227148d78c2fdd79e89f724981a697c526f294c85a0b0521" +dependencies = [ + "async-trait", + "async_cell", + "cfg-if", + "futures", + "futures-channel", + "gloo-timers 0.3.0", + "js-sys", + "leptos", + "slotmap", + "tokio", + "web-sys", +] + +[[package]] +name = "leptos_query_devtools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da420d373103de2681e69a15d040054a9023e5135a9a39bfaa0adf2af41fd2" +dependencies = [ + "cfg-if", + "js-sys", + "leptos", + "leptos_query", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "leptos_reactive" version = "0.6.13" diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 83150c77..89cc1a6b 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -35,6 +35,9 @@ turf = "0.9.2" leptos_animated_for = "0.4.7" num-traits = "0.2.19" lol_alloc = "0.4.1" +leptos_query = "0.5.3" +cookie = "0.18.1" +leptos_query_devtools = "0.1.3" [dependencies.uuid] version = "1.7.0" @@ -79,8 +82,22 @@ features = ["prost"] workspace = true [features] -csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr", "uuid/js"] -hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +csr = [ + "leptos/csr", + "leptos_meta/csr", + "leptos_router/csr", + "uuid/js", + "leptos_query/csr", + "leptos_query_devtools/csr", +] +hydrate = [ + "leptos/hydrate", + "leptos_meta/hydrate", + "leptos_router/hydrate", + "leptos_query/hydrate", + # This is intended, hydrate feature for devtool is csr + "leptos_query_devtools/csr", +] ssr = [ "dep:actix-files", "dep:actix-web", @@ -93,6 +110,7 @@ ssr = [ "leptos_router/ssr", "leptos-use/ssr", "leptos-use/actix", + "leptos_query/ssr", ] compress = [] diff --git a/frontend/src/main.rs b/frontend/src/main.rs index 32c0a288..596e3d85 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -16,9 +16,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> { let conf = get_configuration(None).await?; let addr = conf.leptos_options.site_addr; // Generate the list of routes in your Leptos App - let routes = generate_route_list(App); + let routes = + leptos_query::with_query_suppression(|| generate_route_list(App)); println!("listening on http://{}", &addr); - init_config().await?; HttpServer::new(move || { let leptos_options: &LeptosOptions = &conf.leptos_options; diff --git a/grpc/build.rs b/grpc/build.rs index d50d711a..2a4bc214 100644 --- a/grpc/build.rs +++ b/grpc/build.rs @@ -25,6 +25,14 @@ fn main() { ".", r#"#[cfg_attr(feature = "extra_trait", derive(derive_more::IsVariant, derive_more::Unwrap))]"#, ) + .message_attribute( + "Create", + r#"#[cfg_attr(feature = "extra_trait", derive(Hash))]"#, + ) + .message_attribute( + "Query", + r#"#[cfg_attr(feature = "extra_trait", derive(Hash))]"#, + ) .extern_path(".google.protobuf.Any", "::prost_wkt_types::Any") .extern_path(".google.protobuf.Timestamp", "::prost_wkt_types::Timestamp") .extern_path(".google.protobuf.Value", "::prost_wkt_types::Value") From aad5201a0e654d6a0f2e7f646cbdc9b5b4579d5e Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:39:42 +0800 Subject: [PATCH 03/18] fix(Frontend): :bug: Fix cookie path not set --- frontend/src/session.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/session.rs b/frontend/src/session.rs index 456c1272..56c0b02c 100644 --- a/frontend/src/session.rs +++ b/frontend/src/session.rs @@ -19,7 +19,10 @@ pub fn use_token_info( ) -> (Signal<Option<TokenInfo>>, WriteSignal<Option<TokenInfo>>) { use_cookie_with_options::<_, JsonCodec>( "token_info", - UseCookieOptions::default().max_age(60 * 60 * 1000), + UseCookieOptions::default() + .max_age(60 * 60 * 1000) + .path("/") + .same_site(cookie::SameSite::Strict), ) } From d84a38f80005de1c7d71081a4e1423e38fe86ba3 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:40:34 +0800 Subject: [PATCH 04/18] chore(Frontend): :fire: Remove default template license --- frontend/LICENSE | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 frontend/LICENSE diff --git a/frontend/LICENSE b/frontend/LICENSE deleted file mode 100644 index e869ce3b..00000000 --- a/frontend/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 henrik - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. From 5e5c9e3845f09da3950112cd27dd625a7f962fa4 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:43:21 +0800 Subject: [PATCH 05/18] refactor(Frontend): :recycle: Change frontend config to global static variable --- frontend/src/config.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/config.rs b/frontend/src/config.rs index 36e2251d..cb039bfb 100644 --- a/frontend/src/config.rs +++ b/frontend/src/config.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; -#[cfg(feature = "ssr")] use std::sync::OnceLock; use leptos::*; @@ -78,7 +76,7 @@ fn default_extension_language_mappings() -> Vec<ExtensionLanguageMapping> { } fn default_page_size() -> usize { - 50 + 20 } impl Default for FrontendConfig { @@ -107,8 +105,10 @@ impl Default for BackendConfig { #[cfg(feature = "ssr")] pub async fn init_config() -> Result<()> { let config = load_config().await?; - let _ = CONFIG.set(config); - Ok(()) + CONFIG.set(config).map_err(|_| Error { + kind: ErrorKind::Internal, + context: "Cannot init config, already set".into(), + }) } #[cfg(feature = "ssr")] @@ -149,8 +149,11 @@ pub fn frontend_config() -> &'static FrontendConfig { } #[cfg(not(feature = "ssr"))] -pub fn frontend_config() -> Rc<FrontendConfig> { - expect_context() +static FRONTEND_CONFIG: OnceLock<FrontendConfig> = OnceLock::new(); + +#[cfg(not(feature = "ssr"))] +pub fn frontend_config() -> &'static FrontendConfig { + FRONTEND_CONFIG.get().expect("config is not init!") } #[cfg(feature = "ssr")] @@ -166,7 +169,6 @@ pub fn backend_config() -> &'static BackendConfig { pub fn ProvideConfig(children: Children) -> impl IntoView { let json = serde_json::to_string(frontend_config()).expect("Cannot to json"); - provide_context(Rc::new(frontend_config().to_owned())); view! { <script id="config" type="application/json"> {json} @@ -185,7 +187,11 @@ pub fn ProvideConfig(children: Children) -> impl IntoView { .unwrap(); let config: FrontendConfig = serde_json::from_str(&json).unwrap(); - provide_context(Rc::new(config)); + FRONTEND_CONFIG + .set(config) + .map_err(|_| "Cannot init config, already set".to_owned()) + .unwrap(); + view! { <script id="config" type="application/json"> {json} From fc5cd344cae9d5c477c77fbc48c23095617b31a0 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:44:11 +0800 Subject: [PATCH 06/18] chore(Frontend): :lipstick: working in styling --- frontend/src/pages/create/problem.rs | 2 +- frontend/src/pages/problem/content.rs | 2 +- frontend/src/pages/problem/editor.rs | 2 +- frontend/src/pages/problem/problem.rs | 32 ++++++++++++++------------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/frontend/src/pages/create/problem.rs b/frontend/src/pages/create/problem.rs index 5647bd41..f8792ee1 100644 --- a/frontend/src/pages/create/problem.rs +++ b/frontend/src/pages/create/problem.rs @@ -83,7 +83,7 @@ pub fn Problem() -> impl IntoView { }); view! { - <main class="container grow flex items-center justify-center py-4"> + <main class="container grow flex items-center justify-center py-10"> <form class="flex flex-col flex-nowrap justify-center p-4 gap-4" on:submit=submit> <h1 class="text-xl">Create a new problem</h1> diff --git a/frontend/src/pages/problem/content.rs b/frontend/src/pages/problem/content.rs index cf1f9b62..ab8e8bbc 100644 --- a/frontend/src/pages/problem/content.rs +++ b/frontend/src/pages/problem/content.rs @@ -9,7 +9,7 @@ pub fn ProblemContent( full_info: grpc::ProblemFullInfo, ) -> impl IntoView { view! { - <div class=tw_join!("p-3 rounded h-full flex flex-col", class)> + <div class=tw_join!("p-3 rounded h-full w-full flex flex-col", class)> <ul class="flex flex-row space-x-4 p-4 pt-0 mb-2 border-b-2 border-accent"> <li>Problem</li> <li>Solution</li> diff --git a/frontend/src/pages/problem/editor.rs b/frontend/src/pages/problem/editor.rs index 72f0d719..2a00b9f1 100644 --- a/frontend/src/pages/problem/editor.rs +++ b/frontend/src/pages/problem/editor.rs @@ -48,7 +48,7 @@ pub fn ProblemEditor( let disabled = Signal::derive(move || select_lang.with(|v| v.is_empty())); view! { - <form class=tw_join!("flex flex-col h-full bg-lighten p-3 rounded", class) on:submit=submit> + <form class=tw_join!("flex flex-col h-full w-full bg-lighten p-3 rounded", class) on:submit=submit> <ul class="flex flex-row justify-between p-2 pt-0 mb-2 border-b-2 border-accent"> <li>Code</li> diff --git a/frontend/src/pages/problem/problem.rs b/frontend/src/pages/problem/problem.rs index 57fb92bf..dc7c5ddc 100644 --- a/frontend/src/pages/problem/problem.rs +++ b/frontend/src/pages/problem/problem.rs @@ -16,7 +16,7 @@ struct ProblemParams { #[component(transparent)] pub fn ProblemRouter() -> impl IntoView { view! { - <Route path="problem/:id" view=Problem> + <Route path="/problem/:id" view=Problem> <Route path="" view=Content/> </Route> } @@ -45,9 +45,7 @@ fn Problem() -> impl IntoView { v.map(|langs| { let id = params()?.id; - Result::<_>::Ok( - view! { <ProblemEditor id langs class="col-span-2 col-start-4"/> }, - ) + Result::<_>::Ok(view! { <ProblemEditor id langs/> }) }) }) }; @@ -55,11 +53,13 @@ fn Problem() -> impl IntoView { view! { <main class="grow grid grid-cols-5 grid-flow-row gap-4"> <Outlet/> - <Suspense fallback=|| { - view! { <p>loading</p> } - }> - <ErrorFallback>{editor}</ErrorFallback> - </Suspense> + <div class="col-span-2 col-start-4"> + <Suspense fallback=|| { + view! { <p>loading</p> } + }> + <ErrorFallback>{editor}</ErrorFallback> + </Suspense> + </div> </main> } } @@ -86,15 +86,17 @@ fn Content() -> impl IntoView { let content = move || { full_info().map(|v| { v.map(|full_info| { - view! { <ProblemContent full_info class="col-span-3"/> } + view! { <ProblemContent full_info/> } }) }) }; view! { - <Suspense fallback=|| { - view! { <p>loading</p> } - }> - <ErrorFallback>{content}</ErrorFallback> - </Suspense> + <div class="col-span-3"> + <Suspense fallback=|| { + view! { <p>loading</p> } + }> + <ErrorFallback>{content}</ErrorFallback> + </Suspense> + </div> } } From c341b6c586a050be136f26de7bee3f2d977ba0ac Mon Sep 17 00:00:00 2001 From: Eason <30045503+Eason0729@users.noreply.github.com> Date: Thu, 22 Aug 2024 08:33:40 +0800 Subject: [PATCH 07/18] Cannot add problem to another contest if problem has been in contest that deleted (#66) * Fix bug children's foreign key not deleted * chore remove idea warning * Fix typo --- backend/src/endpoint/announcement.rs | 6 +- backend/src/endpoint/chat.rs | 4 +- backend/src/endpoint/contest.rs | 114 ++++++++++++++----------- backend/src/endpoint/education.rs | 6 +- backend/src/endpoint/imgur.rs | 2 +- backend/src/endpoint/problem.rs | 24 ++++-- backend/src/endpoint/submit.rs | 8 +- backend/src/endpoint/testcase.rs | 7 +- backend/src/endpoint/token.rs | 6 +- backend/src/endpoint/user.rs | 6 +- backend/src/util/error.rs | 7 +- judger/src/error.rs | 18 ++-- judger/src/filesystem/adapter/error.rs | 12 +-- judger/src/filesystem/adapter/fuse.rs | 27 +++--- judger/src/filesystem/adapter/reply.rs | 2 +- judger/src/filesystem/entry/mod.rs | 2 +- judger/src/filesystem/mod.rs | 4 +- judger/src/filesystem/resource.rs | 6 +- judger/src/filesystem/table.rs | 6 +- judger/src/language/spec/mod.rs | 2 +- judger/src/language/stage/judge.rs | 2 +- judger/src/language/stage/mod.rs | 10 +-- judger/src/language/stage/run.rs | 2 +- judger/src/sandbox/monitor/mem_cpu.rs | 2 +- judger/src/sandbox/monitor/mod.rs | 6 +- judger/src/sandbox/monitor/output.rs | 2 +- judger/src/sandbox/monitor/wrapper.rs | 6 +- judger/src/sandbox/process/lifetime.rs | 7 +- judger/src/sandbox/process/mod.rs | 4 +- judger/src/sandbox/process/nsjail.rs | 4 +- judger/src/server.rs | 21 ++--- 31 files changed, 177 insertions(+), 158 deletions(-) diff --git a/backend/src/endpoint/announcement.rs b/backend/src/endpoint/announcement.rs index d3dcdc43..2c664f93 100644 --- a/backend/src/endpoint/announcement.rs +++ b/backend/src/endpoint/announcement.rs @@ -175,7 +175,7 @@ impl Announcement for ArcServer { let id = *model.id.as_ref(); - tracing::info!(count.announcement = 1, id = id); + info!(count.announcement = 1, id = id); Ok(id.into()) }) @@ -197,7 +197,7 @@ impl Announcement for ArcServer { req.bound_check()?; req.get_or_insert(|req| async move { - tracing::trace!(id = req.id); + trace!(id = req.id); let mut model = Entity::find_by_id(req.id) .with_auth(&auth) @@ -242,7 +242,7 @@ impl Announcement for ArcServer { if result.rows_affected == 0 { Err(Error::NotInDB) } else { - tracing::info!(counter.announcement = -1, id = req.id); + info!(counter.announcement = -1, id = req.id); Ok(()) } }) diff --git a/backend/src/endpoint/chat.rs b/backend/src/endpoint/chat.rs index 14b6f17a..23576658 100644 --- a/backend/src/endpoint/chat.rs +++ b/backend/src/endpoint/chat.rs @@ -51,7 +51,7 @@ impl Chat for ArcServer { .map_err(Into::<Error>::into)?; let id = *model.id.as_ref(); - tracing::debug!(id = id, "chat_created"); + debug!(id = id, "chat_created"); Ok(id.into()) }) @@ -80,7 +80,7 @@ impl Chat for ArcServer { if result.rows_affected == 0 { Err(Error::NotInDB) } else { - tracing::info!(counter.chat = -1, id = req.id); + info!(counter.chat = -1, id = req.id); Ok(()) } }) diff --git a/backend/src/endpoint/contest.rs b/backend/src/endpoint/contest.rs index 9ba8bdf7..a31f7ef1 100644 --- a/backend/src/endpoint/contest.rs +++ b/backend/src/endpoint/contest.rs @@ -1,9 +1,9 @@ use super::*; - use crate::entity::{ contest::{Paginator, *}, - *, + problem, *, }; +use sea_orm::sea_query::Expr; use grpc::backend::contest_server::*; @@ -164,7 +164,7 @@ impl Contest for ArcServer { let id = *model.id.as_ref(); - tracing::info!(count.contest = 1, id = id); + info!(count.contest = 1, id = id); Ok(id.into()) }) @@ -185,7 +185,7 @@ impl Contest for ArcServer { let (_, perm) = auth.assume_login()?; req.get_or_insert(|req| async move { - tracing::trace!(id = req.id); + trace!(id = req.id); let mut model = Entity::find_by_id(req.id).with_auth(&auth).write()? .one(self.db.deref()) @@ -238,6 +238,8 @@ impl Contest for ArcServer { let (auth, req) = self.rate_limit(req).in_current_span().await?; req.get_or_insert(|req| async move { + let txn = self.db.begin().await?; + let result = Entity::delete_by_id(req.id) .with_auth(&auth) .write()? @@ -246,12 +248,20 @@ impl Contest for ArcServer { .await .map_err(Into::<Error>::into)?; + problem::Entity::update_many() + .col_expr(problem::Column::ContestId, Expr::value(Value::Int(None))) + .filter(crate::entity::testcase::Column::ProblemId.eq(req.id)) + .exec(&txn) + .instrument(info_span!("remove_child")) + .await?; + + txn.commit().await.map_err(|_| Error::Retry)?; + if result.rows_affected == 0 { - Err(Error::NotInDB) - } else { - tracing::info!(counter.contest = -1, id = req.id); - Ok(()) + return Err(Error::NotInDB); } + info!(counter.contest = -1, id = req.id); + Ok(()) }) .await .with_grpc() @@ -260,47 +270,30 @@ impl Contest for ArcServer { #[instrument( skip_all, level = "info", - name = "oj.backend.Contest/join", + name = "oj.backend.Contest/publish", err(level = "debug", Display) )] - async fn join(&self, req: Request<JoinContestRequest>) -> Result<Response<()>, Status> { + async fn publish(&self, req: Request<PublishRequest>) -> Result<Response<()>, Status> { let (auth, req) = self.rate_limit(req).in_current_span().await?; - let (user_id, _) = auth.assume_login()?; req.get_or_insert(|req| async move { - let model = Entity::find_by_id(req.id) + let mut model = Entity::find_by_id(req.id) .with_auth(&auth) - .read()? + .write()? + .columns([Column::Id]) .one(self.db.deref()) .instrument(info_span!("fetch").or_current()) .await .map_err(Into::<Error>::into)? - .ok_or(Error::NotInDB)?; - // FIXME: abstract away password checking logic - - if let Some(tar) = model.password { - let password = req - .password - .as_ref() - .ok_or(Error::NotInPayload("password"))?; - if !self.crypto.hash_eq(password, &tar) { - return Err(Error::PermissionDeny("mismatched password")); - } - } - - let pivot = user_contest::ActiveModel { - user_id: ActiveValue::Set(user_id), - contest_id: ActiveValue::Set(model.id), - ..Default::default() - }; + .ok_or(Error::NotInDB)? + .into_active_model(); - pivot - .save(self.db.deref()) - .instrument(info_span!("insert_pviot").or_current()) - .await - .map_err(Into::<Error>::into)?; + model.public = ActiveValue::Set(true); - tracing::debug!(user_id = user_id, contest_id = req.id); + model + .update(self.db.deref()) + .instrument(info_span!("update").or_current()) + .await?; Ok(()) }) .await @@ -310,10 +303,10 @@ impl Contest for ArcServer { #[instrument( skip_all, level = "info", - name = "oj.backend.Contest/publish", + name = "oj.backend.Contest/unpublish", err(level = "debug", Display) )] - async fn publish(&self, req: Request<PublishRequest>) -> Result<Response<()>, Status> { + async fn unpublish(&self, req: Request<PublishRequest>) -> Result<Response<()>, Status> { let (auth, req) = self.rate_limit(req).in_current_span().await?; req.get_or_insert(|req| async move { @@ -328,7 +321,7 @@ impl Contest for ArcServer { .ok_or(Error::NotInDB)? .into_active_model(); - model.public = ActiveValue::Set(true); + model.public = ActiveValue::Set(false); model .update(self.db.deref()) @@ -343,30 +336,47 @@ impl Contest for ArcServer { #[instrument( skip_all, level = "info", - name = "oj.backend.Contest/unpublish", + name = "oj.backend.Contest/join", err(level = "debug", Display) )] - async fn unpublish(&self, req: Request<PublishRequest>) -> Result<Response<()>, Status> { + async fn join(&self, req: Request<JoinContestRequest>) -> Result<Response<()>, Status> { let (auth, req) = self.rate_limit(req).in_current_span().await?; + let (user_id, _) = auth.assume_login()?; req.get_or_insert(|req| async move { - let mut model = Entity::find_by_id(req.id) + let model = Entity::find_by_id(req.id) .with_auth(&auth) - .write()? - .columns([Column::Id]) + .read()? .one(self.db.deref()) .instrument(info_span!("fetch").or_current()) .await .map_err(Into::<Error>::into)? - .ok_or(Error::NotInDB)? - .into_active_model(); + .ok_or(Error::NotInDB)?; + // FIXME: abstract away password checking logic - model.public = ActiveValue::Set(false); + if let Some(tar) = model.password { + let password = req + .password + .as_ref() + .ok_or(Error::NotInPayload("password"))?; + if !self.crypto.hash_eq(password, &tar) { + return Err(Error::PermissionDeny("mismatched password")); + } + } - model - .update(self.db.deref()) - .instrument(info_span!("update").or_current()) - .await?; + let pivot = user_contest::ActiveModel { + user_id: ActiveValue::Set(user_id), + contest_id: ActiveValue::Set(model.id), + ..Default::default() + }; + + pivot + .save(self.db.deref()) + .instrument(info_span!("insert_pviot").or_current()) + .await + .map_err(Into::<Error>::into)?; + + debug!(user_id = user_id, contest_id = req.id); Ok(()) }) .await diff --git a/backend/src/endpoint/education.rs b/backend/src/endpoint/education.rs index ad586258..ea48d9e9 100644 --- a/backend/src/endpoint/education.rs +++ b/backend/src/endpoint/education.rs @@ -104,7 +104,7 @@ impl Education for ArcServer { let id = *model.id.as_ref(); - tracing::info!(count.education = 1, id = id); + info!(count.education = 1, id = id); Ok(id.into()) }) @@ -123,7 +123,7 @@ impl Education for ArcServer { req.bound_check()?; req.get_or_insert(|req| async move { - tracing::trace!(id = req.id); + trace!(id = req.id); let mut model = Entity::find_by_id(req.id) .with_auth(&auth) .write()? @@ -167,7 +167,7 @@ impl Education for ArcServer { if result.rows_affected == 0 { Err(Error::NotInDB) } else { - tracing::info!(counter.education = -1, id = req.id); + info!(counter.education = -1, id = req.id); Ok(()) } }) diff --git a/backend/src/endpoint/imgur.rs b/backend/src/endpoint/imgur.rs index ea1cd41e..0a824b70 100644 --- a/backend/src/endpoint/imgur.rs +++ b/backend/src/endpoint/imgur.rs @@ -19,7 +19,7 @@ impl Image for ArcServer { req.get_or_insert(|req| async move { let url = self.imgur.upload(req.data).await?; - tracing::debug!(counter.image = 1, uri = url); + debug!(counter.image = 1, uri = url); Ok(UploadResponse { url }) }) .await diff --git a/backend/src/endpoint/problem.rs b/backend/src/endpoint/problem.rs index 7aa52283..56f15f88 100644 --- a/backend/src/endpoint/problem.rs +++ b/backend/src/endpoint/problem.rs @@ -1,7 +1,8 @@ use super::*; use grpc::backend::problem_server::*; +use sea_orm::sea_query::Expr; -use crate::entity::{contest, problem::Paginator, problem::*}; +use crate::entity::{contest, problem::Paginator, problem::*, testcase}; impl<'a> From<WithAuth<'a, Model>> for ProblemFullInfo { fn from(value: WithAuth<'a, Model>) -> Self { @@ -139,7 +140,7 @@ impl Problem for ArcServer { let id = *model.id.as_ref(); - tracing::info!(count.problem = 1, id = id); + info!(count.problem = 1, id = id); Ok(id.into()) }) @@ -191,17 +192,28 @@ impl Problem for ArcServer { async fn remove(&self, req: Request<RemoveRequest>) -> Result<Response<()>, Status> { let (auth, req) = self.rate_limit(req).in_current_span().await?; req.get_or_insert(|req| async move { + let txn = self.db.begin().await?; + let result = Entity::delete_by_id(req.id) .with_auth(&auth) .write()? - .exec(self.db.deref()) + .exec(&txn) .instrument(info_span!("remove").or_current()) - .await - .map_err(Into::<Error>::into)?; + .await?; + + testcase::Entity::update_many() + .col_expr(testcase::Column::ProblemId, Expr::value(Value::Int(None))) + .filter(testcase::Column::ProblemId.eq(req.id)) + .exec(&txn) + .instrument(info_span!("remove_child")) + .await?; + + txn.commit().await.map_err(|_| Error::Retry)?; if result.rows_affected == 0 { return Err(Error::NotInDB); } + info!(count.problem = -1, id = req.id); Ok(()) }) .await @@ -286,7 +298,7 @@ impl Problem for ArcServer { let mut model = model.ok_or(Error::NotInDB)?.into_active_model(); if let Some(x) = model.contest_id.into_value() { - tracing::debug!(old_id = x.to_string()); + debug!(old_id = x.to_string()); } model.contest_id = ActiveValue::Set(None); model diff --git a/backend/src/endpoint/submit.rs b/backend/src/endpoint/submit.rs index 06b3e234..60230185 100644 --- a/backend/src/endpoint/submit.rs +++ b/backend/src/endpoint/submit.rs @@ -91,11 +91,11 @@ impl Submit for ArcServer { async fn info(&self, req: Request<Id>) -> Result<Response<SubmitInfo>, Status> { let (auth, req) = self.rate_limit(req).in_current_span().await?; - tracing::debug!(id = req.id); + debug!(id = req.id); let model = Entity::read_filter(Entity::find_by_id(req.id), &auth)? .one(self.db.deref()) - .instrument(tracing::debug_span!("fetch").or_current()) + .instrument(debug_span!("fetch").or_current()) .await .map_err(Into::<Error>::into)? .ok_or(Error::NotInDB)?; @@ -156,7 +156,7 @@ impl Submit for ArcServer { .instrument(info_span!("construct_submit").or_current()) .await?; - tracing::info!(counter.submit = 1, id = id); + info!(counter.submit = 1, id = id); Ok(id.into()) }) @@ -189,7 +189,7 @@ impl Submit for ArcServer { if result.rows_affected == 0 { Err(Error::NotInDB) } else { - tracing::info!(counter.submit = -1, id = req.id); + info!(counter.submit = -1, id = req.id); Ok(()) } }) diff --git a/backend/src/endpoint/testcase.rs b/backend/src/endpoint/testcase.rs index 77c23315..8aed017b 100644 --- a/backend/src/endpoint/testcase.rs +++ b/backend/src/endpoint/testcase.rs @@ -102,7 +102,7 @@ impl Testcase for ArcServer { let id = *model.id.as_ref(); - tracing::info!(count.testcase = 1, id = id); + info!(count.testcase = 1, id = id); Ok(id.into()) }) @@ -121,7 +121,7 @@ impl Testcase for ArcServer { req.bound_check()?; req.get_or_insert(|req| async move { - tracing::trace!(id = req.id); + trace!(id = req.id); let mut model = Entity::write_filter(Entity::find_by_id(req.id), &auth)? .one(self.db.deref()) .instrument(info_span!("fetch").or_current()) @@ -162,7 +162,7 @@ impl Testcase for ArcServer { if result.rows_affected == 0 { Err(Error::NotInDB) } else { - tracing::info!(counter.testcase = -1, id = req.id); + info!(counter.testcase = -1, id = req.id); Ok(()) } }) @@ -197,6 +197,7 @@ impl Testcase for ArcServer { let problem: problem::IdModel = problem.ok_or(Error::NotInDB)?; let mut model = model.ok_or(Error::NotInDB)?.into_active_model(); + // FIXME: use is_set(), be sure to know what Option<T> in sea_orm said if let ActiveValue::Set(Some(v)) = model.problem_id { return Err(Error::AlreadyExist("testcase already linked")); } diff --git a/backend/src/endpoint/token.rs b/backend/src/endpoint/token.rs index ca106a3f..d3be66a0 100644 --- a/backend/src/endpoint/token.rs +++ b/backend/src/endpoint/token.rs @@ -77,7 +77,7 @@ impl Token for ArcServer { // FIXME: add request_id let (_, req) = self.rate_limit(req).in_current_span().await?; - tracing::debug!(username = req.username); + debug!(username = req.username); let model = user::Entity::find() .filter(user::Column::Username.eq(req.username)) @@ -99,7 +99,7 @@ impl Token for ArcServer { expiry: into_prost(expiry), })) } else { - tracing::trace!("password_mismatch"); + trace!("password_mismatch"); Err(Error::PermissionDeny("wrong password").into()) } } @@ -164,7 +164,7 @@ impl Token for ArcServer { .remove(token.to_string()) .in_current_span() .await?; - tracing::event!(Level::TRACE, token = token); + event!(Level::TRACE, token = token); return Ok(Response::new(())); } diff --git a/backend/src/endpoint/user.rs b/backend/src/endpoint/user.rs index c86fb14e..b8efe380 100644 --- a/backend/src/endpoint/user.rs +++ b/backend/src/endpoint/user.rs @@ -118,7 +118,7 @@ impl User for ArcServer { let mut model: ActiveModel = Default::default(); - tracing::debug!(username = req.info.username); + debug!(username = req.info.username); let hash = self.crypto.hash(req.info.password.as_str()); @@ -133,7 +133,7 @@ impl User for ArcServer { .map_err(Into::<Error>::into)?; let id = *model.id.as_ref(); - tracing::info!(counter.user = 1, id = id); + info!(counter.user = 1, id = id); Ok(id.into()) }) @@ -213,7 +213,7 @@ impl User for ArcServer { if result.rows_affected == 0 { Err(Error::NotInDB) } else { - tracing::info!(counter.announcement = -1, id = req.id); + info!(counter.announcement = -1, id = req.id); Ok(()) } }) diff --git a/backend/src/util/error.rs b/backend/src/util/error.rs index 92461c1c..9c9d4497 100644 --- a/backend/src/util/error.rs +++ b/backend/src/util/error.rs @@ -33,8 +33,6 @@ pub enum Error { Unreachable(&'static str), #[error("Number too large(or small)")] NumberTooLarge, - // #[error("Buffer `{0}` too large")] - // BufferTooLarge(&'static str), #[error("`{0}` Already exist")] AlreadyExist(&'static str), #[error("require permission `{0}`")] @@ -47,6 +45,8 @@ pub enum Error { Judger(#[from] judger::Error), #[error("token error: `{0}`")] Token(#[from] token::Error), + #[error("retry later")] + Retry, } impl From<sea_orm::DbErr> for Error { @@ -103,6 +103,7 @@ impl From<Error> for Status { Error::Image(x) => report_internal!(error, "{}", x), Error::Judger(x) => x.into(), Error::Token(x) => x.into(), + Error::Retry => Status::aborted("Should retry"), } } } @@ -110,7 +111,7 @@ impl From<Error> for Status { /// Tracing information for error /// /// useful to log the tracing information to client -/// without exposing the server's internal erro +/// without exposing the server's internal error pub struct Tracing { trace_id: TraceId, span_id: SpanId, diff --git a/judger/src/error.rs b/judger/src/error.rs index 3225eee3..2c155a84 100644 --- a/judger/src/error.rs +++ b/judger/src/error.rs @@ -8,8 +8,8 @@ pub enum Error { Io(#[from] std::io::Error), #[error("sandbox error: {0}")] Sandbox(#[from] SandboxError), - /// the program is running on a 32 bit platform, - /// and have a object reached [`u32::MAX`] + /// the program is running on a 32-bit platform, + /// and have an object reached [`u32::MAX`] #[error("32 bit problem")] Platform, } @@ -23,10 +23,10 @@ impl From<Error> for Status { #[derive(thiserror::Error, Debug)] pub enum ClientError { - #[error("invaild secret")] - InvaildSecret, - #[error("invaild language uuid")] - InvaildLanguageUuid, + #[error("invalid secret")] + InvalidSecret, + #[error("invalid language uuid")] + InvalidLanguageUuid, #[error("impossible memory requirement")] ImpossibleMemoryRequirement, } @@ -34,9 +34,9 @@ pub enum ClientError { impl From<ClientError> for Status { fn from(value: ClientError) -> Self { match value { - ClientError::InvaildSecret => Status::permission_denied("Invaild secret"), - ClientError::InvaildLanguageUuid => { - Status::failed_precondition("Invaild language uuid") + ClientError::InvalidSecret => Status::permission_denied("Invalid secret"), + ClientError::InvalidLanguageUuid => { + Status::failed_precondition("Invalid language uuid") } ClientError::ImpossibleMemoryRequirement => { Status::failed_precondition("Impossible memory requirement") diff --git a/judger/src/filesystem/adapter/error.rs b/judger/src/filesystem/adapter/error.rs index 0e104517..0c51c970 100644 --- a/judger/src/filesystem/adapter/error.rs +++ b/judger/src/filesystem/adapter/error.rs @@ -20,17 +20,17 @@ pub enum FuseError { #[error("unimplemented")] Unimplemented, #[error("missed inode")] - InvaildIno, + InvalidIno, #[error("missed handle")] HandleNotFound, - #[error("underlaying file error")] - Underlaying, + #[error("underlying file error")] + Underlying, #[error("invalid path")] InvalidPath, #[error("permission deny")] PermissionDeny, #[error("invalid argument")] - InvialdArg, + InvalidArg, #[error("Already exist")] AlreadyExist, } @@ -47,9 +47,9 @@ impl From<FuseError> for fuse3::Errno { log::info!("out of resource"); libc::ENOMEM } - FuseError::InvalidPath | FuseError::InvaildIno => libc::ENOENT, + FuseError::InvalidPath | FuseError::InvalidIno => libc::ENOENT, FuseError::PermissionDeny => libc::EACCES, - FuseError::InvialdArg => libc::EINVAL, + FuseError::InvalidArg => libc::EINVAL, FuseError::AlreadyExist => libc::EEXIST, err => { log::warn!("FUSE driver broken: {}", err); diff --git a/judger/src/filesystem/adapter/fuse.rs b/judger/src/filesystem/adapter/fuse.rs index 0e5b2207..8d98c378 100644 --- a/judger/src/filesystem/adapter/fuse.rs +++ b/judger/src/filesystem/adapter/fuse.rs @@ -3,7 +3,6 @@ use std::{ffi::OsStr, num::NonZeroU32, path::Path, sync::Arc}; use bytes::Bytes; use futures_core::Future; use spin::Mutex; -use tokio::fs::metadata; use tokio::io::{AsyncRead, AsyncSeek}; use tokio::sync::Mutex as AsyncMutex; @@ -87,7 +86,7 @@ where async fn lookup(&self, req: Request, parent: u64, name: &OsStr) -> FuseResult<ReplyEntry> { let tree = self.tree.lock(); - let parent_node = tree.get(parent as usize).ok_or(FuseError::InvaildIno)?; + let parent_node = tree.get(parent as usize).ok_or(FuseError::InvalidIno)?; let node = parent_node .get_by_component(name) .ok_or(FuseError::InvalidPath)?; @@ -122,7 +121,7 @@ where } async fn opendir(&self, _: Request, inode: u64, flags: u32) -> FuseResult<ReplyOpen> { let tree = self.tree.lock(); - let node = tree.get(inode as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(inode as usize).ok_or(FuseError::InvalidIno)?; if node.get_value().kind() != FileType::Directory { return Err(FuseError::NotDir.into()); } @@ -135,7 +134,7 @@ where // ignore write permission, because some application may open files // with write permission but never write let tree = self.tree.lock(); - let node = tree.get(inode as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(inode as usize).ok_or(FuseError::InvalidIno)?; if node.get_value().kind() == FileType::Directory { return Err(FuseError::IsDir.into()); } @@ -152,7 +151,7 @@ where offset: i64, ) -> FuseResult<ReplyDirectory<Self::DirEntryStream<'_>>> { let tree = self.tree.lock(); - let node = tree.get(parent as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(parent as usize).ok_or(FuseError::InvalidIno)?; if node.get_value().kind() != FileType::Directory { return Err(FuseError::NotDir.into()); @@ -201,7 +200,7 @@ where _: u64, ) -> FuseResult<ReplyDirectoryPlus<Self::DirEntryPlusStream<'_>>> { let tree = self.tree.lock(); - let node = tree.get(parent as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(parent as usize).ok_or(FuseError::InvalidIno)?; if node.get_value().kind() != FileType::Directory { return Err(FuseError::NotDir.into()); @@ -322,7 +321,7 @@ where _: u32, ) -> FuseResult<()> { let tree = self.tree.lock(); - let node = tree.get(inode as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(inode as usize).ok_or(FuseError::InvalidIno)?; match node.get_value().kind() { FileType::Directory | FileType::NamedPipe | FileType::CharDevice => { @@ -342,7 +341,7 @@ where _: u32, ) -> FuseResult<ReplyAttr> { let tree = self.tree.lock(); - let node = tree.get(inode as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(inode as usize).ok_or(FuseError::InvalidIno)?; // FIXME: unsure about the inode Ok(reply_attr(&req, node.get_value(), inode)) } @@ -354,7 +353,7 @@ where _: SetAttr, ) -> FuseResult<ReplyAttr> { let tree = self.tree.lock(); - let node = tree.get(inode as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(inode as usize).ok_or(FuseError::InvalidIno)?; Ok(reply_attr(&req, node.get_value(), inode)) } async fn create( @@ -366,7 +365,7 @@ where flags: u32, ) -> FuseResult<ReplyCreated> { let mut tree = self.tree.lock(); - let mut parent_node = tree.get_mut(parent as usize).ok_or(FuseError::InvaildIno)?; + let mut parent_node = tree.get_mut(parent as usize).ok_or(FuseError::InvalidIno)?; if parent_node.get_value().kind() != FileType::Directory { return Err(FuseError::NotDir.into()); } @@ -389,7 +388,7 @@ where _: u32, ) -> FuseResult<ReplyEntry> { let mut tree = self.tree.lock(); - let mut parent_node = tree.get_mut(parent as usize).ok_or(FuseError::InvaildIno)?; + let mut parent_node = tree.get_mut(parent as usize).ok_or(FuseError::InvalidIno)?; if parent_node.get_value().kind() != FileType::Directory { return Err(FuseError::NotDir.into()); } @@ -401,18 +400,18 @@ where } async fn readlink(&self, _: Request, inode: Inode) -> FuseResult<ReplyData> { let tree = self.tree.lock(); - let node = tree.get(inode as usize).ok_or(FuseError::InvaildIno)?; + let node = tree.get(inode as usize).ok_or(FuseError::InvalidIno)?; let link = node .get_value() .get_symlink() - .ok_or(FuseError::InvialdArg)?; + .ok_or(FuseError::InvalidArg)?; Ok(ReplyData { data: Bytes::copy_from_slice(link.as_encoded_bytes()), }) } async fn unlink(&self, _: Request, parent: Inode, name: &OsStr) -> FuseResult<()> { let mut tree = self.tree.lock(); - let mut parent_node = tree.get_mut(parent as usize).ok_or(FuseError::InvaildIno)?; + let mut parent_node = tree.get_mut(parent as usize).ok_or(FuseError::InvalidIno)?; if parent_node.get_value().kind() != FileType::Directory { return Err(FuseError::NotDir.into()); } diff --git a/judger/src/filesystem/adapter/reply.rs b/judger/src/filesystem/adapter/reply.rs index 8de93a37..ed587a3b 100644 --- a/judger/src/filesystem/adapter/reply.rs +++ b/judger/src/filesystem/adapter/reply.rs @@ -1,4 +1,4 @@ -//! collection of function that fill the value of +//! collection of function that fill with the value of //! reply packets back to fuse connection use std::{ffi::OsString, time::Duration}; diff --git a/judger/src/filesystem/entry/mod.rs b/judger/src/filesystem/entry/mod.rs index 8f83ea2d..bb25b913 100644 --- a/judger/src/filesystem/entry/mod.rs +++ b/judger/src/filesystem/entry/mod.rs @@ -136,7 +136,7 @@ where pub async fn write(&mut self, offset: u64, data: &[u8], resource: &Resource) -> Option<u32> { // FIXME: consume logic should move somewhere else let required_size = data.len() as u64 + offset; - resource.comsume_other(required_size.saturating_sub(self.get_size()))?; + resource.consume_other(required_size.saturating_sub(self.get_size()))?; match self { Self::MemFile(block) => Some(block.write(offset, data).await.unwrap()), diff --git a/judger/src/filesystem/mod.rs b/judger/src/filesystem/mod.rs index 3393101d..289ba14c 100644 --- a/judger/src/filesystem/mod.rs +++ b/judger/src/filesystem/mod.rs @@ -1,5 +1,5 @@ -//! Filesystem module that is mountable(actuallly mount and -//! is accessible for user in this operation system) +//! Filesystem module that is mountable(actually mount and +//! is accessible for user in this operating system) mod adapter; mod entry; mod handle; diff --git a/judger/src/filesystem/resource.rs b/judger/src/filesystem/resource.rs index 55db660b..0c3721a2 100644 --- a/judger/src/filesystem/resource.rs +++ b/judger/src/filesystem/resource.rs @@ -11,7 +11,7 @@ impl Resource { Self(AtomicU64::new(cap)) } /// consume some amount of resource - pub fn comsume(&self, size: u32) -> Option<()> { + pub fn consume(&self, size: u32) -> Option<()> { let a = self.0.fetch_sub(size as u64, Ordering::AcqRel); if (a & (1 << 63)) != 0 { None @@ -23,8 +23,8 @@ impl Resource { /// /// return None if the resource is not enough or the size /// is out of range (greater than[`u32::MAX`]) - pub fn comsume_other<T: TryInto<u32>>(&self, size: T) -> Option<()> { + pub fn consume_other<T: TryInto<u32>>(&self, size: T) -> Option<()> { let size = size.try_into().ok()?; - self.comsume(size) + self.consume(size) } } diff --git a/judger/src/filesystem/table.rs b/judger/src/filesystem/table.rs index 8618192e..ff18f478 100644 --- a/judger/src/filesystem/table.rs +++ b/judger/src/filesystem/table.rs @@ -294,7 +294,7 @@ mod test { use super::*; #[test] fn test_adj_table() { - let mut table = super::AdjTable::new(); + let mut table = AdjTable::new(); let mut root = table.insert_root(0); root.insert(OsStr::new("a").into(), 1); let mut b = root.insert(OsStr::new("b").into(), 2).unwrap(); @@ -306,7 +306,7 @@ mod test { } #[test] fn get_or_insert() { - let mut table = super::AdjTable::new(); + let mut table = AdjTable::new(); table.insert_root(0); table.insert_by_path( vec!["abc", "efg", "123", "456"] @@ -325,7 +325,7 @@ mod test { } #[test] fn parent_child_insert() { - let mut table = super::AdjTable::new(); + let mut table = AdjTable::new(); let mut root = table.insert_root(0); // inode 1 assert_eq!(root.get_id(), 1); let mut a = root.insert(OsStr::new("a").into(), 1).unwrap(); // inode 2 diff --git a/judger/src/language/spec/mod.rs b/judger/src/language/spec/mod.rs index 449910db..2d49a913 100644 --- a/judger/src/language/spec/mod.rs +++ b/judger/src/language/spec/mod.rs @@ -107,7 +107,7 @@ impl Spec { let mut raw: Raw = toml::from_str(content).unwrap(); raw.fill(); - // FIXME: use compsition instead + // FIXME: use composition instead Self { info: LangInfo::from(&raw), id: raw.id, diff --git a/judger/src/language/stage/judge.rs b/judger/src/language/stage/judge.rs index 6ec80991..1cb734b8 100644 --- a/judger/src/language/stage/judge.rs +++ b/judger/src/language/stage/judge.rs @@ -43,7 +43,7 @@ impl Judger { } } } - AssertionMode::SkipContinousSpace => { + AssertionMode::SkipContinuousSpace => { // skip space and newline, continous space is consider same let output = output.iter().map(|x| match x { b'\n' | b' ' => b' ', diff --git a/judger/src/language/stage/mod.rs b/judger/src/language/stage/mod.rs index 41f755e2..8af73ddc 100644 --- a/judger/src/language/stage/mod.rs +++ b/judger/src/language/stage/mod.rs @@ -11,7 +11,7 @@ pub use run::Runner; /// internal status code, use to decouple the grpc status code /// -/// Status code is commonly use in OJ, it include example such as: AC, WA... +/// Status code is commonly use in OJ, it includes example such as: AC, WA... #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum StatusCode { Accepted, @@ -27,7 +27,7 @@ pub enum StatusCode { /// internal assertion mode, use to decouple the grpc status code /// -/// Assertion mode reperesent how the output is compared +/// Assertion mode represent how the output is compared #[derive(Clone, Copy)] pub enum AssertionMode { /// Skip single space and newline @@ -36,10 +36,10 @@ pub enum AssertionMode { /// /// `a\nb` and `a\n\nb` are different SkipSpace, - /// Skip continous space and newline + /// Skip continuous space and newline /// /// `ab`, `a\nb` and `a\n\nb` are the same - SkipContinousSpace, + SkipContinuousSpace, /// Exact match Exact, } @@ -56,7 +56,7 @@ impl From<JudgeMatchRule> for AssertionMode { match rule { JudgeMatchRule::ExactSame => AssertionMode::Exact, JudgeMatchRule::IgnoreSnl => AssertionMode::SkipSpace, - JudgeMatchRule::SkipSnl => AssertionMode::SkipContinousSpace, + JudgeMatchRule::SkipSnl => AssertionMode::SkipContinuousSpace, } } } diff --git a/judger/src/language/stage/run.rs b/judger/src/language/stage/run.rs index ebc9d78e..bd674dc6 100644 --- a/judger/src/language/stage/run.rs +++ b/judger/src/language/stage/run.rs @@ -46,7 +46,7 @@ impl Runner { /// See [`Context`] for more information struct RunCtx { spec: Arc<Spec>, - path: std::path::PathBuf, + path: PathBuf, limit: Stat, } diff --git a/judger/src/sandbox/monitor/mem_cpu.rs b/judger/src/sandbox/monitor/mem_cpu.rs index 2856d7d9..0cbe87ad 100644 --- a/judger/src/sandbox/monitor/mem_cpu.rs +++ b/judger/src/sandbox/monitor/mem_cpu.rs @@ -126,7 +126,7 @@ impl super::Monitor for Monitor { /// get the final resource usage /// /// Please remember thatActively limit(notify) cpu resource is achieved - /// by polling the cgroup, therefore the delay requirespecial attention, + /// by polling the cgroup, therefore the delay require special attention, /// it is only guaranteed to below limitation provided + [`MONITOR_ACCURACY`]. async fn stat(self) -> Self::Resource { // FIXME: check running process, this line is commented out because of uncollected process diff --git a/judger/src/sandbox/monitor/mod.rs b/judger/src/sandbox/monitor/mod.rs index 08bac8f8..eed17b6c 100644 --- a/judger/src/sandbox/monitor/mod.rs +++ b/judger/src/sandbox/monitor/mod.rs @@ -24,7 +24,7 @@ lazy_static::lazy_static! { pub trait Monitor { type Resource; - /// wait for exhuast of resource + /// wait for exhaust of resource /// /// This function is cancel safe. async fn wait_exhaust(&mut self) -> MonitorKind { @@ -37,9 +37,9 @@ pub trait Monitor { tokio::time::sleep(Duration::from_millis(12)).await; } } - /// poll for exhuast of resource + /// poll for exhaust of resource /// - /// Implementor should do bith [`wait_exhaust`] and [`poll_exhaust`] + /// Implementor should do both [`wait_exhaust`] and [`poll_exhaust`] /// for better performance. fn poll_exhaust(&mut self) -> Option<MonitorKind>; /// get the resource usage diff --git a/judger/src/sandbox/monitor/output.rs b/judger/src/sandbox/monitor/output.rs index f5aaa46b..b4374461 100644 --- a/judger/src/sandbox/monitor/output.rs +++ b/judger/src/sandbox/monitor/output.rs @@ -82,7 +82,7 @@ mod test { #[tokio::test] async fn monitor_output_limit() { - let (mut stdin, stdout) = tokio::io::duplex(1024); + let (mut stdin, stdout) = duplex(1024); let mut monitor = Monitor::inner_new(9, stdout); stdin.write_all(b"1234567890").await.unwrap(); assert_eq!( diff --git a/judger/src/sandbox/monitor/wrapper.rs b/judger/src/sandbox/monitor/wrapper.rs index d58539b1..3a18d0bf 100644 --- a/judger/src/sandbox/monitor/wrapper.rs +++ b/judger/src/sandbox/monitor/wrapper.rs @@ -66,9 +66,9 @@ impl<'a> CgroupWrapper<'a> { /// get memory usage(statistics) pub fn memory(&self) -> Memory { let controller = self.0.controller_of::<MemController>().unwrap(); - let kusage = controller.kmem_stat(); + let kernel_usage = controller.kmem_stat(); - let kernel = kusage.max_usage_in_bytes; + let kernel = kernel_usage.max_usage_in_bytes; let user = controller.memory_stat().max_usage_in_bytes; let total = kernel + user; @@ -96,7 +96,7 @@ impl CgroupWrapperOwned { } /// poll until cgroup is deleted /// - /// After the cgroup is empty(`tasks` is empty), the cgroup is can be delete safely + /// After the cgroup is empty(`tasks` is empty), the cgroup is can be deleted safely /// /// However, in some rare cases, the monitor is reading file in cgroup /// when the cgroup is about to be deleted, this will cause the cgroup to stay busy diff --git a/judger/src/sandbox/process/lifetime.rs b/judger/src/sandbox/process/lifetime.rs index ba860179..02b918cb 100644 --- a/judger/src/sandbox/process/lifetime.rs +++ b/judger/src/sandbox/process/lifetime.rs @@ -9,7 +9,7 @@ use tokio::{ process::*, time, }; -/// A unlaunched process that is mounted with a filesystem +/// A not yet launched process that is mounted with a filesystem struct MountedProcess<C: Context> { context: C, fs: C::FS, @@ -84,10 +84,9 @@ impl<C: Context> Process<C> { let root = self.fs.get_path(); // FIXME: check spec before unwrap let jail = self.context.get_args().next().unwrap(); - let unjailed = [root.as_ref().as_os_str(), jail].join(OsStr::new("")); - let unjailed = PathBuf::from(unjailed); + let real_path = PathBuf::from([root.as_ref().as_os_str(), jail].join(OsStr::new(""))); - let mut ancestors = unjailed.ancestors(); + let mut ancestors = real_path.ancestors(); ancestors.next().unwrap(); ancestors.next().unwrap().as_os_str().to_os_string() } diff --git a/judger/src/sandbox/process/mod.rs b/judger/src/sandbox/process/mod.rs index 7b1e026e..f2ef3b09 100644 --- a/judger/src/sandbox/process/mod.rs +++ b/judger/src/sandbox/process/mod.rs @@ -1,6 +1,6 @@ -//! A module that provides a way to setup environment for a process and run. +//! A module that provides a way to set up environment for a process and run. //! -//! Using this module should be SAFE(can't launching a process without +//! Using this module should be SAFE(can't launch a process without //! explicit resource limitation) //! //! ```norun diff --git a/judger/src/sandbox/process/nsjail.rs b/judger/src/sandbox/process/nsjail.rs index 5d077666..9f9d7688 100644 --- a/judger/src/sandbox/process/nsjail.rs +++ b/judger/src/sandbox/process/nsjail.rs @@ -14,7 +14,7 @@ pub trait Argument { fn get_args(self) -> impl Iterator<Item = Cow<'static, OsStr>>; } -/// factory pattern for conbinating arguments +/// factory pattern for combining arguments #[derive(Default)] pub struct ArgFactory { args: Vec<Cow<'static, OsStr>>, @@ -31,7 +31,7 @@ impl ArgFactory { } } -/// base auguments for nsjail +/// base arguments for nsjail pub struct BaseArg; impl Argument for BaseArg { diff --git a/judger/src/server.rs b/judger/src/server.rs index 51d0e8b5..f2ac0222 100644 --- a/judger/src/server.rs +++ b/judger/src/server.rs @@ -16,7 +16,7 @@ use crate::{ const PLUGIN_PATH: &str = "plugins"; -fn check_secret<T>(req: tonic::Request<T>) -> Result<T, Status> { +fn check_secret<T>(req: Request<T>) -> Result<T, Status> { let (meta, _, payload) = req.into_parts(); if CONFIG.secret.is_none() { return Ok(payload); @@ -24,13 +24,13 @@ fn check_secret<T>(req: tonic::Request<T>) -> Result<T, Status> { let secret = CONFIG.secret.as_ref().unwrap(); if let Some(header) = meta.get("Authorization") { let secret = ["basic ", secret].concat().into_bytes(); - let vaild = header + let valid = header .as_bytes() .iter() .zip(secret.iter()) .map(|(&a, &b)| a == b) .reduce(|a, b| a && b); - if vaild.unwrap_or(false) { + if valid.unwrap_or(false) { return Ok(payload); } } @@ -64,12 +64,12 @@ impl Judger for Server { let cpu = payload.time; let source = payload.code; let uuid = - Uuid::from_str(&payload.lang_uid).map_err(|_| ClientError::InvaildLanguageUuid)?; + Uuid::from_str(&payload.lang_uid).map_err(|_| ClientError::InvalidLanguageUuid)?; let plugin = self .plugins .get(&uuid) - .ok_or(ClientError::InvaildLanguageUuid)?; + .ok_or(ClientError::InvalidLanguageUuid)?; let resource: u32 = plugin .get_memory_reserved(payload.memory) .try_into() @@ -106,7 +106,7 @@ impl Judger for Server { }))) } - async fn judger_info(&self, req: tonic::Request<()>) -> Result<Response<JudgeInfo>, Status> { + async fn judger_info(&self, req: Request<()>) -> Result<Response<JudgeInfo>, Status> { check_secret(req)?; let list = self .plugins @@ -123,10 +123,7 @@ impl Judger for Server { type ExecStream = tokio_stream::Once<Result<ExecResult, Status>>; - async fn exec( - &self, - req: Request<ExecRequest>, - ) -> Result<Response<Self::ExecStream>, tonic::Status> { + async fn exec(&self, req: Request<ExecRequest>) -> Result<Response<Self::ExecStream>, Status> { let payload = check_secret(req)?; let memory = payload.memory; @@ -136,12 +133,12 @@ impl Judger for Server { let input = payload.input; let uuid = - Uuid::from_str(&payload.lang_uid).map_err(|_| ClientError::InvaildLanguageUuid)?; + Uuid::from_str(&payload.lang_uid).map_err(|_| ClientError::InvalidLanguageUuid)?; let plugin = self .plugins .get(&uuid) - .ok_or(ClientError::InvaildLanguageUuid)?; + .ok_or(ClientError::InvalidLanguageUuid)?; let resource: u32 = plugin .get_memory_reserved(payload.memory) From 94ca70b7489e69f99e57b7694811669e1f937f9a Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:24:55 +0800 Subject: [PATCH 08/18] refactor(Frontend): :truck: move `session`/`grpc`/`config` & extract `errors` into utils & component --- frontend/src/{ => components}/errors/error_fallback.rs | 0 .../src/{ => components}/errors/internal_server_error.rs | 0 frontend/src/{ => components}/errors/not_found.rs | 0 frontend/src/errors/mod.rs | 8 -------- frontend/src/{ => utils}/config.rs | 0 frontend/src/{errors => utils}/error.rs | 0 frontend/src/{ => utils}/grpc.rs | 0 frontend/src/{ => utils}/session.rs | 0 8 files changed, 8 deletions(-) rename frontend/src/{ => components}/errors/error_fallback.rs (100%) rename frontend/src/{ => components}/errors/internal_server_error.rs (100%) rename frontend/src/{ => components}/errors/not_found.rs (100%) delete mode 100644 frontend/src/errors/mod.rs rename frontend/src/{ => utils}/config.rs (100%) rename frontend/src/{errors => utils}/error.rs (100%) rename frontend/src/{ => utils}/grpc.rs (100%) rename frontend/src/{ => utils}/session.rs (100%) diff --git a/frontend/src/errors/error_fallback.rs b/frontend/src/components/errors/error_fallback.rs similarity index 100% rename from frontend/src/errors/error_fallback.rs rename to frontend/src/components/errors/error_fallback.rs diff --git a/frontend/src/errors/internal_server_error.rs b/frontend/src/components/errors/internal_server_error.rs similarity index 100% rename from frontend/src/errors/internal_server_error.rs rename to frontend/src/components/errors/internal_server_error.rs diff --git a/frontend/src/errors/not_found.rs b/frontend/src/components/errors/not_found.rs similarity index 100% rename from frontend/src/errors/not_found.rs rename to frontend/src/components/errors/not_found.rs diff --git a/frontend/src/errors/mod.rs b/frontend/src/errors/mod.rs deleted file mode 100644 index 5f2b7dec..00000000 --- a/frontend/src/errors/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub use error_fallback::ErrorFallback; -use internal_server_error::InternalServerError; -pub use not_found::NotFound; -mod error; -mod error_fallback; -mod internal_server_error; -mod not_found; -pub use error::{Context, Error, ErrorKind, Result}; diff --git a/frontend/src/config.rs b/frontend/src/utils/config.rs similarity index 100% rename from frontend/src/config.rs rename to frontend/src/utils/config.rs diff --git a/frontend/src/errors/error.rs b/frontend/src/utils/error.rs similarity index 100% rename from frontend/src/errors/error.rs rename to frontend/src/utils/error.rs diff --git a/frontend/src/grpc.rs b/frontend/src/utils/grpc.rs similarity index 100% rename from frontend/src/grpc.rs rename to frontend/src/utils/grpc.rs diff --git a/frontend/src/session.rs b/frontend/src/utils/session.rs similarity index 100% rename from frontend/src/session.rs rename to frontend/src/utils/session.rs From e4b4442d05a8ef6639c91c244b91b5d0df0bc727 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:29:43 +0800 Subject: [PATCH 09/18] refactor(Frontend): :recycle: change to `use utils::*` --- frontend/src/app.rs | 24 +++++++++------ frontend/src/components/editor.rs | 2 +- .../src/components/errors/error_fallback.rs | 9 +++--- frontend/src/components/navbar.rs | 6 ++-- frontend/src/lib.rs | 8 ++--- frontend/src/main.rs | 2 +- frontend/src/pages/create/problem.rs | 19 +++++------- frontend/src/pages/login.rs | 13 +++----- frontend/src/pages/pages.rs | 30 +++++++++++-------- frontend/src/pages/problem/content.rs | 6 ++-- frontend/src/pages/problem/editor.rs | 17 +++++++---- frontend/src/pages/problem/problem.rs | 14 ++++----- frontend/src/utils/config.rs | 2 +- frontend/src/utils/grpc.rs | 2 +- 14 files changed, 78 insertions(+), 76 deletions(-) diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 3d046602..6c9e8746 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1,8 +1,13 @@ use leptos::*; use leptos_meta::*; +use leptos_query_devtools::LeptosQueryDevtools; use leptos_router::*; -use crate::{components::ProvideToast, config::ProvideConfig, pages::Pages}; +use crate::{ + components::*, + pages::Pages, + utils::{config::ProvideConfig, *}, +}; // use tracing_subscriber::fmt::format::Pretty; // use tracing_subscriber::prelude::*; // use tracing_web::{performance_layer, MakeWebConsoleWriter}; @@ -11,19 +16,20 @@ use crate::{components::ProvideToast, config::ProvideConfig, pages::Pages}; pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); + provide_query_service(); view! { + <Stylesheet id="leptos" href="/pkg/mdoj.css" /> + <Title text="MDOJ" /> <ProvideConfig> <ProvideToast> - <Router> - <Stylesheet id="leptos" href="/pkg/mdoj.css"/> - <Title text="MDOJ"/> - - <div class="bg-black-950 w-full min-h-dvh flex flex-col text-text"> - <Pages/> - </div> - </Router> + <div class="bg-black-950 w-full min-h-dvh flex flex-col text-text"> + <Router> + <Pages /> + </Router> + </div> </ProvideToast> </ProvideConfig> + <LeptosQueryDevtools /> } } diff --git a/frontend/src/components/editor.rs b/frontend/src/components/editor.rs index ede690ec..55ca3dd2 100644 --- a/frontend/src/components/editor.rs +++ b/frontend/src/components/editor.rs @@ -3,7 +3,7 @@ use leptos::*; use wasm_bindgen::prelude::*; use web_sys::Event; -use crate::config::frontend_config; +use crate::utils::*; #[wasm_bindgen] extern "C" { diff --git a/frontend/src/components/errors/error_fallback.rs b/frontend/src/components/errors/error_fallback.rs index 8ba69461..9e7fd9a5 100644 --- a/frontend/src/components/errors/error_fallback.rs +++ b/frontend/src/components/errors/error_fallback.rs @@ -1,6 +1,7 @@ use leptos::*; -use super::{Error, ErrorKind, InternalServerError, NotFound}; +use super::*; +use crate::utils::*; #[component] pub fn ErrorFallback(children: Children) -> impl IntoView { @@ -12,13 +13,13 @@ fn fallback(errors: RwSignal<Errors>) -> impl IntoView { errors().into_iter().next().map(|(_, err)| { let err: Error = err.into(); match err.kind { - ErrorKind::NotFound => view! { <NotFound/> }.into_view(), + ErrorKind::NotFound => view! { <NotFound /> }.into_view(), ErrorKind::RateLimit => todo!(), ErrorKind::Unauthenticated => todo!(), - ErrorKind::OutOfRange => view! { <NotFound/> }.into_view(), + ErrorKind::OutOfRange => view! { <NotFound /> }.into_view(), ErrorKind::Network => todo!(), ErrorKind::Internal => { - view! { <InternalServerError/> }.into_view() + view! { <InternalServerError /> }.into_view() } ErrorKind::PermissionDenied => todo!(), ErrorKind::Browser => todo!(), diff --git a/frontend/src/components/navbar.rs b/frontend/src/components/navbar.rs index c2dbee9f..91ddf50c 100644 --- a/frontend/src/components/navbar.rs +++ b/frontend/src/components/navbar.rs @@ -2,7 +2,7 @@ use leptos::*; use leptos_router::*; use leptos_use::*; -use crate::session::use_token; +use crate::utils::*; #[component] pub fn Navbar() -> impl IntoView { @@ -11,7 +11,7 @@ pub fn Navbar() -> impl IntoView { <nav class="bg-black-900 sticky top-0 p-2 flex flex-row justify-between border-b-2 border-black-400 z-10"> <div class="flex flex-row flex-nowrap"> <A href="/"> - <img src="https://placehold.co/100" class="h-12 aspect-square mx-5"/> + <img src="https://placehold.co/100" class="h-12 aspect-square mx-5" /> </A> <ul class="flex flex-row flex-nowrap justify-between items-center text-base"> <NavbarLink href="/problems">Problems</NavbarLink> @@ -33,7 +33,7 @@ pub fn Navbar() -> impl IntoView { } > - <img src="https://placehold.co/100" class="h-12 aspect-square mx-5"/> + <img src="https://placehold.co/100" class="h-12 aspect-square mx-5" /> </Show> </div> </nav> diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 1a2b27cf..518c0bf8 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,10 +1,7 @@ pub mod app; pub mod components; -pub mod config; -pub mod errors; -pub mod grpc; pub mod pages; -pub mod session; +pub mod utils; use cfg_if::cfg_if; #[cfg(target_arch = "wasm32")] @@ -12,6 +9,8 @@ use lol_alloc::{AssumeSingleThreaded, FreeListAllocator}; #[cfg(target_arch = "wasm32")] #[global_allocator] +/// SAFETY: leptos use single threaded +/// Change to lock allocator when we have multithread in web static ALLOCATOR: AssumeSingleThreaded<FreeListAllocator> = unsafe { AssumeSingleThreaded::new(FreeListAllocator::new()) }; @@ -23,7 +22,6 @@ if #[cfg(feature = "hydrate")] { #[wasm_bindgen] pub fn hydrate() { use app::*; - use leptos::*; console_error_panic_hook::set_once(); diff --git a/frontend/src/main.rs b/frontend/src/main.rs index 596e3d85..4f3c7ec3 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -6,7 +6,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> { // use frontend::{app::*, config}; use frontend::{ app::*, - config::{backend_config, init_config}, + utils::config::{backend_config, init_config}, }; use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; diff --git a/frontend/src/pages/create/problem.rs b/frontend/src/pages/create/problem.rs index f8792ee1..2494000a 100644 --- a/frontend/src/pages/create/problem.rs +++ b/frontend/src/pages/create/problem.rs @@ -1,12 +1,7 @@ use leptos::*; use leptos_router::*; -use crate::{ - components::*, - errors::*, - grpc::{self, WithToken}, - session::*, -}; +use crate::{components::*, utils::*}; #[component] pub fn Problem() -> impl IntoView { @@ -89,25 +84,25 @@ pub fn Problem() -> impl IntoView { <div class="flex flex-col"> <label class="text-text pb-2">Title</label> - <Input value=title/> + <Input value=title /> </div> <div class="flex flex-col"> <label class="text-text pb-2">Tags</label> - <Input value=tags/> + <Input value=tags /> </div> <div class="w-full flex-wrap flex flex-row flex-1 gap-4 justify-evenly"> <div class="flex flex-col min-w-fit flex-grow"> <label class="text-text pb-2">Difficulty</label> - <InputNumber value=difficulty/> + <InputNumber value=difficulty /> </div> <div class="flex flex-col min-w-fit flex-grow"> <label class="text-text pb-2">Time (nanosecond)</label> - <InputNumber value=time/> + <InputNumber value=time /> </div> <div class="flex flex-col min-w-fit flex-grow"> <label class="text-text pb-2">Memory (byte)</label> - <InputNumber value=memory/> + <InputNumber value=memory /> </div> <div class="flex flex-col min-w-fit flex-grow"> <label class="text-text pb-2">Match Rule</label> @@ -121,7 +116,7 @@ pub fn Problem() -> impl IntoView { <div class="w-full"> <label class="text-text pb-2">Content</label> - <Editor class="w-full h-full min-h-80" lang_ext="md" editor_ref/> + <Editor class="w-full h-full min-h-80" lang_ext="md" editor_ref /> </div> <div class="w-full"> <Button type_="submit" class="w-full" disabled> diff --git a/frontend/src/pages/login.rs b/frontend/src/pages/login.rs index 19b3a8e3..f635441a 100644 --- a/frontend/src/pages/login.rs +++ b/frontend/src/pages/login.rs @@ -1,12 +1,7 @@ use leptos::*; use leptos_router::use_navigate; -use crate::{ - components::*, - errors::*, - grpc, - session::{use_token_info, TokenInfo}, -}; +use crate::{components::*, utils::*}; #[component] pub fn Login() -> impl IntoView { @@ -86,20 +81,20 @@ pub fn Login() -> impl IntoView { > <div class="flex justify-center"> - <img src="https://placehold.co/200" alt="Logo" class="max-w-64"/> + <img src="https://placehold.co/200" alt="Logo" class="max-w-64" /> </div> <div class="pt-4 flex flex-col"> <label for="username" class="text-text pb-2"> Username </label> - <Input attr:id="username" value=username/> + <Input attr:id="username" value=username /> </div> <div class="pt-4 flex flex-col"> <label for="password" class="text-text pb-2"> Password </label> - <Input variant=InputVariant::Password attr:id="password" value=password/> + <Input variant=InputVariant::Password attr:id="password" value=password /> </div> <p class="w-full text-red-500 text-center">{error_msg}</p> <div class="pt-4 w-full"> diff --git a/frontend/src/pages/pages.rs b/frontend/src/pages/pages.rs index 3386b96f..54d871d3 100644 --- a/frontend/src/pages/pages.rs +++ b/frontend/src/pages/pages.rs @@ -1,9 +1,13 @@ use leptos::*; use leptos_router::*; use leptos_use::*; -use problem::ProblemRouter; -use crate::{components::*, errors::NotFound, grpc, pages::*, session::*}; +use super::{ + about::About, contests::Contests, create, home::Home, login::Login, + problem::ProblemRouter, problems::Problems, rank::Rank, + submission::Submission, +}; +use crate::{components::*, utils::*}; /// |Permission|Root|Admin|SuperUser|User|Guest /// |:-|:-:|:-:|:-:|:-:|:-:| @@ -36,10 +40,10 @@ pub fn Pages() -> impl IntoView { }; let page_wrapper = move || { view! { - <Navbar/> - <Outlet/> + <Navbar /> + <Outlet /> <Show when=show_footer fallback=|| ()> - <Footer/> + <Footer /> </Show> } }; @@ -47,13 +51,13 @@ pub fn Pages() -> impl IntoView { view! { <Routes> <Route path="" view=page_wrapper> - <Route path="" view=Home/> - <Route path="/problems" view=Problems/> - <Route path="/submissions" view=Submission/> - <Route path="/contests" view=Contests/> - <Route path="/about" view=About/> - <Route path="/rank" view=Rank/> - <ProblemRouter/> + <Route path="" view=Home /> + <Route path="/problems" view=Problems ssr=SsrMode::Async /> + <Route path="/submissions" view=Submission /> + <Route path="/contests" view=Contests /> + <Route path="/about" view=About /> + <Route path="/rank" view=Rank /> + <ProblemRouter /> <ProtectedRoute path="/login" @@ -69,7 +73,7 @@ pub fn Pages() -> impl IntoView { /> // Fallback - <Route path="/*any" view=NotFound/> + <Route path="/*any" view=NotFound /> </Route> </Routes> } diff --git a/frontend/src/pages/problem/content.rs b/frontend/src/pages/problem/content.rs index ab8e8bbc..97059398 100644 --- a/frontend/src/pages/problem/content.rs +++ b/frontend/src/pages/problem/content.rs @@ -1,7 +1,7 @@ use leptos::*; use tailwind_fuse::tw_join; -use crate::{components::*, grpc}; +use crate::{components::*, utils::*}; #[component] pub fn ProblemContent( @@ -20,10 +20,10 @@ pub fn ProblemContent( <h1 class="text-2xl my-2">{full_info.info.title}</h1> <div class="flex-grow relative overflow-y-auto bg-black-900"> - <Markdown content=full_info.content class="absolute h-full w-full top-0 left-0"/> + <Markdown content=full_info.content class="absolute h-full w-full top-0 left-0" /> </div> - <hr class="border-t-2 border-accent mx-1"/> + <hr class="border-t-2 border-accent mx-1" /> <ul class="flex flex-row justify-center space-x-4 p-1"> <li>Memory : {full_info.memory}</li> diff --git a/frontend/src/pages/problem/editor.rs b/frontend/src/pages/problem/editor.rs index 2a00b9f1..852680c2 100644 --- a/frontend/src/pages/problem/editor.rs +++ b/frontend/src/pages/problem/editor.rs @@ -1,7 +1,7 @@ use leptos::*; use tailwind_fuse::tw_join; -use crate::{components::*, errors::*, grpc}; +use crate::{components::*, utils::*}; #[component] pub fn ProblemEditor( @@ -9,9 +9,13 @@ pub fn ProblemEditor( id: i32, langs: grpc::Languages, ) -> impl IntoView { - let select_option = langs.list.into_iter().map(|lang| { + let select_option = langs + .list + .into_iter() + .map(|lang| { view! { <SelectOption value=lang.lang_ext>{lang.lang_name}</SelectOption> } - }).collect_view(); + }) + .collect_view(); let select_lang = create_rw_signal("".to_owned()); let editor_ref = create_editor_ref(); @@ -48,7 +52,10 @@ pub fn ProblemEditor( let disabled = Signal::derive(move || select_lang.with(|v| v.is_empty())); view! { - <form class=tw_join!("flex flex-col h-full w-full bg-lighten p-3 rounded", class) on:submit=submit> + <form + class=tw_join!("flex flex-col h-full w-full bg-lighten p-3 rounded", class) + on:submit=submit + > <ul class="flex flex-row justify-between p-2 pt-0 mb-2 border-b-2 border-accent"> <li>Code</li> @@ -58,7 +65,7 @@ pub fn ProblemEditor( </Select> </li> </ul> - <Editor lang_ext=select_lang editor_ref class="h-full"/> + <Editor lang_ext=select_lang editor_ref class="h-full" /> <Button class="mt-auto" type_="submit" disabled> Submit </Button> diff --git a/frontend/src/pages/problem/problem.rs b/frontend/src/pages/problem/problem.rs index dc7c5ddc..938cbefa 100644 --- a/frontend/src/pages/problem/problem.rs +++ b/frontend/src/pages/problem/problem.rs @@ -2,11 +2,7 @@ use leptos::*; use leptos_router::*; use super::{ProblemContent, ProblemEditor}; -use crate::{ - errors::*, - grpc::{self, WithToken}, - session::*, -}; +use crate::{components::*, utils::*}; #[derive(Params, PartialEq, Clone, Copy)] struct ProblemParams { @@ -17,7 +13,7 @@ struct ProblemParams { pub fn ProblemRouter() -> impl IntoView { view! { <Route path="/problem/:id" view=Problem> - <Route path="" view=Content/> + <Route path="" view=Content /> </Route> } } @@ -45,14 +41,14 @@ fn Problem() -> impl IntoView { v.map(|langs| { let id = params()?.id; - Result::<_>::Ok(view! { <ProblemEditor id langs/> }) + Result::<_>::Ok(view! { <ProblemEditor id langs /> }) }) }) }; view! { <main class="grow grid grid-cols-5 grid-flow-row gap-4"> - <Outlet/> + <Outlet /> <div class="col-span-2 col-start-4"> <Suspense fallback=|| { view! { <p>loading</p> } @@ -86,7 +82,7 @@ fn Content() -> impl IntoView { let content = move || { full_info().map(|v| { v.map(|full_info| { - view! { <ProblemContent full_info/> } + view! { <ProblemContent full_info /> } }) }) }; diff --git a/frontend/src/utils/config.rs b/frontend/src/utils/config.rs index cb039bfb..f5f9b400 100644 --- a/frontend/src/utils/config.rs +++ b/frontend/src/utils/config.rs @@ -3,7 +3,7 @@ use std::sync::OnceLock; use leptos::*; use serde::{Deserialize, Serialize}; -use crate::errors::*; +use super::error::*; #[cfg(feature = "ssr")] static CONFIG: OnceLock<Config> = OnceLock::new(); diff --git a/frontend/src/utils/grpc.rs b/frontend/src/utils/grpc.rs index c87f1c7c..4f0e6e94 100644 --- a/frontend/src/utils/grpc.rs +++ b/frontend/src/utils/grpc.rs @@ -2,7 +2,7 @@ pub use grpc::backend::*; use leptos::*; use tonic::{metadata::MetadataMap, IntoRequest, Request}; -use crate::config::frontend_config; +use super::frontend_config; #[cfg(not(feature = "ssr"))] pub fn new_client() -> tonic_web_wasm_client::Client { From 05aaf13abadfc7e45b2cd9fabb03f55707f057ad Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:18:45 +0800 Subject: [PATCH 10/18] style(Frontend): :art: FMT --- frontend/src/components/highlight.rs | 3 +-- frontend/src/components/toast.rs | 4 ++-- frontend/src/pages/contest.rs | 4 +--- frontend/src/pages/contests.rs | 4 +--- frontend/src/pages/rank.rs | 6 ++---- frontend/src/pages/submission.rs | 6 ++---- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/highlight.rs b/frontend/src/components/highlight.rs index 3e6ae12f..c565a51c 100644 --- a/frontend/src/components/highlight.rs +++ b/frontend/src/components/highlight.rs @@ -26,8 +26,7 @@ pub fn Highlight() -> impl IntoView { logging::log!("change"); set_value(event_target_value(&e)); } - > - </textarea> + ></textarea> <div inner_html=html></div> </div> } diff --git a/frontend/src/components/toast.rs b/frontend/src/components/toast.rs index e1dc1473..3528ffda 100644 --- a/frontend/src/components/toast.rs +++ b/frontend/src/components/toast.rs @@ -145,8 +145,8 @@ fn Toast( variant )> <div class="text-sm">{children()}</div> - <button class="w-6 h-6 pl-2" on:click=move |_| close()> - <Icon icon=icondata::AiCloseOutlined/> + <button class="size-6 pl-2" on:click=move |_| close()> + <Icon icon=icondata::AiCloseOutlined /> </button> </div> <style>{STYLE_SHEET}</style> diff --git a/frontend/src/pages/contest.rs b/frontend/src/pages/contest.rs index e7ff3446..a0f87cea 100644 --- a/frontend/src/pages/contest.rs +++ b/frontend/src/pages/contest.rs @@ -2,7 +2,5 @@ use leptos::*; #[component] pub fn About() -> impl IntoView { - view! { - <h1>About</h1> - } + view! { <h1>About</h1> } } diff --git a/frontend/src/pages/contests.rs b/frontend/src/pages/contests.rs index d239ab2e..585fddd6 100644 --- a/frontend/src/pages/contests.rs +++ b/frontend/src/pages/contests.rs @@ -4,7 +4,5 @@ use crate::components::*; #[component] pub fn Contests() -> impl IntoView { - view! { - <h1>Contest</h1> - } + view! { <h1>Contest</h1> } } diff --git a/frontend/src/pages/rank.rs b/frontend/src/pages/rank.rs index 49e50a4b..bf11055b 100644 --- a/frontend/src/pages/rank.rs +++ b/frontend/src/pages/rank.rs @@ -1,10 +1,8 @@ use leptos::*; -use crate::components::*; +// use crate::components::*; #[component] pub fn Rank() -> impl IntoView { - view! { - <h1>Rank</h1> - } + view! { <h1>Rank</h1> } } diff --git a/frontend/src/pages/submission.rs b/frontend/src/pages/submission.rs index 84abd797..fc8524b4 100644 --- a/frontend/src/pages/submission.rs +++ b/frontend/src/pages/submission.rs @@ -1,10 +1,8 @@ use leptos::*; -use crate::components::*; +// use crate::components::*; #[component] pub fn Submission() -> impl IntoView { - view! { - <h1>Submission</h1> - } + view! { <h1>Submission</h1> } } From e67744de1b5952dde0d654ee104fe695258a7971 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:19:26 +0800 Subject: [PATCH 11/18] fix(Grpc): :rotating_light: close #67 --- grpc/src/bridge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc/src/bridge.rs b/grpc/src/bridge.rs index 01fa4735..7df6057e 100644 --- a/grpc/src/bridge.rs +++ b/grpc/src/bridge.rs @@ -1,6 +1,6 @@ // use crate::backend::{self, playground_result, PlaygroundResult}; use crate::backend; -use crate::judger::{self, exec_result, ExecResult}; +use crate::judger; // impl From<ExecResult> for PlaygroundResult { // fn from(value: ExecResult) -> Self { From 34aac010c7fa53b5e8ceee935a2fe64455ce399c Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:20:27 +0800 Subject: [PATCH 12/18] feat(Frontend): :sparkles: close #36 add search bar --- frontend/src/components/search_bar.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 frontend/src/components/search_bar.rs diff --git a/frontend/src/components/search_bar.rs b/frontend/src/components/search_bar.rs new file mode 100644 index 00000000..ee961bd3 --- /dev/null +++ b/frontend/src/components/search_bar.rs @@ -0,0 +1,21 @@ +use leptos::*; +use leptos_icons::*; +use tailwind_fuse::*; + +use crate::components::*; + +#[component] +pub fn SearchBar( + #[prop(into, optional)] class: String, + submit: impl Fn(ev::SubmitEvent, String) + 'static, +) -> impl IntoView { + let search = create_rw_signal("".to_owned()); + view! { + <form on:submit=move |e| submit(e, search.get_untracked()) class=tw_join!("relative",class)> + <Input value=search class="flex-grow"></Input> + <button type="submit" class="absolute right-4 top-0 h-full"> + <Icon icon=icondata::BsSearch /> + </button> + </form> + } +} From 101d7e3c56880e902194aa01a8fec371c231db83 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:22:47 +0800 Subject: [PATCH 13/18] feat(Frontend): :sparkles: close #35 Add problems page & some component --- frontend/src/components/paginate_navbar.rs | 96 +++++++++ frontend/src/components/paginate_table.rs | 77 +++++++ frontend/src/pages/problems.rs | 131 +++++++++++- frontend/src/utils/paginate.rs | 188 ++++++++++++++++ frontend/src/utils/query.rs | 13 ++ frontend/src/utils/router.rs | 236 +++++++++++++++++++++ 6 files changed, 739 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/paginate_navbar.rs create mode 100644 frontend/src/components/paginate_table.rs create mode 100644 frontend/src/utils/paginate.rs create mode 100644 frontend/src/utils/query.rs create mode 100644 frontend/src/utils/router.rs diff --git a/frontend/src/components/paginate_navbar.rs b/frontend/src/components/paginate_navbar.rs new file mode 100644 index 00000000..d851c14f --- /dev/null +++ b/frontend/src/components/paginate_navbar.rs @@ -0,0 +1,96 @@ +use leptos::*; +use leptos_router::*; +use tailwind_fuse::*; + +use crate::utils::*; + +#[component] +/// There are 4 different case +/// ``` +/// let right_half = size / 2 +/// let left_half = size - right_half +/// +/// 0 1 [right_half] [page] [left_half] ... [max] // leftmost page < [max-1] +/// 0 ... [right_half] [page] [left_half] [max-1] [max] // rightmost page > 1 +/// 0 ... [right_half] [page] [left_half] ... [max] // combination of the above +/// [0..page] [page] [page..=max] // [max] < [size+2] +/// ``` +pub fn PaginateNavbar( + #[prop(default = 1)] size: u32, + #[prop(into)] max_page: Signal<u32>, + #[prop(into)] page: ParamsMapKey<u32>, +) -> impl IntoView { + let left_half = size / 2; + let right_half = size - left_half; + let page_index = use_query_map().use_query_with_default(page); + + view! { + <nav class="grid grid-flow-col auto-cols-max gap-1 text-center"> + <PaginateNavbarButton i=0 page /> + <Show when=move || 1 <= max_page()> + <Show + when=move || page_index() <= left_half + 2 + fallback=|| view! { <PaginateNavbarHidden /> } + > + <PaginateNavbarButton i=1 page /> + </Show> + </Show> + <For + each=move || { + page_index() + .saturating_sub(left_half) + .max(2)..=(page_index() + right_half).min(max_page().saturating_sub(2)) + } + key=|i| *i + children=move |i| { + view! { <PaginateNavbarButton i page /> } + } + ></For> + <Show when=move || 2 <= max_page()> + <Show + when=move || max_page() <= page_index() + right_half + 2 + fallback=|| view! { <PaginateNavbarHidden /> } + > + <PaginateNavbarButton + i=(move || max_page().saturating_sub(1)).into_signal() + page + /> + </Show> + </Show> + <Show when=move || 3 <= max_page()> + <PaginateNavbarButton i=max_page page /> + </Show> + </nav> + } +} + +#[component] +fn PaginateNavbarButton( + #[prop(into)] i: MaybeSignal<u32>, + #[prop(into)] page: ParamsMapKey<u32>, +) -> impl IntoView { + let query_map = use_query_map(); + let href = + query_map.with_query_map(move |map| map.set_query(page, Some(i()))); + let page = query_map.use_query_with_default(page); + let disabled = create_memo(move |_| page() == i()); + view! { + <A + href + class=move || { + tw_join!( + "size-8", + disabled().then_some("bg-primary disabled").unwrap_or("bg-black-900") + ) + } + > + + {i} + </A> + } +} + +#[component] +fn PaginateNavbarHidden() -> impl IntoView { + view! { <p class="bg-black-900 size-8">...</p> } +} diff --git a/frontend/src/components/paginate_table.rs b/frontend/src/components/paginate_table.rs new file mode 100644 index 00000000..88607979 --- /dev/null +++ b/frontend/src/components/paginate_table.rs @@ -0,0 +1,77 @@ +use leptos::*; +use leptos_router::*; +use tailwind_fuse::*; + +use crate::utils::*; + +#[component] +pub fn PaginateTable<const N: usize, S, H>( + #[prop(into)] headers: [(Option<S::Output>, View); N], + #[prop(into)] rows: Vec<(H, [View; N])>, + #[prop(into, optional)] class: String, + #[prop(into)] sort: ParamsMapKey<S>, + #[prop(into)] order: ParamsMapKey<GrpcEnum<grpc::Order>>, +) -> impl IntoView +where + S: QueryType + 'static, + H: ToHref + 'static, +{ + let query_map = use_query_map(); + let headers = headers.map(|(s, col)| { + let navigate = use_navigate(); + let click = move |_| { + let Some(s) = s.clone() else { + return; + }; + let mut query_map = query_map.get_untracked(); + if s == query_map.get_query_with_default(sort) { + let toggle_order = match query_map.get_query_with_default(order) + { + grpc::Order::Ascend => grpc::Order::Descend, + grpc::Order::Descend => grpc::Order::Ascend, + }; + query_map.set_query(order, Some(toggle_order)); + } else { + query_map.set_query(order, None); + query_map.set_query(sort, Some(s)); + } + navigate( + &query_map.to_url(), + NavigateOptions { + scroll: true, + ..Default::default() + }, + ); + }; + view! { + <th> + <button on:click=click>{col}</button> + </th> + } + }); + let rows = rows + .into_iter() + .map(|(href, cols)| view! { <Row cols href /> }) + .collect_view(); + view! { + <table class=tw_join!("w-full grid gap-x-4", class)> + <thead class="grid col-span-full grid-cols-subgrid font-bold text-base border-b-2 border-black-400 bg-black-900 p-4"> + <tr class="contents">{headers}</tr> + </thead> + <tbody class="contents">{rows}</tbody> + </table> + } +} + +#[component] +fn Row<const N: usize>( + cols: [View; N], + href: impl ToHref + 'static, +) -> impl IntoView { + let cols = cols.map(|v| view! { <td>{v}</td> }).collect_view(); + view! { + <A class="grid col-span-full grid-cols-subgrid even:bg-black-900 text-sm p-4" href> + <tr class="grid col-span-full grid-cols-subgrid">{cols}</tr> + </A> + } +} diff --git a/frontend/src/pages/problems.rs b/frontend/src/pages/problems.rs index 62c10c3e..6b08ea59 100644 --- a/frontend/src/pages/problems.rs +++ b/frontend/src/pages/problems.rs @@ -1,11 +1,138 @@ use leptos::*; use leptos_router::*; -use crate::components::*; +use crate::{ + components::*, + utils::{grpc::list_problem_request as lp_req, *}, +}; + +async fn fetcher( + paginator: Paginator<lp_req::Create>, + size: u64, + token: Option<String>, +) -> Result<(String, u64, Vec<grpc::ProblemInfo>)> { + let request = match paginator { + Paginator::Create(offset, create) => grpc::ListProblemRequest { + size, + offset, + request: Some(lp_req::Request::Create(create)), + }, + Paginator::Paginate(offset, paginator) => grpc::ListProblemRequest { + size, + offset, + request: Some(lp_req::Request::Paginator(paginator)), + }, + }; + let mut client = + grpc::problem_client::ProblemClient::new(grpc::new_client()); + let list = client + .list(request.with_optional_token(token)) + .await? + .into_inner(); + Result::<_>::Ok((list.paginator, list.remain, list.list)) +} #[component] pub fn Problems() -> impl IntoView { + let mut problem_query = create_paginate_query(fetcher, Default::default()); + + let params_map = use_query_map(); + let page = create_params_map_key("p", 0u32); + let order = create_params_map_key("o", GrpcEnum(grpc::Order::Ascend)); + let sort = create_params_map_key("s", GrpcEnum(lp_req::Sort::Order)); + let text = create_params_map_key("t", "".to_owned()); + + let info = Signal::derive(move || { + params_map.with(|map| lp_req::Create { + order: map.get_query_with_default(order).into(), + query: Some(lp_req::Query { + contest_id: None, + sort_by: map.get_query(sort).map(|v| v.into()), + text: map.get_query(text), + }), + }) + }); + + let page_value = params_map.use_query_with_default(page); + + let query_result = problem_query.query(move || (page_value(), info())); + let table = move || { + query_result + .data + .get() + .map(|d| d.map(|infos| view! { <Table infos sort order></Table> })) + }; + + let max_page = query_result.max_page; + + let navigate = use_navigate(); + let submit = move |e: ev::SubmitEvent, search: String| { + e.prevent_default(); + let mut map = params_map.get_untracked(); + if search.is_empty() { + map.set_query(text, None); + } else { + map.set_query(text, Some(search)); + map.set_query(page, None); + } + + navigate( + &map.to_url(), + NavigateOptions { + scroll: true, + ..Default::default() + }, + ) + }; + view! { + <div class="container min-h-full flex flex-col items-center gap-4 py-10"> + <nav class="self-end"> + <SearchBar submit /> + </nav> + <Suspense fallback=|| view! { "loading" }> + <ErrorFallback>{table}</ErrorFallback> + </Suspense> + <PaginateNavbar size=4 page max_page /> + </div> + } +} + +#[component] +fn Table( + infos: Vec<grpc::ProblemInfo>, + sort: ParamsMapKey<GrpcEnum<lp_req::Sort>>, + order: ParamsMapKey<GrpcEnum<grpc::Order>>, +) -> impl IntoView { + let headers = [ + (Some(lp_req::Sort::Order), "Id".into_view()), + (None, "Title".into_view()), + (Some(lp_req::Sort::Difficulty), "Difficulty".into_view()), + (Some(lp_req::Sort::SubmitCount), "Submit".into_view()), + (Some(lp_req::Sort::AcRate), "Ac Rate".into_view()), + ]; + let rows: Vec<_> = infos + .into_iter() + .map(|info| { + ( + format!("/problem/{}", info.id), + [ + format!("{:04}", info.id).into_view(), + info.title.into_view(), + info.difficulty.into_view(), + info.submit_count.into_view(), + format!("{:.2}", info.ac_rate * 100.0).into_view(), + ], + ) + }) + .collect(); + view! { - <div class="container flex items-center justify-between text-lg"></div> + <PaginateTable + class="grid-cols-[max-content_1fr_max-content_max-content_max-content]" + headers + rows + sort + order + /> } } diff --git a/frontend/src/utils/paginate.rs b/frontend/src/utils/paginate.rs new file mode 100644 index 00000000..a910a9e5 --- /dev/null +++ b/frontend/src/utils/paginate.rs @@ -0,0 +1,188 @@ +use std::{ + fmt::Debug, + future::Future, + hash::{DefaultHasher, Hash, Hasher}, +}; + +use leptos::*; +use leptos_query::*; + +use super::{config::*, error::*, session::*}; + +mod private { + use super::*; + + pub trait PaginateQueryKey: Debug + Clone + Hash + PartialEq {} + + impl<T> PaginateQueryKey for T where T: Debug + Clone + Hash + PartialEq {} + + #[derive(Debug, Clone)] + pub struct InnerPaginateQueryKey<Info: PaginateQueryKey + 'static> { + pub page: i64, + pub info: Info, + pub prev_paginator: Option<(i64, String)>, + } + + impl<Info: PaginateQueryKey + 'static> Hash for InnerPaginateQueryKey<Info> { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.page.hash(state); + self.info.hash(state); + } + } + + impl<T: PaginateQueryKey + 'static> PartialEq for InnerPaginateQueryKey<T> { + fn eq(&self, other: &Self) -> bool { + self.page == other.page && self.info == other.info + } + } + + impl<Info: PaginateQueryKey + 'static> Eq for InnerPaginateQueryKey<Info> {} +} +use private::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Paginator<T> { + /// offset item count `.0` from beginning + Create(i64, T), + /// offset item count `.0` from `.1` paginator + Paginate(i64, String), +} + +/// `fetcher` is `fn(Paginator<Info>, page index, token) -> (next paginator, remain item count, Data)` +pub fn create_paginate_query<T, Fu, Info, Data>( + fetcher: T, + options: QueryOptions<Result<Data>>, +) -> PaginateQuery<Info, Data> +where + T: Fn(Paginator<Info>, u64, Option<String>) -> Fu + 'static, + Fu: Future<Output = Result<(String, u64, Data)>> + 'static, + Info: PaginateQueryKey + 'static, + Data: QueryValue + 'static, + InnerPaginateQueryKey<Info>: QueryKey + 'static, + Result<Data>: QueryValue + 'static, +{ + let max_page = create_rw_signal(0); + + let prev_paginator = store_value(None); + + let page_size = frontend_config().page_size; + let token = use_token(); + + let fetcher = move |query_token: InnerPaginateQueryKey<Info>| { + let (paginator, next_paginator_index) = + if let Some((index, paginator)) = query_token.prev_paginator { + let delta = query_token.page - index; + ( + Paginator::Paginate(delta * page_size as i64, paginator), + query_token.page + (!delta.is_negative()) as i64, + ) + } else { + ( + Paginator::Create( + query_token.page * page_size as i64, + query_token.info, + ), + query_token.page + 1, + ) + }; + let fu = fetcher(paginator, page_size as u64, token.get_untracked()); + async move { + let (next_paginator, remain_item, data) = fu.await?; + + let remain_page = remain_item.div_ceil(page_size as u64) as u32; + + // FIXME: move this to result, bc this only trigger when refetch + if remain_page != 0 { + prev_paginator + .set_value(Some((next_paginator_index, next_paginator))); + } + + let page = query_token.page as u32 + remain_page; + if max_page.get_untracked() < page { + max_page.set(page); + } + + Ok(data) + } + }; + let scope = create_query(fetcher, options); + + PaginateQuery { + version: Default::default(), + prev_paginator, + max_page, + scope, + } +} + +#[derive(Clone)] +pub struct PaginateQuery<Info, Data> +where + Info: PaginateQueryKey + 'static, + InnerPaginateQueryKey<Info>: QueryKey + 'static, + // (page index, next paginator, remain page count, Data) + Result<Data>: QueryValue + 'static, +{ + version: StoredValue<u64>, + prev_paginator: StoredValue<Option<(i64, String)>>, + max_page: RwSignal<u32>, + scope: QueryScope<InnerPaginateQueryKey<Info>, Result<Data>>, +} + +#[derive(Clone, Copy)] +pub struct PaginateQueryResult<Data: 'static> { + /// Should be called inside of a [`Transition`](leptos::Transition) or [`Suspense`](leptos::Suspense) component. + pub data: Signal<Option<Result<Data>>>, + /// How max page count + pub max_page: Signal<u32>, +} + +impl<Info, Data> PaginateQuery<Info, Data> +where + Info: PaginateQueryKey + 'static, + InnerPaginateQueryKey<Info>: QueryKey + 'static, + Result<Data>: QueryValue + 'static, +{ + /// `key` is `fn() -> (page index, Info)` + pub fn query( + &mut self, + key: impl Fn() -> (u32, Info) + 'static, + ) -> PaginateQueryResult<Data> { + let version = self.version; + let prev_paginator = self.prev_paginator; + let max_page = self.max_page; + let current_page = create_rw_signal(0); + + let query = self.scope.use_query(move || { + let (page, info) = key(); + + // cache invalidation + let mut hasher = DefaultHasher::new(); + info.hash(&mut hasher); + let hash = hasher.finish(); + if hash != version() { + version.set_value(hash); + prev_paginator.set_value(None); + max_page.set(0); + } + + current_page.set(page); + InnerPaginateQueryKey { + page: page as i64, + info, + prev_paginator: prev_paginator(), + } + }); + + let data = Signal::derive(move || { + (query.data)().map(|v| { + let data = v?; + + Result::<_>::Ok(data) + }) + }); + let max_page = self.max_page.into_signal(); + + PaginateQueryResult { data, max_page } + } +} diff --git a/frontend/src/utils/query.rs b/frontend/src/utils/query.rs new file mode 100644 index 00000000..65fee7d9 --- /dev/null +++ b/frontend/src/utils/query.rs @@ -0,0 +1,13 @@ +use leptos::*; +use leptos_query::*; + +use super::session::*; + +pub fn provide_query_service() { + provide_query_client(); + let token = use_token(); + let client = use_query_client(); + create_effect(move |_| { + token.with(|_| client.invalidate_all_queries()); + }); +} diff --git a/frontend/src/utils/router.rs b/frontend/src/utils/router.rs new file mode 100644 index 00000000..73376d49 --- /dev/null +++ b/frontend/src/utils/router.rs @@ -0,0 +1,236 @@ +use std::{marker::PhantomData, str::FromStr}; + +use leptos::*; +use leptos_router::*; + +pub trait QueryType { + type Output: Clone + PartialEq + 'static; + fn inner(self) -> Self::Output; + fn convert_to_type(s: &str) -> Option<Self::Output>; + fn convert_to_string(o: Self::Output) -> String; +} + +impl<T> QueryType for T +where + T: FromStr + ToString + Clone + PartialEq + 'static, +{ + type Output = T; + + fn inner(self) -> Self::Output { + self + } + + fn convert_to_type(s: &str) -> Option<Self::Output> { + s.parse().ok() + } + + fn convert_to_string(o: Self::Output) -> String { + o.to_string() + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct GrpcEnum<T>(pub T); + +impl<T> QueryType for GrpcEnum<T> +where + T: TryFrom<i32> + Into<i32> + Clone + PartialEq + 'static, +{ + type Output = T; + + fn inner(self) -> Self::Output { + self.0 + } + + fn convert_to_type(s: &str) -> Option<Self::Output> { + s.parse().ok().map(|n| T::try_from(n).ok()).flatten() + } + + fn convert_to_string(o: Self::Output) -> String { + o.into().to_string() + } +} + +pub struct ParamsMapKey<T: QueryType>(StoredValue<InnerParamsMapKey<T>>) +where + InnerParamsMapKey<T>: 'static; + +impl<T: QueryType> Clone for ParamsMapKey<T> { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl<T: QueryType> Copy for ParamsMapKey<T> {} + +#[derive(Debug)] +pub struct InnerParamsMapKey<T: QueryType> { + key: &'static str, + default: T::Output, + _maker: PhantomData<T>, +} + +impl<T: QueryType> Clone for InnerParamsMapKey<T> { + fn clone(&self) -> Self { + Self { + key: self.key, + default: self.default.clone(), + _maker: self._maker.clone(), + } + } +} + +impl<T: QueryType> ParamsMapKey<T> { + pub fn new(key: &'static str, default: T) -> Self { + Self(store_value(InnerParamsMapKey { + key, + default: default.inner(), + _maker: Default::default(), + })) + } + + pub fn key(&self) -> &'static str { + self.0.with_value(|v| v.key) + } +} + +impl<T: QueryType> ParamsMapKey<T> { + pub fn default(&self) -> T::Output { + self.0.with_value(|v| v.default.clone()) + } +} + +pub fn create_params_map_key<T: QueryType>( + key: &'static str, + default: T, +) -> ParamsMapKey<T> { + ParamsMapKey::new(key, default) +} + +pub trait MemoParamsMapExtra { + /// generate new query string, should use with [`use_query_map`] + fn with_query_map( + &self, + f: impl Fn(&mut ParamsMap) + 'static, + ) -> Signal<String>; + + /// generate new url, should use with [`use_query_map`] + fn with_query_map_url( + &self, + f: impl Fn(&mut ParamsMap) + 'static, + ) -> Signal<String>; + + fn use_query<T: QueryType>( + &self, + query: ParamsMapKey<T>, + ) -> Signal<Option<T::Output>>; + + fn use_query_with_default<T: QueryType>( + &self, + query: ParamsMapKey<T>, + ) -> Signal<T::Output>; +} + +impl MemoParamsMapExtra for Memo<ParamsMap> { + fn with_query_map( + &self, + f: impl Fn(&mut ParamsMap) + 'static, + ) -> Signal<String> { + let map = self.clone(); + Signal::derive(move || { + let mut map = map(); + f(&mut map); + map.to_query_string() + }) + } + + fn with_query_map_url( + &self, + f: impl Fn(&mut ParamsMap) + 'static, + ) -> Signal<String> { + let query = self.with_query_map(f); + let location = use_location(); + let pathname = location.pathname; + let hash = location.hash; + Signal::derive(move || format!("{}{}{}", pathname(), hash(), query())) + } + + fn use_query<T: QueryType>( + &self, + query: ParamsMapKey<T>, + ) -> Signal<Option<T::Output>> { + let map = self.clone(); + Signal::derive(move || map().get_query(query)) + } + + fn use_query_with_default<T: QueryType>( + &self, + query: ParamsMapKey<T>, + ) -> Signal<T::Output> { + let map = self.clone(); + Signal::derive(move || map().get_query_with_default(query)) + } +} + +pub trait ParamsMapExtra { + fn get_query<T>(&self, query: ParamsMapKey<T>) -> Option<T::Output> + where + T: QueryType; + + fn get_query_with_default<T>(&self, query: ParamsMapKey<T>) -> T::Output + where + T: QueryType; + + fn set_query<T>( + &mut self, + query: ParamsMapKey<T>, + value: Option<T::Output>, + ) where + T: QueryType; + + fn to_url(&self) -> String; +} + +impl ParamsMapExtra for ParamsMap { + fn get_query<T>(&self, query: ParamsMapKey<T>) -> Option<T::Output> + where + T: QueryType, + { + self.get(query.key()) + .map(|v| T::convert_to_type(v)) + .flatten() + } + + fn get_query_with_default<T>(&self, query: ParamsMapKey<T>) -> T::Output + where + T: QueryType, + { + self.get_query(query) + .unwrap_or_else(move || query.default()) + } + + fn set_query<T>(&mut self, query: ParamsMapKey<T>, value: Option<T::Output>) + where + T: QueryType, + { + let value = value + .map(move |v| { + query.0.with_value(|query| v != query.default).then_some(v) + }) + .flatten(); + match value { + Some(value) => { + self.insert(query.key().to_owned(), T::convert_to_string(value)) + } + None => self.remove(query.key()), + }; + } + + fn to_url(&self) -> String { + let query = self.to_query_string(); + let location = use_location(); + let pathname = location.pathname.get_untracked(); + let hash = location.hash.get_untracked(); + format!("{pathname}{hash}{query}") + } +} From c568be15081ccdb081d7a272f8a3a9eb08209094 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:23:40 +0800 Subject: [PATCH 14/18] chore(Frontend): :rotating_light: Fix lint warning --- frontend/src/components/editor.rs | 8 ++++---- frontend/src/components/errors/mod.rs | 6 ++++++ frontend/src/components/mod.rs | 8 ++++++++ frontend/src/pages/about.rs | 6 ++---- frontend/src/pages/contests.rs | 2 +- frontend/src/pages/mod.rs | 9 +-------- frontend/src/utils/config.rs | 1 + frontend/src/utils/grpc.rs | 1 - frontend/src/utils/mod.rs | 15 +++++++++++++++ 9 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/errors/mod.rs create mode 100644 frontend/src/utils/mod.rs diff --git a/frontend/src/components/editor.rs b/frontend/src/components/editor.rs index 55ca3dd2..2b8504a2 100644 --- a/frontend/src/components/editor.rs +++ b/frontend/src/components/editor.rs @@ -69,17 +69,17 @@ pub fn Editor( let c = Object::new(); let paths = Object::new(); Reflect::set( - &*paths, + &paths, &"vs".into(), &"https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs".into(), ) .unwrap(); - Reflect::set(&*c, &"paths".into(), &*paths).unwrap(); + Reflect::set(&c, &"paths".into(), &paths).unwrap(); loader_config(c); let config = Object::new(); - Reflect::set(&*config, &"theme".into(), &"vs-dark".into()).unwrap(); - Reflect::set(&*config, &"automaticLayout".into(), &true.into()) + Reflect::set(&config, &"theme".into(), &"vs-dark".into()).unwrap(); + Reflect::set(&config, &"automaticLayout".into(), &true.into()) .unwrap(); let init_monaco = Closure::once_into_js(move || { diff --git a/frontend/src/components/errors/mod.rs b/frontend/src/components/errors/mod.rs new file mode 100644 index 00000000..328577b2 --- /dev/null +++ b/frontend/src/components/errors/mod.rs @@ -0,0 +1,6 @@ +mod error_fallback; +mod internal_server_error; +mod not_found; +pub use error_fallback::*; +pub use internal_server_error::*; +pub use not_found::*; diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index b3c8bbaf..9dd08c7b 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -1,6 +1,7 @@ pub mod badge; pub mod button; pub mod editor; +pub mod errors; pub mod footer; pub mod highlight; pub mod input; @@ -8,7 +9,10 @@ pub mod input_number; pub mod markdown; pub mod modal; pub mod navbar; +pub mod paginate_navbar; +pub mod paginate_table; pub mod redirect_if; +pub mod search_bar; pub mod select; pub mod toast; pub mod toggle; @@ -16,6 +20,7 @@ pub mod toggle; pub use badge::Badge; pub use button::{Button, ButtonVariant}; pub use editor::{create_editor_ref, Editor}; +pub use errors::*; pub use footer::Footer; pub use highlight::Highlight; pub use input::{Input, InputVariant}; @@ -23,6 +28,9 @@ pub use input_number::InputNumber; pub use markdown::Markdown; pub use modal::{Modal, ModalLevel}; pub use navbar::Navbar; +pub use paginate_navbar::PaginateNavbar; +pub use paginate_table::PaginateTable; pub use redirect_if::RedirectIf; +pub use search_bar::SearchBar; pub use select::{Select, SelectOption}; pub use toast::{use_toast, ProvideToast, ToastVariant}; diff --git a/frontend/src/pages/about.rs b/frontend/src/pages/about.rs index ee7f57cb..6516fa7b 100644 --- a/frontend/src/pages/about.rs +++ b/frontend/src/pages/about.rs @@ -1,10 +1,8 @@ use leptos::*; -use crate::components::*; +// use crate::components::*; #[component] pub fn About() -> impl IntoView { - view! { - <h1>About</h1> - } + view! { <h1>About</h1> } } diff --git a/frontend/src/pages/contests.rs b/frontend/src/pages/contests.rs index 585fddd6..7da86cf8 100644 --- a/frontend/src/pages/contests.rs +++ b/frontend/src/pages/contests.rs @@ -1,6 +1,6 @@ use leptos::*; -use crate::components::*; +// use crate::components::*; #[component] pub fn Contests() -> impl IntoView { diff --git a/frontend/src/pages/mod.rs b/frontend/src/pages/mod.rs index 612287a4..55ff8549 100644 --- a/frontend/src/pages/mod.rs +++ b/frontend/src/pages/mod.rs @@ -8,11 +8,4 @@ mod problem; mod problems; mod rank; mod submission; -use about::About; -use contests::Contests; -use home::Home; -use login::Login; -pub use pages::Pages; -use problems::Problems; -use rank::Rank; -use submission::Submission; +pub use pages::*; diff --git a/frontend/src/utils/config.rs b/frontend/src/utils/config.rs index f5f9b400..1a7ade02 100644 --- a/frontend/src/utils/config.rs +++ b/frontend/src/utils/config.rs @@ -3,6 +3,7 @@ use std::sync::OnceLock; use leptos::*; use serde::{Deserialize, Serialize}; +#[cfg(feature = "ssr")] use super::error::*; #[cfg(feature = "ssr")] diff --git a/frontend/src/utils/grpc.rs b/frontend/src/utils/grpc.rs index 4f0e6e94..563a9cc7 100644 --- a/frontend/src/utils/grpc.rs +++ b/frontend/src/utils/grpc.rs @@ -1,5 +1,4 @@ pub use grpc::backend::*; -use leptos::*; use tonic::{metadata::MetadataMap, IntoRequest, Request}; use super::frontend_config; diff --git a/frontend/src/utils/mod.rs b/frontend/src/utils/mod.rs new file mode 100644 index 00000000..3c353220 --- /dev/null +++ b/frontend/src/utils/mod.rs @@ -0,0 +1,15 @@ +pub mod config; +mod error; +pub mod grpc; +mod paginate; +mod query; +mod router; +mod session; + +pub use config::{frontend_config, FrontendConfig}; +pub use error::*; +pub use grpc::WithToken; +pub use paginate::*; +pub use query::*; +pub use router::*; +pub use session::*; From 6998c85dcb95ba9638b43fb3f574930321b6437f Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:29:17 +0800 Subject: [PATCH 15/18] style(Frontend): :bug: Fix flashing when change page --- frontend/style/main.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/style/main.scss b/frontend/style/main.scss index 6ee50e00..cefe102d 100644 --- a/frontend/style/main.scss +++ b/frontend/style/main.scss @@ -8,4 +8,5 @@ html { overflow-x: hidden; font-family: "Noto Sans TC"; font-weight: 500; + background-color: black; } \ No newline at end of file From 174487ac05392031c12f6a23ba93ec5efc646772 Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:29:57 +0800 Subject: [PATCH 16/18] style(Frontend): :art: clippy --- frontend/src/components/editor.rs | 5 ++--- frontend/src/components/paginate_navbar.rs | 2 +- frontend/src/components/toast.rs | 2 +- frontend/src/utils/config.rs | 8 +------- frontend/src/utils/grpc.rs | 1 + frontend/src/utils/router.rs | 24 +++++++++------------- frontend/src/utils/session.rs | 2 +- 7 files changed, 17 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/editor.rs b/frontend/src/components/editor.rs index 2b8504a2..759f66bb 100644 --- a/frontend/src/components/editor.rs +++ b/frontend/src/components/editor.rs @@ -79,8 +79,7 @@ pub fn Editor( let config = Object::new(); Reflect::set(&config, &"theme".into(), &"vs-dark".into()).unwrap(); - Reflect::set(&config, &"automaticLayout".into(), &true.into()) - .unwrap(); + Reflect::set(&config, &"automaticLayout".into(), &true.into()).unwrap(); let init_monaco = Closure::once_into_js(move || { let editor = MONACO.editor().create_editor( @@ -98,7 +97,7 @@ pub fn Editor( create_effect(move |_| { editor_ref.with(|editor| { - let Some(model) = editor.as_ref().map(|e| e.get_model()).flatten() + let Some(model) = editor.as_ref().and_then(|e| e.get_model()) else { return; }; diff --git a/frontend/src/components/paginate_navbar.rs b/frontend/src/components/paginate_navbar.rs index d851c14f..ce9c2248 100644 --- a/frontend/src/components/paginate_navbar.rs +++ b/frontend/src/components/paginate_navbar.rs @@ -80,7 +80,7 @@ fn PaginateNavbarButton( class=move || { tw_join!( "size-8", - disabled().then_some("bg-primary disabled").unwrap_or("bg-black-900") + if disabled() { "bg-primary disabled" } else { "bg-black-900" } ) } > diff --git a/frontend/src/components/toast.rs b/frontend/src/components/toast.rs index 3528ffda..8707c930 100644 --- a/frontend/src/components/toast.rs +++ b/frontend/src/components/toast.rs @@ -105,7 +105,7 @@ fn Toast( is_pending, .. } = { - let close = close.clone(); + let close = close; use_timeout_fn(move |_| close(), 4.0 * 1000.0) }; diff --git a/frontend/src/utils/config.rs b/frontend/src/utils/config.rs index 1a7ade02..731c4300 100644 --- a/frontend/src/utils/config.rs +++ b/frontend/src/utils/config.rs @@ -91,18 +91,12 @@ impl Default for FrontendConfig { } } -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] pub struct BackendConfig { #[serde(default)] pub trust_xff: bool, } -impl Default for BackendConfig { - fn default() -> Self { - Self { trust_xff: false } - } -} - #[cfg(feature = "ssr")] pub async fn init_config() -> Result<()> { let config = load_config().await?; diff --git a/frontend/src/utils/grpc.rs b/frontend/src/utils/grpc.rs index 563a9cc7..09d2a5d5 100644 --- a/frontend/src/utils/grpc.rs +++ b/frontend/src/utils/grpc.rs @@ -59,6 +59,7 @@ where #[cfg(feature = "ssr")] fn with_xff(metadata: MetadataMap) -> MetadataMap { use actix_web::http::header; + use leptos::*; use leptos_actix::ResponseOptions; let mut header_map = metadata.into_headers(); diff --git a/frontend/src/utils/router.rs b/frontend/src/utils/router.rs index 73376d49..7e148d50 100644 --- a/frontend/src/utils/router.rs +++ b/frontend/src/utils/router.rs @@ -43,7 +43,7 @@ where } fn convert_to_type(s: &str) -> Option<Self::Output> { - s.parse().ok().map(|n| T::try_from(n).ok()).flatten() + s.parse().ok().and_then(|n| T::try_from(n).ok()) } fn convert_to_string(o: Self::Output) -> String { @@ -57,7 +57,7 @@ where impl<T: QueryType> Clone for ParamsMapKey<T> { fn clone(&self) -> Self { - Self(self.0.clone()) + Self(self.0) } } @@ -75,7 +75,7 @@ impl<T: QueryType> Clone for InnerParamsMapKey<T> { Self { key: self.key, default: self.default.clone(), - _maker: self._maker.clone(), + _maker: self._maker, } } } @@ -136,7 +136,7 @@ impl MemoParamsMapExtra for Memo<ParamsMap> { &self, f: impl Fn(&mut ParamsMap) + 'static, ) -> Signal<String> { - let map = self.clone(); + let map = *self; Signal::derive(move || { let mut map = map(); f(&mut map); @@ -159,7 +159,7 @@ impl MemoParamsMapExtra for Memo<ParamsMap> { &self, query: ParamsMapKey<T>, ) -> Signal<Option<T::Output>> { - let map = self.clone(); + let map = *self; Signal::derive(move || map().get_query(query)) } @@ -167,7 +167,7 @@ impl MemoParamsMapExtra for Memo<ParamsMap> { &self, query: ParamsMapKey<T>, ) -> Signal<T::Output> { - let map = self.clone(); + let map = *self; Signal::derive(move || map().get_query_with_default(query)) } } @@ -196,9 +196,7 @@ impl ParamsMapExtra for ParamsMap { where T: QueryType, { - self.get(query.key()) - .map(|v| T::convert_to_type(v)) - .flatten() + self.get(query.key()).and_then(|v| T::convert_to_type(v)) } fn get_query_with_default<T>(&self, query: ParamsMapKey<T>) -> T::Output @@ -213,11 +211,9 @@ impl ParamsMapExtra for ParamsMap { where T: QueryType, { - let value = value - .map(move |v| { - query.0.with_value(|query| v != query.default).then_some(v) - }) - .flatten(); + let value = value.and_then(move |v| { + query.0.with_value(|query| v != query.default).then_some(v) + }); match value { Some(value) => { self.insert(query.key().to_owned(), T::convert_to_string(value)) diff --git a/frontend/src/utils/session.rs b/frontend/src/utils/session.rs index 56c0b02c..c3121dde 100644 --- a/frontend/src/utils/session.rs +++ b/frontend/src/utils/session.rs @@ -33,5 +33,5 @@ pub fn use_token() -> Signal<Option<String>> { pub fn use_role() -> Signal<Option<Role>> { let (user_info, _) = use_token_info(); - (move || user_info().as_ref().map(|s| s.role.clone())).into_signal() + (move || user_info().as_ref().map(|s| s.role)).into_signal() } From 025066f1e1a888ea84bf0e43fe7f95ea6e7609ab Mon Sep 17 00:00:00 2001 From: kaiyohugo <41114603+KAIYOHUGO@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:40:25 +0800 Subject: [PATCH 17/18] fix(Frontend): :rotating_light: make rust doc don't run test on text block mark code block as text block --- frontend/src/components/paginate_navbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/paginate_navbar.rs b/frontend/src/components/paginate_navbar.rs index ce9c2248..f822403e 100644 --- a/frontend/src/components/paginate_navbar.rs +++ b/frontend/src/components/paginate_navbar.rs @@ -6,7 +6,7 @@ use crate::utils::*; #[component] /// There are 4 different case -/// ``` +/// ```text /// let right_half = size / 2 /// let left_half = size - right_half /// From ae4015f4f921c7ff79347f68178d9430860dacdc Mon Sep 17 00:00:00 2001 From: Eason <30045503+Eason0729@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:12:00 +0800 Subject: [PATCH 18/18] Cannot add problem to another contest if problem has been in contest that deleted (#68) * Fix bug children's foreign key not deleted * chore remove idea warning * Fix typo * update entity * update entity to use on delete for foreign key --- backend/justfile | 3 +- backend/migration/src/lib.rs | 6 +- .../src/m20231207_000001_create_table.rs | 53 +++++++----- .../src/m20240821_000001_create_tag.rs | 81 +++++++++++++++++++ backend/src/endpoint/problem.rs | 13 +-- backend/src/entity/announcement.rs | 10 +-- backend/src/entity/chat.rs | 8 +- backend/src/entity/contest.rs | 16 ++-- backend/src/entity/education.rs | 6 +- backend/src/entity/mod.rs | 2 + backend/src/entity/problem.rs | 11 +++ backend/src/entity/submit.rs | 10 +-- backend/src/entity/tag_problem.rs | 46 +++++++++++ backend/src/entity/testcase.rs | 10 +-- backend/src/entity/token.rs | 6 +- backend/src/entity/user.rs | 26 +++--- backend/src/entity/user_contest.rs | 5 +- backend/src/entity/util/helper.rs | 2 +- 18 files changed, 233 insertions(+), 81 deletions(-) create mode 100644 backend/migration/src/m20240821_000001_create_tag.rs create mode 100644 backend/src/entity/tag_problem.rs diff --git a/backend/justfile b/backend/justfile index 96047b08..aa93de1d 100644 --- a/backend/justfile +++ b/backend/justfile @@ -4,7 +4,8 @@ prepare: # Apply the migration and generate the entity code for manual inspection entity-codegen: - rm database/* + rm -r database + mkdir -p database sea-orm-cli migrate -u sqlite://database/backend.sqlite?mode=rwc sea-orm-cli generate entity -u sqlite://database/backend.sqlite?mode=rwc -o src/pending diff --git a/backend/migration/src/lib.rs b/backend/migration/src/lib.rs index a047dbf6..1b2e95c9 100644 --- a/backend/migration/src/lib.rs +++ b/backend/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20231207_000001_create_table; +mod m20240821_000001_create_tag; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec<Box<dyn MigrationTrait>> { - vec![Box::new(m20231207_000001_create_table::Migration)] + vec![ + Box::new(m20231207_000001_create_table::Migration), + Box::new(m20240821_000001_create_tag::Migration), + ] } } diff --git a/backend/migration/src/m20231207_000001_create_table.rs b/backend/migration/src/m20231207_000001_create_table.rs index bd95c525..d67aa051 100644 --- a/backend/migration/src/m20231207_000001_create_table.rs +++ b/backend/migration/src/m20231207_000001_create_table.rs @@ -67,7 +67,7 @@ enum Education { Content, } #[derive(Iden)] -enum Problem { +pub enum Problem { Table, Id, UserId, @@ -178,6 +178,7 @@ impl MigrationTrait for Migration { .col( ColumnDef::new(Announcement::Public) .boolean() + .not_null() .default(false), ) .col( @@ -191,14 +192,16 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-announcement-contest") .from(Announcement::Table, Announcement::ContestId) - .to(Contest::Table, Contest::Id), + .to(Contest::Table, Contest::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col(ColumnDef::new(Announcement::UserId).integer().not_null()) .foreign_key( ForeignKey::create() .name("fk-announcement-user") .from(Announcement::Table, Announcement::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::SetNull), ) .col( ColumnDef::new(Announcement::CreateAt) @@ -232,7 +235,8 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-announcement-user-hoster") .from(Contest::Table, Contest::Hoster) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::SetNull), ) .col(ColumnDef::new(Contest::Begin).date_time().not_null()) .col(ColumnDef::new(Contest::End).date_time().not_null()) @@ -283,14 +287,16 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-education-problem") .from(Education::Table, Education::ProblemId) - .to(Problem::Table, Problem::Id), + .to(Problem::Table, Problem::Id) + .on_delete(ForeignKeyAction::SetNull), ) .col(ColumnDef::new(Education::UserId).integer().not_null()) .foreign_key( ForeignKey::create() .name("fk-education-user") .from(Education::Table, Education::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::SetNull), ) .col( ColumnDef::new(Education::Tags) @@ -325,14 +331,16 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-problem-user") .from(Problem::Table, Problem::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::SetNull), ) - .col(ColumnDef::new(Problem::ContestId).integer()) + .col(ColumnDef::new(Problem::ContestId).integer().not_null()) .foreign_key( ForeignKey::create() .name("fk-problem-contest") .from(Problem::Table, Problem::ContestId) - .to(Contest::Table, Contest::Id), + .to(Contest::Table, Contest::Id) + .on_delete(ForeignKeyAction::SetNull), ) .col( ColumnDef::new(Problem::AcceptCount) @@ -408,14 +416,16 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-submit-user") .from(Submit::Table, Submit::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col(ColumnDef::new(Submit::ProblemId).integer().not_null()) .foreign_key( ForeignKey::create() .name("fk-submit-problem") .from(Submit::Table, Submit::ProblemId) - .to(Problem::Table, Problem::Id), + .to(Problem::Table, Problem::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col( ColumnDef::new(Submit::UploadAt) @@ -472,14 +482,16 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-test-user") .from(Testcase::Table, Testcase::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col(ColumnDef::new(Testcase::ProblemId).integer().null()) .foreign_key( ForeignKey::create() .name("fk-test-user") .from(Testcase::Table, Testcase::ProblemId) - .to(Problem::Table, Problem::Id), + .to(Problem::Table, Problem::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col(ColumnDef::new(Testcase::Input).binary().not_null()) .col(ColumnDef::new(Testcase::Output).binary().not_null()) @@ -515,7 +527,8 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-token-user") .from(Token::Table, Token::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col(ColumnDef::new(Token::Rand).binary().not_null()) .col( @@ -585,14 +598,16 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-pivot-contest-user") .from(UserContest::Table, UserContest::ContestId) - .to(Contest::Table, Contest::Id), + .to(Contest::Table, Contest::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col(ColumnDef::new(UserContest::UserId).integer().not_null()) .foreign_key( ForeignKey::create() .name("fk-pivot-user-contest") .from(UserContest::Table, UserContest::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col( ColumnDef::new(UserContest::Score) @@ -621,14 +636,16 @@ impl MigrationTrait for Migration { ForeignKey::create() .name("fk-chat-user") .from(Chat::Table, Chat::UserId) - .to(User::Table, User::Id), + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col(ColumnDef::new(Chat::ProblemId).integer().not_null()) .foreign_key( ForeignKey::create() .name("fk-chat-problem") .from(Chat::Table, Chat::ProblemId) - .to(Problem::Table, Problem::Id), + .to(Problem::Table, Problem::Id) + .on_delete(ForeignKeyAction::Cascade), ) .col( ColumnDef::new(Chat::CreateAt) diff --git a/backend/migration/src/m20240821_000001_create_tag.rs b/backend/migration/src/m20240821_000001_create_tag.rs new file mode 100644 index 00000000..e5615a60 --- /dev/null +++ b/backend/migration/src/m20240821_000001_create_tag.rs @@ -0,0 +1,81 @@ +use crate::m20231207_000001_create_table::Problem; +use sea_orm::{DatabaseBackend, Statement}; +use sea_orm_migration::prelude::*; + +#[derive(Iden)] +enum Tag { + Table, + Id, + Name, +} + +#[derive(Iden)] +enum TagProblem { + Table, + Id, + ProblemId, + TagId, +} + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Tag::Table) + .if_not_exists() + .col( + ColumnDef::new(Tag::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Tag::Name).string().unique_key().not_null()) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name("idx-tag-name".to_lowercase()) + .table(Tag::Table) + .col(Tag::Name) + .to_owned(), + ) + .await?; + manager + .create_table( + Table::create() + .table(TagProblem::Table) + .if_not_exists() + .col( + ColumnDef::new(TagProblem::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(TagProblem::ProblemId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-pivot-problem-tag") + .from(TagProblem::Table, TagProblem::ProblemId) + .to(Problem::Table, Problem::Id), + ) + .col(ColumnDef::new(TagProblem::TagId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-pivot-tag-problem") + .from(TagProblem::Table, TagProblem::TagId) + .to(Tag::Table, Tag::Id), + ) + .to_owned(), + ) + .await + } +} diff --git a/backend/src/endpoint/problem.rs b/backend/src/endpoint/problem.rs index 56f15f88..4d08581e 100644 --- a/backend/src/endpoint/problem.rs +++ b/backend/src/endpoint/problem.rs @@ -192,24 +192,13 @@ impl Problem for ArcServer { async fn remove(&self, req: Request<RemoveRequest>) -> Result<Response<()>, Status> { let (auth, req) = self.rate_limit(req).in_current_span().await?; req.get_or_insert(|req| async move { - let txn = self.db.begin().await?; - let result = Entity::delete_by_id(req.id) .with_auth(&auth) .write()? - .exec(&txn) + .exec(self.db.deref()) .instrument(info_span!("remove").or_current()) .await?; - testcase::Entity::update_many() - .col_expr(testcase::Column::ProblemId, Expr::value(Value::Int(None))) - .filter(testcase::Column::ProblemId.eq(req.id)) - .exec(&txn) - .instrument(info_span!("remove_child")) - .await?; - - txn.commit().await.map_err(|_| Error::Retry)?; - if result.rows_affected == 0 { return Err(Error::NotInDB); } diff --git a/backend/src/entity/announcement.rs b/backend/src/entity/announcement.rs index 1bc9f1e0..950b5e4d 100644 --- a/backend/src/entity/announcement.rs +++ b/backend/src/entity/announcement.rs @@ -52,13 +52,13 @@ pub enum Relation { User, } -impl Related<super::contest::Entity> for Entity { +impl Related<contest::Entity> for Entity { fn to() -> RelationDef { Relation::Contest.def() } } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } @@ -66,7 +66,7 @@ impl Related<super::user::Entity> for Entity { impl ActiveModelBehavior for ActiveModel {} -impl super::Filter for Entity { +impl Filter for Entity { fn read_filter<S: QueryFilter + Send>(query: S, auth: &Auth) -> Result<S, Error> { Ok(match auth.perm() { RoleLv::Guest => query.filter(Column::Public.eq(true)), @@ -182,7 +182,7 @@ impl SortSource<PartialModel> for ParentPagerTrait { fn sort_col(_data: &Self::Data) -> impl ColumnTrait { Column::UpdateAt } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { data.1 } fn save_val(data: &mut Self::Data, model: &PartialModel) { @@ -222,7 +222,7 @@ impl SortSource<PartialModel> for ColPagerTrait { Sort::Public => Column::Public, } } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { &data.1 } fn save_val(data: &mut Self::Data, model: &PartialModel) { diff --git a/backend/src/entity/chat.rs b/backend/src/entity/chat.rs index 0dd1494f..e561c7af 100644 --- a/backend/src/entity/chat.rs +++ b/backend/src/entity/chat.rs @@ -35,13 +35,13 @@ pub enum Relation { User, } -impl Related<super::problem::Entity> for Entity { +impl Related<problem::Entity> for Entity { fn to() -> RelationDef { Relation::Problem.def() } } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } @@ -49,7 +49,7 @@ impl Related<super::user::Entity> for Entity { impl ActiveModelBehavior for ActiveModel {} -impl super::Filter for Entity { +impl Filter for Entity { #[instrument(skip_all, level = "debug")] fn read_filter<S: QueryFilter + Send>(query: S, _: &Auth) -> Result<S, Error> { Ok(query) @@ -105,7 +105,7 @@ impl SortSource<Model> for ParentPagerTrait { fn sort_col(_data: &Self::Data) -> impl ColumnTrait { Column::CreateAt } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { data.1 } fn save_val(data: &mut Self::Data, model: &Model) { diff --git a/backend/src/entity/contest.rs b/backend/src/entity/contest.rs index 20862199..4a53a0cc 100644 --- a/backend/src/entity/contest.rs +++ b/backend/src/entity/contest.rs @@ -92,37 +92,37 @@ pub enum Relation { Hoster, } -impl Related<super::announcement::Entity> for Entity { +impl Related<announcement::Entity> for Entity { fn to() -> RelationDef { Relation::Announcement.def() } } -impl Related<super::user_contest::Entity> for Entity { +impl Related<user_contest::Entity> for Entity { fn to() -> RelationDef { Relation::UserContest.def() } } -impl Related<super::problem::Entity> for Entity { +impl Related<problem::Entity> for Entity { fn to() -> RelationDef { Relation::Problem.def() } } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { - super::user_contest::Relation::User.def() + user_contest::Relation::User.def() } fn via() -> Option<RelationDef> { - Some(super::user_contest::Relation::Contest.def().rev()) + Some(user_contest::Relation::Contest.def().rev()) } } impl ActiveModelBehavior for ActiveModel {} #[tonic::async_trait] -impl super::ParentalTrait<IdModel> for Entity { +impl ParentalTrait<IdModel> for Entity { #[instrument(skip_all, level = "info")] async fn related_read_by_id( auth: &Auth, @@ -168,7 +168,7 @@ impl super::ParentalTrait<IdModel> for Entity { } } -impl super::Filter for Entity { +impl Filter for Entity { fn read_filter<S: QueryFilter + Send>(query: S, auth: &Auth) -> Result<S, Error> { Ok(match auth.perm() { RoleLv::Guest => query.filter(Column::Public.eq(true)), diff --git a/backend/src/entity/education.rs b/backend/src/entity/education.rs index a04d66d8..3a63903c 100644 --- a/backend/src/entity/education.rs +++ b/backend/src/entity/education.rs @@ -43,13 +43,13 @@ pub enum Relation { User, } -impl Related<super::problem::Entity> for Entity { +impl Related<problem::Entity> for Entity { fn to() -> RelationDef { Relation::Problem.def() } } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } @@ -57,7 +57,7 @@ impl Related<super::user::Entity> for Entity { impl ActiveModelBehavior for ActiveModel {} -impl super::Filter for Entity { +impl Filter for Entity { fn read_filter<S: QueryFilter + Send>(query: S, auth: &Auth) -> Result<S, Error> { let (user_id, perm) = auth.assume_login()?; Ok(match perm { diff --git a/backend/src/entity/mod.rs b/backend/src/entity/mod.rs index 240baac7..85160543 100644 --- a/backend/src/entity/mod.rs +++ b/backend/src/entity/mod.rs @@ -8,6 +8,8 @@ pub mod contest; pub mod education; pub mod problem; pub mod submit; +pub mod tag; +pub mod tag_problem; pub mod testcase; pub mod token; pub mod user; diff --git a/backend/src/entity/problem.rs b/backend/src/entity/problem.rs index 627db673..c7f48509 100644 --- a/backend/src/entity/problem.rs +++ b/backend/src/entity/problem.rs @@ -97,6 +97,8 @@ pub enum Relation { Chat, #[sea_orm(has_many = "super::testcase::Entity")] Test, + #[sea_orm(has_many = "super::tag_problem::Entity")] + TagProblem, #[sea_orm( belongs_to = "super::user::Entity", from = "Column::UserId", @@ -149,6 +151,15 @@ impl Related<super::contest::Entity> for Entity { } } +impl Related<tag::Entity> for Entity { + fn to() -> RelationDef { + tag_problem::Relation::Tag.def() + } + fn via() -> Option<RelationDef> { + Some(tag_problem::Relation::Problem.def().rev()) + } +} + impl ActiveModelBehavior for ActiveModel {} #[async_trait] diff --git a/backend/src/entity/submit.rs b/backend/src/entity/submit.rs index ec5cd748..c82af6b5 100644 --- a/backend/src/entity/submit.rs +++ b/backend/src/entity/submit.rs @@ -72,13 +72,13 @@ pub enum Relation { User, } -impl Related<super::problem::Entity> for Entity { +impl Related<problem::Entity> for Entity { fn to() -> RelationDef { Relation::Problem.def() } } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } @@ -86,7 +86,7 @@ impl Related<super::user::Entity> for Entity { impl ActiveModelBehavior for ActiveModel {} -impl super::Filter for Entity { +impl Filter for Entity { #[instrument(skip_all, level = "debug")] fn read_filter<S: QueryFilter + Send>(query: S, _: &Auth) -> Result<S, Error> { Ok(query) @@ -144,7 +144,7 @@ impl SortSource<PartialModel> for ParentPagerTrait { fn sort_col(_data: &Self::Data) -> impl ColumnTrait { Column::UploadAt } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { data.1 } fn save_val(data: &mut Self::Data, model: &PartialModel) { @@ -180,7 +180,7 @@ impl SortSource<PartialModel> for ColPagerTrait { fn sort_col(_data: &Self::Data) -> impl ColumnTrait { Column::UploadAt } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { *data } fn save_val(data: &mut Self::Data, model: &PartialModel) { diff --git a/backend/src/entity/tag_problem.rs b/backend/src/entity/tag_problem.rs new file mode 100644 index 00000000..67f90ce5 --- /dev/null +++ b/backend/src/entity/tag_problem.rs @@ -0,0 +1,46 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "tag_problem")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub problem_id: i32, + pub tag_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::problem::Entity", + from = "Column::ProblemId", + to = "super::problem::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Problem, + #[sea_orm( + belongs_to = "super::tag::Entity", + from = "Column::TagId", + to = "super::tag::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Tag, +} + +impl Related<super::problem::Entity> for Entity { + fn to() -> RelationDef { + Relation::Problem.def() + } +} + +impl Related<super::tag::Entity> for Entity { + fn to() -> RelationDef { + Relation::Tag.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/src/entity/testcase.rs b/backend/src/entity/testcase.rs index 85c57f1a..52b1bf90 100644 --- a/backend/src/entity/testcase.rs +++ b/backend/src/entity/testcase.rs @@ -48,13 +48,13 @@ pub enum Relation { User, } -impl Related<super::problem::Entity> for Entity { +impl Related<problem::Entity> for Entity { fn to() -> RelationDef { Relation::Problem.def() } } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } @@ -62,7 +62,7 @@ impl Related<super::user::Entity> for Entity { impl ActiveModelBehavior for ActiveModel {} -impl super::Filter for Entity { +impl Filter for Entity { fn read_filter<S: QueryFilter + Send>(query: S, auth: &Auth) -> Result<S, Error> { let (user_id, perm) = auth.assume_login()?; Ok(match perm { @@ -127,7 +127,7 @@ impl SortSource<PartialModel> for ParentPagerTrait { fn sort_col(_data: &Self::Data) -> impl ColumnTrait { Column::Score } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { data.1 } fn save_val(data: &mut Self::Data, model: &PartialModel) { @@ -163,7 +163,7 @@ impl SortSource<PartialModel> for ColPagerTrait { fn sort_col(_data: &Self::Data) -> impl ColumnTrait { Column::Score } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { *data } fn save_val(data: &mut Self::Data, model: &PartialModel) { diff --git a/backend/src/entity/token.rs b/backend/src/entity/token.rs index 5ffed7f7..4b0048b6 100644 --- a/backend/src/entity/token.rs +++ b/backend/src/entity/token.rs @@ -29,7 +29,7 @@ pub enum Relation { User, } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } @@ -37,7 +37,7 @@ impl Related<super::user::Entity> for Entity { impl ActiveModelBehavior for ActiveModel {} -impl super::Filter for Entity { +impl Filter for Entity { fn read_filter<S: QueryFilter + Send>(query: S, auth: &Auth) -> Result<S, Error> { let (user_id, role) = auth.assume_login()?; Ok(match role { @@ -94,7 +94,7 @@ impl SortSource<Model> for ColPagerTrait { fn sort_col(data: &Self::Data) -> impl ColumnTrait { Column::Expiry } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { data.to_string() } fn save_val(data: &mut Self::Data, model: &Model) { diff --git a/backend/src/entity/user.rs b/backend/src/entity/user.rs index cf66fde3..ff8c7129 100644 --- a/backend/src/entity/user.rs +++ b/backend/src/entity/user.rs @@ -69,54 +69,54 @@ pub enum Relation { PublicProblem, } -impl Related<super::announcement::Entity> for Entity { +impl Related<announcement::Entity> for Entity { fn to() -> RelationDef { Relation::Announcement.def() } } -impl Related<super::education::Entity> for Entity { +impl Related<education::Entity> for Entity { fn to() -> RelationDef { Relation::Education.def() } } -impl Related<super::problem::Entity> for Entity { +impl Related<problem::Entity> for Entity { fn to() -> RelationDef { Relation::Problem.def() } } -impl Related<super::submit::Entity> for Entity { +impl Related<submit::Entity> for Entity { fn to() -> RelationDef { Relation::Submit.def() } } -impl Related<super::testcase::Entity> for Entity { +impl Related<testcase::Entity> for Entity { fn to() -> RelationDef { Relation::Test.def() } } -impl Related<super::token::Entity> for Entity { +impl Related<token::Entity> for Entity { fn to() -> RelationDef { Relation::Token.def() } } -impl Related<super::user_contest::Entity> for Entity { +impl Related<user_contest::Entity> for Entity { fn to() -> RelationDef { Relation::UserContest.def() } } -impl Related<super::contest::Entity> for Entity { +impl Related<contest::Entity> for Entity { fn to() -> RelationDef { - super::user_contest::Relation::Contest.def() + user_contest::Relation::Contest.def() } fn via() -> Option<RelationDef> { - Some(super::user_contest::Relation::User.def().rev()) + Some(user_contest::Relation::User.def().rev()) } } @@ -138,7 +138,7 @@ impl Linked for UserToProblem { } } -impl super::Filter for Entity { +impl Filter for Entity { #[instrument(skip_all, level = "debug")] fn read_filter<S: QueryFilter + Send>(query: S, _: &Auth) -> Result<S, Error> { Ok(query) @@ -225,7 +225,7 @@ impl SortSource<Model> for ColPagerTrait { Sort::CreateDate => Column::CreateAt, } } - fn get_val(data: &Self::Data) -> impl Into<sea_orm::Value> + Clone + Send { + fn get_val(data: &Self::Data) -> impl Into<Value> + Clone + Send { &data.1 } fn save_val(data: &mut Self::Data, model: &Model) { @@ -260,7 +260,7 @@ fn to_order(raw: bool) -> sea_query::Order { } #[async_trait] -impl util::paginator::PaginateRaw for ParentPaginator { +impl PaginateRaw for ParentPaginator { type Source = ParentSource; type Reflect = Model; diff --git a/backend/src/entity/user_contest.rs b/backend/src/entity/user_contest.rs index 3e6db5dc..3183411c 100644 --- a/backend/src/entity/user_contest.rs +++ b/backend/src/entity/user_contest.rs @@ -1,3 +1,4 @@ +use super::*; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -30,13 +31,13 @@ pub enum Relation { User, } -impl Related<super::contest::Entity> for Entity { +impl Related<contest::Entity> for Entity { fn to() -> RelationDef { Relation::Contest.def() } } -impl Related<super::user::Entity> for Entity { +impl Related<user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } diff --git a/backend/src/entity/util/helper.rs b/backend/src/entity/util/helper.rs index 5d69f9b5..4ea4d855 100644 --- a/backend/src/entity/util/helper.rs +++ b/backend/src/entity/util/helper.rs @@ -142,7 +142,7 @@ impl<PK: ColumnTrait, E: EntityTrait> Paginate<E> for PaginatePk<PK> { /// /// It's fast and cost `O(max(n,max))` to compute, but inaccurate if there is more than `max` /// -/// It actually build sql query like this +/// It actually builds sql queries like this /// /// ```sql /// SELECT CASE