diff --git a/Cargo.lock b/Cargo.lock index 29dedc5f5683f604006f374faa9b2c43eb35814b..40dcedbbe790b1c941a354848ada9b474eb2d8de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix-codec" @@ -679,6 +679,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -865,6 +866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1181,6 +1183,21 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags 2.6.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "h2" version = "0.3.26" @@ -1245,6 +1262,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1407,6 +1430,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1417,6 +1441,7 @@ checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1501,6 +1526,7 @@ dependencies = [ "diesel_migrations", "env_logger", "futures", + "git2", "handlebars", "itertools 0.11.0", "lazy_static", @@ -1514,11 +1540,14 @@ dependencies = [ "rust-argon2", "serde", "serde_json", + "serde_with", "thiserror", "tokio", + "toml", "tonic", "tonic-build", "uuid", + "walkdir", "which", "zip", ] @@ -1541,6 +1570,20 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + [[package]] name = "libsqlite3-sys" version = "0.26.0" @@ -1552,6 +1595,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1718,6 +1787,24 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.2.1" @@ -2289,6 +2376,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index 87c6b922ef716a6705ede895204823d3243a1372..4f312a65c2ce7f5b5e39da0d0815d860304ac5b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,10 @@ prost = "0.12.1" async-channel = "2.0.0" dashmap = "5.5.3" itertools = "0.11.0" +git2 = "0.19.0" +walkdir = "2.5.0" +toml = "0.8.19" +serde_with = "3.11.0" [build-dependencies] tonic-build = "0.10.2" diff --git a/Dockerfile b/Dockerfile index eb4184c2e25ca9592e5c368f2e1fba218d6d5ad6..4330a395b8e679b223b20de9808f161045f7f6ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,15 @@ RUN apt-get update && \ protobuf-compiler \ gcc \ g++ \ - libpq-dev && \ + libpq-dev \ + python3 \ + openjdk-17-jdk \ + pdf2svg \ + make \ + pypy3 \ + texlive-full \ + librsvg2-bin \ + pandoc \ apt-get clean && \ rm -rf /var/lib/apt/lists/* RUN rustup component add rustfmt diff --git a/alvokanto/Cargo.lock b/alvokanto/Cargo.lock index 3ae40f80a84e5ee2089e10a9996e394cdca9b921..00a7da2b3e9085cedd8b74fe19bf30956550ee17 100644 --- a/alvokanto/Cargo.lock +++ b/alvokanto/Cargo.lock @@ -399,6 +399,7 @@ dependencies = [ "tokio", "tonic", "tonic-build", + "walkdir", "which", ] @@ -698,6 +699,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -884,6 +886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1206,6 +1209,21 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags 2.6.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "h2" version = "0.3.26" @@ -1270,6 +1288,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1432,6 +1456,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1442,6 +1467,7 @@ checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1526,6 +1552,7 @@ dependencies = [ "diesel_migrations", "env_logger", "futures", + "git2", "handlebars", "itertools 0.11.0", "lazy_static", @@ -1539,11 +1566,14 @@ dependencies = [ "rust-argon2", "serde", "serde_json", + "serde_with", "thiserror", "tokio", + "toml", "tonic", "tonic-build", "uuid", + "walkdir", "which", "zip", ] @@ -1566,6 +1596,20 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + [[package]] name = "libsqlite3-sys" version = "0.26.0" @@ -1577,6 +1621,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1743,6 +1813,24 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.2.1" @@ -2314,6 +2402,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/alvokanto/Cargo.toml b/alvokanto/Cargo.toml index bdb29067a2025cc22dc134c76a6a555f9e4dd26d..c3aee9272ae9f3e60f498ed6eb5c188ca50dcf69 100644 --- a/alvokanto/Cargo.toml +++ b/alvokanto/Cargo.toml @@ -19,6 +19,7 @@ regex = "1.10.2" env_logger = "0.10.0" fs_extra = "1.3.0" chrono = "0.4.31" +walkdir = "2" [build-dependencies] tonic-build = "0.10.2" diff --git a/alvokanto/Dockerfile b/alvokanto/Dockerfile index 045a119f78b277b2ab0df42481965fbdd381ffa2..e522444cb399217738c78e48dd8af8d495e67c92 100644 --- a/alvokanto/Dockerfile +++ b/alvokanto/Dockerfile @@ -40,6 +40,7 @@ RUN apt-get update && \ fpc \ openjdk-17-jdk \ python3 \ + pypy3 \ libcap2 \ libpq5 && \ apt-get clean && \ diff --git a/alvokanto/dev.Dockerfile b/alvokanto/dev.Dockerfile index 59f246c35f6952ca68336dc8b3f4c89cba0ee524..7926f06948c3c61939eb5660caff024b1e29f958 100644 --- a/alvokanto/dev.Dockerfile +++ b/alvokanto/dev.Dockerfile @@ -24,6 +24,7 @@ RUN apt-get update && \ fpc \ openjdk-17-jdk \ python3 \ + pypy3 \ libcap2 \ libpq-dev && \ apt-get clean && \ diff --git a/alvokanto/src/isolate.rs b/alvokanto/src/isolate.rs index 7c92fc5cde159252a573129ecb9491878d465d72..4ed0282a48359d7c2b1839eefa39a3d748985d55 100644 --- a/alvokanto/src/isolate.rs +++ b/alvokanto/src/isolate.rs @@ -186,6 +186,7 @@ pub fn run( // java: /usr/bin/java and python: /usr/bin/python3 "--dir=usr/lib", "--dir=usr/bin", + "--dir=etc/java-17-openjdk", // java: libjli.so "--dir=proc=proc:fs", ] @@ -199,6 +200,7 @@ pub fn run( "--dir=usr/bin", "--dir=usr/include", "--dir=proc=proc:fs", + "--dir=etc/java-17-openjdk", ] }) .arg(format!("--processes={}", run_params.process_limit)) diff --git a/alvokanto/src/language.rs b/alvokanto/src/language.rs index 79e6414e7f3b236b6dcf92905c1a0e23c9dcd5f6..b025b940cf4faaca8c205598ff23c20569f3441a 100644 --- a/alvokanto/src/language.rs +++ b/alvokanto/src/language.rs @@ -97,14 +97,10 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> { build_gcc_params(2, "GNU G++20 {}", "/usr/bin/g++".into(), "c++", "c++20"), ); languages.insert( - "cpp.17.g++".into(), - build_gcc_params(2, "GNU G++17 {}", "/usr/bin/g++".into(), "c++", "c++17"), + "c.11.gcc".into(), + build_gcc_params(5, "GNU GCC C11 {}", "/usr/bin/gcc".into(), "c", "c11"), ); - languages.insert( - "c.18.gcc".into(), - build_gcc_params(5, "GNU GCC C18 {}", "/usr/bin/gcc".into(), "c", "c18"), - ); - languages.insert( + /*languages.insert( "pascal.fpc".into(), LanguageParams { order: 6, @@ -143,12 +139,12 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> { }, process_limit: 1, }, - ); + );*/ languages.insert( - "java.8".into(), + "java.17".into(), LanguageParams { order: 7, - name: "Java 8".into(), + name: "Java 17".into(), suffix: ".java".into(), compile: Compile::Command( &|source_text, source_name| { @@ -163,7 +159,7 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> { .into() }, CommandTuple { - binary_path: "/usr/lib/jvm/java-1.8-openjdk/bin/javac".into(), + binary_path: "/usr/lib/jvm/java-17-openjdk-amd64/bin/javac".into(), args: vec![ "-cp".into(), "\".;*\"".into(), @@ -176,7 +172,7 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> { "{}.class".into(), ), run: CommandTuple { - binary_path: "/usr/bin/java".into(), + binary_path: "/usr/lib/jvm/java-17-openjdk-amd64/bin/java".into(), args: vec![ "-Xmx512m".into(), "-Xss64m".into(), @@ -187,18 +183,18 @@ pub fn get_supported_languages() -> HashMap<String, LanguageParams> { "{}".into(), ], }, - process_limit: 19, + process_limit: 60, }, ); languages.insert( - "python.3".into(), + "pypy.3".into(), LanguageParams { order: 8, - name: "Python 3".into(), + name: "PyPy Python 3.9.16".into(), suffix: ".py".into(), compile: Compile::NoCompile, run: CommandTuple { - binary_path: "/usr/bin/python3".into(), + binary_path: "/usr/bin/pypy3".into(), args: vec!["{.}".into()], }, process_limit: 1, diff --git a/alvokanto/src/main.rs b/alvokanto/src/main.rs index 0d81835417d97cd793db7509d63eee06d173ddfa..5b2f8db85a4c12233800b6fbb20c06e040613c65 100644 --- a/alvokanto/src/main.rs +++ b/alvokanto/src/main.rs @@ -1,10 +1,11 @@ use chrono::Local; -use jughisto::import_contest::format_width; use jughisto::job_protocol::job_queue_client::JobQueueClient; use jughisto::job_protocol::{job, job_result, GetJobRequest, JobResult, Language}; use std::path::PathBuf; use which::which; +use walkdir::WalkDir; + use tonic::transport::channel::Channel; use tonic::Status; @@ -14,12 +15,10 @@ mod language; use isolate::{new_isolate_box, CommandTuple, CompileParams, IsolateBox, RunStats, RunStatus}; use language::Compile; use log::info; -use std::convert::TryInto; use std::fs; use std::fs::read_to_string; use std::fs::File; use std::io::Write; -use std::os::unix::fs::PermissionsExt; use tokio::time::{sleep, Duration}; pub fn get_isolate_executable_path() -> PathBuf { @@ -52,12 +51,13 @@ fn run_cached( let path_with_suffix = PathBuf::from("/data/").join(&request.source_path); let path_without_suffix = path_with_suffix.with_extension(""); + isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed"); + if let Compile::Command(_, command, output) = &language.compile { let output_path = output .replace("{.}", path_with_suffix.to_str().unwrap()) .replace("{}", path_without_suffix.to_str().unwrap()); if !root_data.join(&output_path).exists() { - isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed"); fs_extra::dir::copy( path_with_suffix.parent().unwrap(), &isolate_box.path, @@ -174,7 +174,6 @@ fn run_cached( .collect(), }; - isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed"); let run_stats = isolate::execute( isolate_executable_path, isolate_box, @@ -219,136 +218,6 @@ fn run_cached( } } -fn compile_checker_if_necessary( - isolate_executable_path: &PathBuf, - isolate_box: &IsolateBox, - supported_languages: &HashMap<String, language::LanguageParams>, - uuid: &String, - request: &job::Judgement, -) -> Option<JobResult> { - let root_data = PathBuf::from("/data/"); - - let checker_language = supported_languages.get(&request.checker_language); - if checker_language.is_none() { - return Some(JobResult { - uuid: uuid.to_string(), - code: job_result::Code::InvalidLanguage.into(), - which: None, - }); - } - let checker_language = checker_language.unwrap(); - - let path_with_suffix = PathBuf::from("/data/").join(&request.checker_source_path); - let path_without_suffix = path_with_suffix.with_extension(""); - if let Compile::Command(_, command, output) = &checker_language.compile { - let output_path = output - .replace("{.}", path_with_suffix.to_str().unwrap()) - .replace("{}", path_without_suffix.to_str().unwrap()); - if !root_data.join(&output_path).exists() { - isolate::reset(isolate_executable_path, isolate_box.id).expect("reset to work"); - fs_extra::dir::copy( - path_with_suffix.parent().unwrap(), - &isolate_box.path, - &fs_extra::dir::CopyOptions { - overwrite: false, - skip_exist: false, - buffer_size: 64000, //64kb - copy_inside: true, - content_only: true, - depth: 0, - }, - ) - .unwrap(); - - let command = CommandTuple { - binary_path: command.binary_path.clone(), - args: command - .args - .iter() - .map(|c| { - c.replace( - "{.}", - path_with_suffix.file_name().unwrap().to_str().unwrap(), - ) - .replace( - "{}", - path_without_suffix.file_name().unwrap().to_str().unwrap(), - ) - }) - .collect(), - }; - - info!("Compiling: {:#?}", command); - - let compile_stats = isolate::compile( - isolate_executable_path, - isolate_box, - CompileParams { - uuid, - // 1GiB - memory_limit_kib: 1_024 * 1_024, - // 25 seconds - time_limit_ms: 25_000, - command: &command, - }, - ) - .unwrap(); - - if match compile_stats { - RunStats { - exit_code: Some(c), .. - } => c != 0, - RunStats { - exit_code: None, .. - } => true, - } { - fs_extra::dir::create(&isolate_box.path, true).unwrap(); - return Some(JobResult { - uuid: uuid.to_string(), - code: job_result::Code::Ok.into(), - which: Some(job_result::Which::Judgement(job_result::Judgement { - failed_test: 0, - verdict: job_result::judgement::Verdict::CompilationError.into(), - exit_code: compile_stats.exit_code.unwrap_or(42), - exit_signal: compile_stats.exit_signal, - memory_kib: compile_stats.memory_kib.unwrap(), - time_ms: compile_stats.time_ms.unwrap(), - time_wall_ms: compile_stats.time_wall_ms.unwrap(), - error_output: read_to_string(compile_stats.stderr_path) - .unwrap_or("".into()), - judge_start_instant: Local::now() - .naive_utc() - .format("%Y-%m-%dT%H:%M:%S%.f") - .to_string(), - judge_end_instant: Local::now() - .naive_utc() - .format("%Y-%m-%dT%H:%M:%S%.f") - .to_string(), - })), - }); - } - - fs::copy( - isolate_box.path.join( - output - .replace( - "{.}", - path_with_suffix.file_name().unwrap().to_str().unwrap(), - ) - .replace( - "{}", - path_without_suffix.file_name().unwrap().to_str().unwrap(), - ), - ), - &output_path, - ) - .unwrap(); - } - } - - None -} - fn judge( isolate_executable_path: &PathBuf, isolate_box: &IsolateBox, @@ -369,23 +238,11 @@ fn judge( } let language = language.unwrap(); - if let Some(res) = compile_checker_if_necessary( - isolate_executable_path, - isolate_box, - supported_languages, - &uuid, - &request, - ) { - return res; - } - let judge_start_instant = Local::now().naive_utc(); - let mut binary_bytes = request.source_text.as_bytes().to_vec(); + isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed"); if let Compile::Command(transform, command, _) = &language.compile { - isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed"); - { let mut file = File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap(); @@ -449,19 +306,10 @@ fn judge( })), }; } - - binary_bytes = fs::read( - isolate_box.path.join( - language - .run - .binary_path - .to_str() - .unwrap() - .replace("{.}", &format!("x{}", language.suffix)) - .replace("{}", "x"), - ), - ) - .unwrap(); + } else { + let mut file = + File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap(); + file.write_all(request.source_text.as_bytes()).unwrap(); } let mut last_execute_stats: Option<RunStats> = None; @@ -487,33 +335,33 @@ fn judge( }; let mut error_output: Option<String> = None; - let mut failed_test: i32 = 0; + let mut failed_test: u32 = 0; let mut memory_kib = 0; let mut time_ms = 0; let mut time_wall_ms = 0; - for i in 1..request.test_count + 1 { - isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed"); - let stdin_path = format_width(&request.test_pattern, i.try_into().unwrap()); - let answer_path = format!("{}.a", stdin_path); + let mut testcases = WalkDir::new(format!("/data/{}/testcases/", request.package_id)) + .into_iter() + .filter_map(|e| { + e.ok().filter(|e| { + e.file_name() + .to_str() + .map(|s| s.ends_with(".in")) + .unwrap_or(false) + }) + }) + .collect::<Vec<_>>(); + testcases.sort_by(|a, b| a.file_name().cmp(b.file_name())); + let testcase_count = testcases.len(); + for (i, test) in testcases.into_iter().enumerate() { + let stdin_path = test.path().strip_prefix("/data/").unwrap(); + let answer_path = stdin_path.with_extension("sol"); info!( "Starting run {}/{} with test {:?}", - i, request.test_count, stdin_path + i + 1, + testcase_count, + stdin_path ); - if let Compile::Command(_, _, _) = &language.compile { - let mut file = File::create(isolate_box.path.join(&command.binary_path)).unwrap(); - let mut perms = file.metadata().unwrap().permissions(); - perms.set_mode(0o755); - file.set_permissions(perms).unwrap(); - file.write_all(&binary_bytes).unwrap(); - } else { - let mut file = - File::create(isolate_box.path.join(format!("x{}", language.suffix))).unwrap(); - let mut perms = file.metadata().unwrap().permissions(); - perms.set_mode(0o555); - file.set_permissions(perms).unwrap(); - file.write_all(&binary_bytes).unwrap(); - } let execute_stats = isolate::execute( isolate_executable_path, isolate_box, @@ -523,7 +371,7 @@ fn judge( uuid: &uuid, memory_limit_kib, time_limit_ms, - stdin_path: Some(stdin_path.clone()), + stdin_path: Some(stdin_path.to_str().unwrap().into()), }, ) .expect("Crashed while running"); @@ -542,23 +390,22 @@ fn judge( } => true, } { error_output = Some(read_to_string(&execute_stats.stderr_path).unwrap_or("".into())); - failed_test = i; + failed_test = (i + 1) as u32; last_execute_stats = Some(execute_stats); break; } let output_bytes = fs::read(&execute_stats.stdout_path).unwrap(); - isolate::reset(isolate_executable_path, isolate_box.id).expect("Reset failed"); { let mut file = File::create(isolate_box.path.join("stdin")).unwrap(); file.write_all(&output_bytes).unwrap(); } last_execute_stats = Some(execute_stats); - // TODO: Support non-compile based languages let command = CommandTuple { binary_path: PathBuf::from(format!("/data-{}/", &uuid)) - .join(&request.checker_source_path) + .join(&request.package_id) + .join("evaluator") .with_extension(""), args: vec![ PathBuf::from(format!("/data-{}/", &uuid)) @@ -601,7 +448,7 @@ fn judge( } => true, } { error_output = Some(read_to_string(checker_stats.stderr_path).unwrap_or("".into())); - failed_test = i; + failed_test = (i + 1) as u32; break; } } diff --git a/dev.Dockerfile b/dev.Dockerfile index c7f8ea3e5bd8aa91a39f771bdedb19ab30acf7af..baaa03b8a82cfb922ec8151ff62dc9aec775f12e 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -5,7 +5,17 @@ RUN apt-get update && \ protobuf-compiler \ gcc \ g++ \ - libpq-dev && \ + libpq-dev \ + libssl-dev \ + pkg-config \ + python3 \ + openjdk-17-jdk \ + pdf2svg \ + make \ + pypy3 \ + texlive-full \ + librsvg2-bin \ + pandoc && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* RUN rustup component add rustfmt @@ -14,4 +24,4 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ cargo install systemfd && \ cargo install cargo-watch WORKDIR /usr/src/jughisto -CMD systemfd --no-pid -s http::0.0.0.0:8000 -- cargo watch -x 'run --color always' -i alvokanto/ +CMD systemfd --no-pid -s http::0.0.0.0:8000 -- cargo watch -w src -x 'run --color always' -i alvokanto/ diff --git a/docker-compose.yml b/docker-compose.yml index e2a095519dabfc6a97853b33b439df08934d0f72..f8dd647b7c9aeba3a5dd40a2bec29e1dd72f8212 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,5 @@ services: build: context: . dockerfile: alvokanto/Dockerfile - volumes: - - type: bind - source: /sys/fs/cgroup/isolate.slice - target: /sys/fs/cgroup/isolate.slice privileged: true + cgroup: host diff --git a/migrations/2024-10-01-193839_create_repository/down.sql b/migrations/2024-10-01-193839_create_repository/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..c738d9360bfcda4cc9d9c26df7627db893e6781d --- /dev/null +++ b/migrations/2024-10-01-193839_create_repository/down.sql @@ -0,0 +1,4 @@ +alter table problem +drop column repository_id; + +drop table repository diff --git a/migrations/2024-10-01-193839_create_repository/up.sql b/migrations/2024-10-01-193839_create_repository/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..17bb8248ecea6307cb2e87b619c58dbb3128263e --- /dev/null +++ b/migrations/2024-10-01-193839_create_repository/up.sql @@ -0,0 +1,35 @@ +create table repository ( + id serial primary key not null, + name text not null, + path text not null, + remote_url text not null, + creation_user_id integer references "user"(id) not null, + creation_instant timestamp not null +); + +alter table problem +drop column if exists checker_path; + +alter table problem +drop column if exists checker_language; + +alter table problem +drop column if exists validator_path; + +alter table problem +drop column if exists validator_language; + +alter table problem +drop column if exists main_solution_path; + +alter table problem +drop column if exists main_solution_language; + +alter table problem +drop column if exists test_count; + +alter table problem +drop column if exists test_pattern; + +alter table problem +add column repository_id int null references repository(id) diff --git a/migrations/2024-12-09-010923_add_balloon_color_problem_name/down.sql b/migrations/2024-12-09-010923_add_balloon_color_problem_name/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..f60cd9315db02f406c08dd6a6bf36b536ce8e00c --- /dev/null +++ b/migrations/2024-12-09-010923_add_balloon_color_problem_name/down.sql @@ -0,0 +1,5 @@ +alter table contest_problems +drop column balloon_color; + +alter table contest_problems +drop column problem_name_override; diff --git a/migrations/2024-12-09-010923_add_balloon_color_problem_name/up.sql b/migrations/2024-12-09-010923_add_balloon_color_problem_name/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..9529196968dabc362c572cdbe93204466f8c85ee --- /dev/null +++ b/migrations/2024-12-09-010923_add_balloon_color_problem_name/up.sql @@ -0,0 +1,5 @@ +alter table contest_problems +add column balloon_color text null; + +alter table contest_problems +add column problem_name_override text null; diff --git a/migrations/2024-12-09-012754_add_user_full_name_single_contest/down.sql b/migrations/2024-12-09-012754_add_user_full_name_single_contest/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..1b80a25eb86a43f605220d504badecf0e09ae475 --- /dev/null +++ b/migrations/2024-12-09-012754_add_user_full_name_single_contest/down.sql @@ -0,0 +1,5 @@ +alter table "user" +drop column full_name; + +alter table "user" +drop column single_contest_id; diff --git a/migrations/2024-12-09-012754_add_user_full_name_single_contest/up.sql b/migrations/2024-12-09-012754_add_user_full_name_single_contest/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..fdd1ff50d09c1898e86133c36df5dda0b294e5ca --- /dev/null +++ b/migrations/2024-12-09-012754_add_user_full_name_single_contest/up.sql @@ -0,0 +1,5 @@ +alter table "user" +add column full_name text null; + +alter table "user" +add column single_contest_id int null references contest(id); diff --git a/migrations/2024-12-09-013119_add_contest_attributes/down.sql b/migrations/2024-12-09-013119_add_contest_attributes/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..50a1af7448604bcd2389dd72b1878acc07de1303 --- /dev/null +++ b/migrations/2024-12-09-013119_add_contest_attributes/down.sql @@ -0,0 +1,5 @@ +alter table contest +drop column frozen_scoreboard_instant; + +alter table contest +drop column blind_scoreboard_instant; diff --git a/migrations/2024-12-09-013119_add_contest_attributes/up.sql b/migrations/2024-12-09-013119_add_contest_attributes/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..facdb17779a8d1d243bc65e30817553cdd4786f5 --- /dev/null +++ b/migrations/2024-12-09-013119_add_contest_attributes/up.sql @@ -0,0 +1,5 @@ +alter table contest +add column frozen_scoreboard_instant timestamp null; + +alter table contest +add column blind_scoreboard_instant timestamp null; diff --git a/migrations/2024-12-09-013806_create_printing_task/down.sql b/migrations/2024-12-09-013806_create_printing_task/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..db7d56ab7f3510619b6c76f863aabd9a32530291 --- /dev/null +++ b/migrations/2024-12-09-013806_create_printing_task/down.sql @@ -0,0 +1 @@ +drop table printing_task; diff --git a/migrations/2024-12-09-013806_create_printing_task/up.sql b/migrations/2024-12-09-013806_create_printing_task/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..649a399f6c2b44888986eb57f7ab769fe3581b44 --- /dev/null +++ b/migrations/2024-12-09-013806_create_printing_task/up.sql @@ -0,0 +1,9 @@ +create table printing_task ( + uuid text primary key not null, + contents text not null, + creation_instant timestamp not null, + status text not null, + done_instant timestamp null, + contest_id integer references contest(id) not null, + user_id integer references "user"(id) not null +) diff --git a/migrations/2024-12-09-014353_create_clarification/down.sql b/migrations/2024-12-09-014353_create_clarification/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..e33dbf836e70135c0b139b54d501a38c1bddb4cd --- /dev/null +++ b/migrations/2024-12-09-014353_create_clarification/down.sql @@ -0,0 +1 @@ +drop table clarification diff --git a/migrations/2024-12-09-014353_create_clarification/up.sql b/migrations/2024-12-09-014353_create_clarification/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..ebd52c7b2d43cac5062c28cfc43ecba67a089594 --- /dev/null +++ b/migrations/2024-12-09-014353_create_clarification/up.sql @@ -0,0 +1,12 @@ +create table clarification ( + uuid text primary key not null, + question text not null, + question_instant timestamp not null, + answer text null, + answer_instant timestamp null, + answer_user_id integer references "user"(id) null, + answer_to_all boolean not null, + contest_problem_id integer references contest_problems(id) null, + contest_id integer references contest(id) not null, + user_id integer references "user"(id) not null +) diff --git a/migrations/2024-12-09-220325_create_backup/down.sql b/migrations/2024-12-09-220325_create_backup/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..34defdf0232284e7417a896095ccb7a85dae8630 --- /dev/null +++ b/migrations/2024-12-09-220325_create_backup/down.sql @@ -0,0 +1 @@ +drop table backup diff --git a/migrations/2024-12-09-220325_create_backup/up.sql b/migrations/2024-12-09-220325_create_backup/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..b35a0decf9e72ed98819079a9bb33f771dcbccce --- /dev/null +++ b/migrations/2024-12-09-220325_create_backup/up.sql @@ -0,0 +1,8 @@ +create table backup ( + uuid text primary key not null, + filename text not null, + contents bytea not null, + creation_instant timestamp not null, + contest_id integer references contest(id) not null, + user_id integer references "user"(id) not null +) diff --git a/migrations/2024-12-14-075202_submission_balloon_delivered_instant/down.sql b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/down.sql new file mode 100644 index 0000000000000000000000000000000000000000..3558767a244ea5c700d64e16c598e750e4d023e7 --- /dev/null +++ b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/down.sql @@ -0,0 +1,2 @@ +alter table submission +drop column balloon_delivered_instant diff --git a/migrations/2024-12-14-075202_submission_balloon_delivered_instant/up.sql b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..3fb71c1241b7d18b56d260ea96b86fe77c231d00 --- /dev/null +++ b/migrations/2024-12-14-075202_submission_balloon_delivered_instant/up.sql @@ -0,0 +1,2 @@ +alter table submission +add column balloon_delivered_instant timestamp null diff --git a/proto/job_queue.proto b/proto/job_queue.proto index c741b61c94b36cdb331b8f2c1da222420378d135..4701660348e45fd05709599c79406c63580b25bb 100644 --- a/proto/job_queue.proto +++ b/proto/job_queue.proto @@ -23,10 +23,7 @@ message Job { int32 time_limit_ms = 4; message Judgement { string source_text = 1; - int32 test_count = 2; - string test_pattern = 3; - string checker_language = 4; - string checker_source_path = 5; + string package_id = 2; }; message RunCached { string source_path = 1; @@ -58,7 +55,7 @@ message JobResult { RuntimeError = 5; }; Verdict verdict = 1; - int32 failed_test = 2; + uint32 failed_test = 2; int32 time_ms = 3; int32 time_wall_ms = 4; int32 memory_kib = 5; diff --git a/src/import_contest.rs b/src/import_contest.rs index 799b33b014f83263679f9f4a9f452a762357e117..b10cd53e507c97eae12836980ee2c308abc9d5b3 100644 --- a/src/import_contest.rs +++ b/src/import_contest.rs @@ -46,6 +46,7 @@ mod xml { pub use super::super::ImportContestError; } + #[allow(dead_code)] pub mod contest { use super::prelude::*; @@ -93,6 +94,7 @@ mod xml { pub mod problem { use super::prelude::*; + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Problem { #[serde(rename = "@url")] @@ -115,6 +117,7 @@ mod xml { pub name: Vec<Name>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Name { #[serde(rename = "@language")] @@ -123,11 +126,13 @@ mod xml { pub value: String, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Statements { pub statement: Vec<Statement>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Statement { #[serde(rename = "@charset")] @@ -142,6 +147,7 @@ mod xml { pub r#type: String, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Judging { #[serde(rename = "@cpu-name")] @@ -155,6 +161,7 @@ mod xml { pub testset: Vec<Testset>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Testset { #[serde(rename = "@name")] @@ -177,6 +184,7 @@ mod xml { pub test: Vec<Test>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Test { #[serde(rename = "@method")] @@ -189,28 +197,33 @@ mod xml { pub cmd: Option<String>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Files { pub resources: Resources, pub executables: Executables, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Resources { pub file: Vec<File>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct File { #[serde(rename = "@path")] pub path: String, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Executables { pub executable: Vec<Executable>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Executable { pub source: Source, @@ -225,6 +238,7 @@ mod xml { pub r#type: String, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Binary { #[serde(rename = "@path")] @@ -233,6 +247,7 @@ mod xml { pub r#type: String, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Assets { pub checker: Checker, @@ -240,6 +255,7 @@ mod xml { pub solutions: Solutions, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Checker { #[serde(rename = "@type")] @@ -250,6 +266,7 @@ mod xml { pub testset: CheckerTestset, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct CheckerTestset { #[serde(rename = "test-count")] @@ -261,17 +278,20 @@ mod xml { pub tests: VerdictTests, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Copy { #[serde(rename = "@path")] pub path: String, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Validators { pub validator: Vec<Validator>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Validator { pub source: Source, @@ -279,6 +299,7 @@ mod xml { pub testset: ValidatorTestset, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct ValidatorTestset { #[serde(rename = "test-count")] @@ -288,11 +309,13 @@ mod xml { pub tests: VerdictTests, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct VerdictTests { pub test: Option<Vec<VerdictTest>>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct VerdictTest { #[serde(rename = "@verdict")] @@ -304,6 +327,7 @@ mod xml { pub solution: Vec<Solution>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Solution { #[serde(rename = "@tag")] @@ -312,11 +336,13 @@ mod xml { pub binary: Binary, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Properties { pub property: Option<Vec<Property>>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Property { #[serde(rename = "@name")] @@ -325,6 +351,7 @@ mod xml { pub value: String, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Stresses { #[serde(rename = "stress-count")] @@ -337,11 +364,13 @@ mod xml { #[derive(Deserialize, Debug)] pub struct StressList {} + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Tags { pub tag: Option<Vec<Tag>>, } + #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Tag { #[serde(rename = "@value")] diff --git a/src/language.rs b/src/language.rs index 0c7d4726141bd0cc4c36213dc6d914c9e18bba2b..7c44e4874d6bbb4d1eaae47f7a3b04acb474f913 100644 --- a/src/language.rs +++ b/src/language.rs @@ -61,10 +61,7 @@ pub async fn judge( job_result_sender: &broadcast::Sender<JobResult>, language: &String, source_text: String, - test_count: i32, - test_pattern: String, - checker_language: String, - checker_source_path: String, + package_id: String, memory_limit_kib: i32, time_limit_ms: i32, ) -> Result<job_result::Judgement, Box<dyn std::error::Error>> { @@ -78,10 +75,7 @@ pub async fn judge( time_limit_ms, which: Some(job::Which::Judgement(job::Judgement { source_text, - test_count, - test_pattern, - checker_language, - checker_source_path, + package_id, })), }) .await?; diff --git a/src/main.rs b/src/main.rs index 4172ee18697cae07eb0ea86918340a6c579f1fbc..4bc897d7652ebf85ce842753c69c8ef661a5cc5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,7 @@ async fn update_database( error_output: Some(judgement.error_output), failed_test: match judgement.failed_test { 0 => None, - test => Some(test), + test => Some(test as i32), }, }, ) @@ -100,10 +100,7 @@ fn create_job_from_submission(submission: Submission, metadata: ProblemByContest which: Some(job::Which::Judgement(job::Judgement { source_text: submission.source_text, - test_count: metadata.test_count, - test_pattern: format!("./{}/{}", metadata.id, metadata.test_pattern), - checker_language: metadata.checker_language, - checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path), + package_id: metadata.id, })), } } @@ -203,15 +200,45 @@ async fn main() -> Result<(), Box<dyn Error>> { .service(pages::post_logout::post_logout) .service(pages::get_main::get_main) .service(pages::get_contests::get_contests) + .service(pages::get_repositories::get_repositories) .service(pages::get_contest_by_id::get_contest_by_id) + .service(pages::patch_contest::patch_contest) + .service(pages::patch_contest_problem::patch_contest_problem) + .service(pages::get_single_contest_waiting::get_single_contest_waiting) + .service(pages::get_single_contest_problems::get_single_contest_problems) + .service(pages::get_single_contest_runs::get_single_contest_runs) + .service(pages::get_single_contest_score::get_single_contest_score) + .service(pages::get_single_contest_clarifications::get_single_contest_clarifications) + .service(pages::get_printing_task_contents::get_printing_task_contents) + .service(pages::get_single_contest_tasks::get_single_contest_tasks) + .service(pages::create_printing_task::create_printing_task) + .service(pages::post_printing_task_done::post_printing_task_done) + .service(pages::get_single_contest_backup::get_single_contest_backup) + .service(pages::create_backup::create_backup) + .service(pages::get_backup_contents::get_backup_contents) + .service(pages::create_clarification::create_clarification) + .service(pages::patch_clarification::patch_clarification) + .service(pages::create_contest_submission::create_contest_submission) + .service(pages::post_submission_balloon_delivered::post_submission_balloon_delivered) .service(pages::get_contest_scoreboard_by_id::get_contest_scoreboard_by_id) + .service(pages::get_contest_clarifications_by_id::get_contest_clarifications_by_id) + .service(pages::get_contest_tasks_by_id::get_contest_tasks_by_id) + .service(pages::get_contest_webcast_by_id::get_contest_webcast_by_id) .service(pages::get_contest_problem_by_id_label::get_contest_problem_by_id_label) .service(pages::get_submissions_me::get_submissions_me) .service(pages::get_submission::get_submission) + .service(pages::get_submission_source::get_submission_source) .service(pages::get_submissions::get_submissions) + .service(pages::get_submissions_by_contest_id::get_submissions_by_contest_id) .service(pages::get_submissions_me_by_contest_id::get_submissions_me_by_contest_id) + .service(pages::get_submissions_by_contest_id_problem_label::get_submissions_by_contest_id_problem_label) .service(pages::get_submissions_me_by_contest_id_problem_label::get_submissions_me_by_contest_id_problem_label) .service(pages::rejudge_submission::rejudge_submission) + .service(pages::import_repository::import_repository) + .service(pages::sync_repository::sync_repository) + .service(pages::sync_repository_contest::sync_repository_contest) + .service(pages::get_repository_by_id::get_repository_by_id) + .service(pages::get_repository_problem_by_path::get_repository_problem_by_path) .service(pages::create_submission::create_submission) .service(pages::create_contest::create_contest) .service(pages::get_problems::get_problems) diff --git a/src/models.rs b/src/models.rs index 351f937a6275a364fb3e25195714ccd50d186912..c989a4e504c69c789b5a74ca8557100fa03314c9 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,8 @@ +pub mod backup; +pub mod clarification; pub mod contest; +pub mod printing_task; pub mod problem; +pub mod repository; pub mod submission; pub mod user; diff --git a/src/models/backup.rs b/src/models/backup.rs new file mode 100644 index 0000000000000000000000000000000000000000..69cd4e354d4bd0e82ef1fec31c557c5ff67c7a55 --- /dev/null +++ b/src/models/backup.rs @@ -0,0 +1,50 @@ +use chrono::prelude::*; +use diesel::insert_into; +use diesel::prelude::*; + +use crate::schema::backup; + +#[derive(Queryable)] +pub struct Backup { + pub uuid: String, + pub filename: String, + pub contents: Vec<u8>, + pub creation_instant: NaiveDateTime, + pub contest_id: i32, + pub user_id: i32, +} + +#[derive(Insertable)] +#[diesel(table_name = backup)] +pub struct NewBackup { + pub uuid: String, + pub filename: String, + pub contents: Vec<u8>, + pub creation_instant: NaiveDateTime, + pub contest_id: i32, + pub user_id: i32, +} + +pub fn insert_backup(connection: &mut PgConnection, new_backup: NewBackup) -> QueryResult<()> { + insert_into(backup::table) + .values(new_backup) + .execute(connection)?; + Ok(()) +} + +pub fn get_backups_by_contest_user( + connection: &mut PgConnection, + user_id: i32, + contest_id: i32, +) -> QueryResult<Vec<Backup>> { + backup::table + .filter(backup::user_id.eq(user_id)) + .filter(backup::contest_id.eq(contest_id)) + .load(connection) +} + +pub fn get_backup_by_uuid(connection: &mut PgConnection, uuid: String) -> QueryResult<Backup> { + backup::table + .filter(backup::uuid.eq(uuid)) + .first(connection) +} diff --git a/src/models/clarification.rs b/src/models/clarification.rs new file mode 100644 index 0000000000000000000000000000000000000000..f5e0f43405d3b6e6577c032a61c6203f21d330fb --- /dev/null +++ b/src/models/clarification.rs @@ -0,0 +1,130 @@ +use chrono::prelude::*; +use diesel::insert_into; +use diesel::prelude::*; +use diesel::update; + +use crate::schema::{clarification, contest_problems, user}; +use crate::user::{User, USER_COLUMNS}; + +#[derive(Queryable)] +pub struct Clarification { + pub uuid: String, + pub question: String, + pub question_instant: NaiveDateTime, + pub answer: Option<String>, + pub answer_instant: Option<NaiveDateTime>, + pub answer_user_id: Option<i32>, + pub answer_to_all: bool, + pub contest_problem_id: Option<i32>, + pub contest_id: i32, + pub user_id: i32, +} + +pub const CLARIFICATION_COLUMNS: ( + clarification::uuid, + clarification::question, + clarification::question_instant, + clarification::answer, + clarification::answer_instant, + clarification::answer_user_id, + clarification::answer_to_all, + clarification::contest_problem_id, + clarification::contest_id, + clarification::user_id, +) = ( + clarification::uuid, + clarification::question, + clarification::question_instant, + clarification::answer, + clarification::answer_instant, + clarification::answer_user_id, + clarification::answer_to_all, + clarification::contest_problem_id, + clarification::contest_id, + clarification::user_id, +); + +#[derive(Insertable)] +#[diesel(table_name = clarification)] +pub struct NewClarification { + pub uuid: String, + pub question: String, + pub question_instant: NaiveDateTime, + pub answer_to_all: bool, + pub contest_problem_id: Option<i32>, + pub contest_id: i32, + pub user_id: i32, +} + +pub fn insert_clarification( + connection: &mut PgConnection, + new_clarification: NewClarification, +) -> QueryResult<Clarification> { + let uuid = new_clarification.uuid.clone(); + insert_into(clarification::table) + .values(new_clarification) + .execute(connection)?; + clarification::table + .filter(clarification::uuid.eq(uuid)) + .first(connection) +} + +pub fn get_clarifications_by_contest_user( + connection: &mut PgConnection, + user_id: i32, + contest_id: i32, +) -> QueryResult<Vec<(Clarification, User, Option<String>)>> { + clarification::table + .inner_join(user::table) + .left_join(contest_problems::table) + .filter( + clarification::answer_to_all + .eq(true) + .or(clarification::user_id.eq(user_id)), + ) + .filter(clarification::contest_id.eq(contest_id)) + .select(( + CLARIFICATION_COLUMNS, + USER_COLUMNS, + contest_problems::label.nullable(), + )) + .order(clarification::question_instant.desc()) + .load(connection) +} + +pub fn get_clarifications_by_contest( + connection: &mut PgConnection, + contest_id: i32, +) -> QueryResult<Vec<(Clarification, User, Option<String>)>> { + clarification::table + .inner_join(user::table) + .left_join(contest_problems::table) + .filter(clarification::contest_id.eq(contest_id)) + .select(( + CLARIFICATION_COLUMNS, + USER_COLUMNS, + contest_problems::label.nullable(), + )) + .order(clarification::question_instant.desc()) + .load(connection) +} + +pub fn update_clarification( + connection: &mut PgConnection, + uuid: String, + new_answer: String, + new_answer_to_all: bool, + new_answer_user_id: i32, + new_answer_instant: NaiveDateTime, +) -> QueryResult<()> { + update(clarification::table) + .filter(clarification::uuid.eq(uuid)) + .set(( + clarification::answer.eq(new_answer), + clarification::answer_to_all.eq(new_answer_to_all), + clarification::answer_user_id.eq(new_answer_user_id), + clarification::answer_instant.eq(new_answer_instant), + )) + .execute(connection)?; + Ok(()) +} diff --git a/src/models/contest.rs b/src/models/contest.rs index 34187dcdd6802ed23234db93b4a22b5016e8f3b4..bd9554e1d443a49c03b82b5da53d2feeaadb6d03 100644 --- a/src/models/contest.rs +++ b/src/models/contest.rs @@ -1,6 +1,7 @@ use chrono::prelude::*; use diesel::insert_into; use diesel::prelude::*; +use diesel::update; use crate::schema::{contest, contest_problems}; @@ -14,6 +15,8 @@ pub struct Contest { pub creation_instant: NaiveDateTime, pub grade_ratio: Option<i32>, pub grade_after_ratio: Option<i32>, + pub frozen_scoreboard_instant: Option<NaiveDateTime>, + pub blind_scoreboard_instant: Option<NaiveDateTime>, } use diesel::sql_types; @@ -28,6 +31,10 @@ pub struct ContestWithAcs { pub start_instant: Option<NaiveDateTime>, #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)] pub end_instant: Option<NaiveDateTime>, + #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)] + pub frozen_scoreboard_instant: Option<NaiveDateTime>, + #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)] + pub blind_scoreboard_instant: Option<NaiveDateTime>, #[diesel(sql_type = sql_types::Integer)] pub creation_user_id: i32, #[diesel(sql_type = sql_types::Timestamp)] @@ -65,6 +72,8 @@ pub const CONTEST_COLUMNS: ( contest::creation_instant, contest::grade_ratio, contest::grade_after_ratio, + contest::frozen_scoreboard_instant, + contest::blind_scoreboard_instant, ) = ( contest::id, contest::name, @@ -74,6 +83,8 @@ pub const CONTEST_COLUMNS: ( contest::creation_instant, contest::grade_ratio, contest::grade_after_ratio, + contest::frozen_scoreboard_instant, + contest::blind_scoreboard_instant, ); pub fn insert_contest( @@ -109,6 +120,8 @@ pub fn get_contests_with_acs( contest.end_instant, contest.creation_user_id, contest.creation_instant, + contest.frozen_scoreboard_instant, + contest.blind_scoreboard_instant, contest.grade_ratio, contest.grade_after_ratio, coalesce(cast(count(first_ac_submission_instant) filter (where @@ -140,6 +153,12 @@ pub fn get_contest_by_id(connection: &mut PgConnection, id: i32) -> QueryResult< contest::table.filter(contest::id.eq(id)).first(connection) } +pub fn get_contest_by_name(connection: &mut PgConnection, name: &str) -> QueryResult<Contest> { + contest::table + .filter(contest::name.eq(name)) + .first(connection) +} + pub fn get_contest_by_contest_problem_id( connection: &mut PgConnection, contest_problem_id: i32, @@ -168,3 +187,74 @@ pub fn relate_problem( .execute(connection)?; Ok(()) } + +#[derive(Queryable, Debug)] +pub struct ContestProblem { + pub id: i32, + pub label: String, + pub contest_id: i32, + pub problem_id: String, +} + +pub fn get_contest_problems_by_contest( + connection: &mut PgConnection, + contest_id: i32, +) -> QueryResult<Vec<ContestProblem>> { + contest_problems::table + .filter(contest_problems::contest_id.eq(contest_id)) + .select(( + contest_problems::id, + contest_problems::label, + contest_problems::contest_id, + contest_problems::problem_id, + )) + .load(connection) +} + +pub fn update_contest_problem( + connection: &mut PgConnection, + contest_problem_id: i32, + new_problem_id: String, + new_label: String, +) -> QueryResult<()> { + update(contest_problems::table) + .filter(contest_problems::id.eq(contest_problem_id)) + .set(( + contest_problems::problem_id.eq(new_problem_id), + contest_problems::label.eq(new_label), + )) + .execute(connection)?; + Ok(()) +} + +pub fn update_contest_problem_balloon_color( + connection: &mut PgConnection, + contest_problem_id: i32, + new_balloon_color: Option<String>, +) -> QueryResult<()> { + update(contest_problems::table) + .filter(contest_problems::id.eq(contest_problem_id)) + .set((contest_problems::balloon_color.eq(new_balloon_color),)) + .execute(connection)?; + Ok(()) +} + +pub fn update_contest( + connection: &mut PgConnection, + contest_id: i32, + start_instant: Option<NaiveDateTime>, + end_instant: Option<NaiveDateTime>, + frozen_scoreboard_instant: Option<NaiveDateTime>, + blind_scoreboard_instant: Option<NaiveDateTime>, +) -> QueryResult<()> { + update(contest::table) + .filter(contest::id.eq(contest_id)) + .set(( + contest::start_instant.eq(start_instant), + contest::end_instant.eq(end_instant), + contest::frozen_scoreboard_instant.eq(frozen_scoreboard_instant), + contest::blind_scoreboard_instant.eq(blind_scoreboard_instant), + )) + .execute(connection)?; + Ok(()) +} diff --git a/src/models/printing_task.rs b/src/models/printing_task.rs new file mode 100644 index 0000000000000000000000000000000000000000..66efedc8f848b34660f996da7cc2f01a021c098a --- /dev/null +++ b/src/models/printing_task.rs @@ -0,0 +1,108 @@ +use chrono::prelude::*; +use diesel::insert_into; +use diesel::prelude::*; +use diesel::update; + +use crate::schema::{printing_task, user}; +use crate::user::{User, USER_COLUMNS}; + +#[derive(Queryable)] +pub struct PrintingTask { + pub uuid: String, + pub contents: String, + pub creation_instant: NaiveDateTime, + pub status: String, + pub done_instant: Option<NaiveDateTime>, + pub contest_id: i32, + pub user_id: i32, +} + +pub const PRINTING_TASK_COLUMNS: ( + printing_task::uuid, + printing_task::contents, + printing_task::creation_instant, + printing_task::status, + printing_task::done_instant, + printing_task::contest_id, + printing_task::user_id, +) = ( + printing_task::uuid, + printing_task::contents, + printing_task::creation_instant, + printing_task::status, + printing_task::done_instant, + printing_task::contest_id, + printing_task::user_id, +); + +#[derive(Insertable)] +#[diesel(table_name = printing_task)] +pub struct NewPrintingTask { + pub uuid: String, + pub contents: String, + pub creation_instant: NaiveDateTime, + pub status: String, + pub done_instant: Option<NaiveDateTime>, + pub contest_id: i32, + pub user_id: i32, +} + +pub fn insert_printing_task( + connection: &mut PgConnection, + new_printing_task: NewPrintingTask, +) -> QueryResult<PrintingTask> { + let uuid = new_printing_task.uuid.clone(); + insert_into(printing_task::table) + .values(new_printing_task) + .execute(connection)?; + printing_task::table + .filter(printing_task::uuid.eq(uuid)) + .first(connection) +} + +pub fn get_printing_tasks_by_contest_user( + connection: &mut PgConnection, + user_id: i32, + contest_id: i32, +) -> QueryResult<Vec<PrintingTask>> { + printing_task::table + .filter(printing_task::user_id.eq(user_id)) + .filter(printing_task::contest_id.eq(contest_id)) + .load(connection) +} + +pub fn get_printing_tasks_by_contest( + connection: &mut PgConnection, + contest_id: i32, +) -> QueryResult<Vec<(PrintingTask, User)>> { + printing_task::table + .inner_join(user::table) + .filter(printing_task::contest_id.eq(contest_id)) + .select((PRINTING_TASK_COLUMNS, USER_COLUMNS)) + .load(connection) +} + +pub fn get_printing_task_by_uuid( + connection: &mut PgConnection, + uuid: String, +) -> QueryResult<(PrintingTask, User)> { + printing_task::table + .inner_join(user::table) + .filter(printing_task::uuid.eq(uuid)) + .select((PRINTING_TASK_COLUMNS, USER_COLUMNS)) + .first(connection) +} + +pub fn update_printing_task( + connection: &mut PgConnection, + uuid: String, + new_done_instant: NaiveDateTime, +) -> QueryResult<()> { + update(printing_task::table) + .filter(printing_task::uuid.eq(uuid)) + .set(( + printing_task::done_instant.eq(new_done_instant), + )) + .execute(connection)?; + Ok(()) +} diff --git a/src/models/problem.rs b/src/models/problem.rs index 0d9f63def7e80a6a6727135a21334dc926961b53..a6c05afc0815ae708b251b65023b8bcfcc0fa3d9 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -12,17 +12,10 @@ pub struct Problem { pub name: String, pub memory_limit_bytes: i32, pub time_limit_ms: i32, - pub checker_path: String, - pub checker_language: String, - pub validator_path: String, - pub validator_language: String, - pub main_solution_path: String, - pub main_solution_language: String, - pub test_count: i32, - pub test_pattern: String, pub status: String, pub creation_user_id: i32, pub creation_instant: NaiveDateTime, + pub repository_id: Option<i32>, } #[derive(Insertable)] @@ -32,14 +25,6 @@ pub struct NewProblem { pub name: String, pub memory_limit_bytes: i32, pub time_limit_ms: i32, - pub checker_path: String, - pub checker_language: String, - pub validator_path: String, - pub validator_language: String, - pub main_solution_path: String, - pub main_solution_language: String, - pub test_count: i32, - pub test_pattern: String, pub status: String, pub creation_user_id: i32, pub creation_instant: NaiveDateTime, @@ -76,10 +61,16 @@ use diesel::sql_types; pub struct ProblemByContestWithScore { #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)] pub user_name: Option<String>, + #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)] + pub user_full_name: Option<String>, #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)] pub first_ac_submission_instant: Option<NaiveDateTime>, + #[diesel(sql_type = sql_types::Nullable<sql_types::Timestamp>)] + pub first_ac_problem_submission_instant: Option<NaiveDateTime>, #[diesel(sql_type = sql_types::Integer)] pub failed_submissions: i32, + #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)] + pub last_failed_submission_verdict: Option<String>, #[diesel(sql_type = sql_types::Integer)] pub id: i32, #[diesel(sql_type = sql_types::Text)] @@ -92,218 +83,335 @@ pub struct ProblemByContestWithScore { pub time_limit_ms: i32, #[diesel(sql_type = sql_types::Integer)] pub user_accepted_count: i32, + #[diesel(sql_type = sql_types::Nullable<sql_types::Text>)] + pub balloon_color: Option<String>, +} + +const WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT: &str = r#" +first_ac as ( + select + min(submission_instant) as first_ac_submission_instant, + contest_problem_id, + filtered_submission.user_id + from filtered_submission + where filtered_submission.verdict = 'AC' + group by filtered_submission.user_id, filtered_submission.contest_problem_id +), first_ac_by_problem as ( + select + min(submission_instant) as first_ac_problem_submission_instant, + contest_problem_id + from filtered_submission + where filtered_submission.verdict = 'AC' + group by filtered_submission.contest_problem_id +), failed_submission_count as ( + select + filtered_submission.user_id, + filtered_submission.contest_problem_id, + cast(count(*) as int) as count + from filtered_submission + left join first_ac on first_ac.contest_problem_id = filtered_submission.contest_problem_id + and filtered_submission.user_id = first_ac.user_id + where ( + first_ac_submission_instant is null or + filtered_submission.submission_instant < first_ac.first_ac_submission_instant + ) + group by filtered_submission.user_id, filtered_submission.contest_problem_id +), last_failed_submission_instant as ( + select + filtered_submission.user_id, + filtered_submission.contest_problem_id, + max(filtered_submission.submission_instant) as last_submission_instant + from filtered_submission + left join first_ac on first_ac.contest_problem_id = filtered_submission.contest_problem_id + and filtered_submission.user_id = first_ac.user_id + where ( + first_ac_submission_instant is null or + filtered_submission.submission_instant < first_ac.first_ac_submission_instant + ) + group by filtered_submission.user_id, filtered_submission.contest_problem_id +), last_failed_submission as ( + select + filtered_submission.user_id, + filtered_submission.contest_problem_id, + filtered_submission.verdict + from filtered_submission + inner join last_failed_submission_instant on last_failed_submission_instant.contest_problem_id = filtered_submission.contest_problem_id + and last_failed_submission_instant.user_id = filtered_submission.user_id + and last_failed_submission_instant.last_submission_instant = filtered_submission.submission_instant +), user_ac_count as ( + select + cast(count(distinct filtered_submission.user_id) as int) as user_accepted_count, + filtered_submission.contest_problem_id + from filtered_submission + where filtered_submission.verdict = 'AC' + group by filtered_submission.contest_problem_id +) +"#; + +const ALL_SUBMISSIONS: &str = r#" +filtered_submission as (select * from submission) +"#; + +const ALL_SUBMISSIONS_IGNORING_CE_VERDICTS: &str = r#" +filtered_submission as (select * from submission where submission.verdict <> 'CE') +"#; + +const BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS: &str = r#" +filtered_submission as ( + select + submission.uuid, + CASE WHEN submission.submission_instant < contest.blind_scoreboard_instant + THEN submission.verdict ELSE 'BL' END as verdict, + submission.submission_instant, + submission.contest_problem_id, + submission.user_id + from submission + inner join contest_problems on contest_problems.id = submission.contest_problem_id + inner join contest on contest.id = contest_problems.contest_id + where ((submission.user_id = $1) OR (submission.submission_instant < contest.frozen_scoreboard_instant)) + AND submission.verdict <> 'CE' +) +"#; + +const FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS: &str = r#" +filtered_submission as ( + select + submission.uuid, + submission.verdict as verdict, + submission.submission_instant, + submission.contest_problem_id, + submission.user_id + from submission + inner join contest_problems on contest_problems.id = submission.contest_problem_id + inner join contest on contest.id = contest_problems.contest_id + where submission.submission_instant < contest.frozen_scoreboard_instant + and submission.verdict <> 'CE' +) +"#; + +const SELECT_PROBLEMS_USER_WITH_SCORE: &str = r#" + select + "user".name as user_name, + "user".full_name as user_full_name, + first_ac_submission_instant, + first_ac_problem_submission_instant, + coalesce(failed_submission_count.count, 0) as failed_submissions, + last_failed_submission.verdict as last_failed_submission_verdict, + contest_problems.id, + problem.name, + contest_problems.label, + problem.memory_limit_bytes, + problem.time_limit_ms, + coalesce(user_ac_count.user_accepted_count, 0) as user_accepted_count, + contest_problems.balloon_color + from contest_problems + inner join problem on problem.id = contest_problems.problem_id + inner join "user" on "user".id = $1 + left join last_failed_submission + on last_failed_submission.contest_problem_id = contest_problems.id + and last_failed_submission.user_id = "user".id + left join failed_submission_count + on failed_submission_count.contest_problem_id = contest_problems.id + and failed_submission_count.user_id = "user".id + left join first_ac + on first_ac.contest_problem_id = contest_problems.id + and first_ac.user_id = "user".id + left join first_ac_by_problem + on first_ac_by_problem.contest_problem_id = contest_problems.id + left join user_ac_count + on user_ac_count.contest_problem_id = contest_problems.id +"#; + +// all verdicts for a given user +pub fn get_problems_user_with_score_without_blind_and_frozen( + connection: &mut PgConnection, + user_id: i32, +) -> QueryResult<Vec<ProblemByContestWithScore>> { + diesel::sql_query(format!( + r#" + with {}, {} + {} + order by problem.creation_instant + "#, + ALL_SUBMISSIONS, + WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT, + SELECT_PROBLEMS_USER_WITH_SCORE + )) + .bind::<sql_types::Integer, _>(user_id) + .load(connection) } -pub fn get_problems_user_with_score( +pub fn get_problems_user_with_score_with_blind_and_frozen( connection: &mut PgConnection, user_id: i32, ) -> QueryResult<Vec<ProblemByContestWithScore>> { - diesel::sql_query( + diesel::sql_query(format!( r#" - with first_ac as ( - select - min(submission_instant) as first_ac_submission_instant, - contest_problem_id, - submission.user_id - from submission - where submission.verdict = 'AC' - group by submission.user_id, submission.contest_problem_id - ), failed_submissions as ( - select - submission.user_id, - submission.contest_problem_id, - cast(count(*) as int) as count - from submission - left join first_ac on first_ac.contest_problem_id = submission.contest_problem_id - and submission.user_id = first_ac.user_id - where ( - first_ac_submission_instant is null or - submission.submission_instant < first_ac.first_ac_submission_instant - ) - group by submission.user_id, submission.contest_problem_id - ), user_acs_count as ( - select - cast(count(distinct submission.user_id) as int) as user_accepted_count, - submission.contest_problem_id - from submission - where submission.verdict = 'AC' - group by submission.contest_problem_id - ) - select - "user".name as user_name, - first_ac_submission_instant, - coalesce(failed_submissions.count, 0) as failed_submissions, - contest_problems.id, - problem.name, - contest_problems.label, - problem.memory_limit_bytes, - problem.time_limit_ms, - coalesce(user_acs_count.user_accepted_count, 0) as user_accepted_count - from contest_problems - inner join problem on problem.id = contest_problems.problem_id - inner join "user" on "user".id = $1 - left join failed_submissions - on failed_submissions.contest_problem_id = contest_problems.id - and failed_submissions.user_id = "user".id - left join first_ac - on first_ac.contest_problem_id = contest_problems.id - and first_ac.user_id = "user".id - left join user_acs_count - on user_acs_count.contest_problem_id = contest_problems.id + with {}, {} + {} + order by problem.creation_instant + "#, + BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS, + WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT, + SELECT_PROBLEMS_USER_WITH_SCORE + )) + .bind::<sql_types::Integer, _>(user_id) + .load(connection) +} + +pub fn get_problems_user_by_contest_id_with_score_without_blind_and_frozen( + connection: &mut PgConnection, + user_id: i32, + contest_id: i32, +) -> QueryResult<Vec<ProblemByContestWithScore>> { + diesel::sql_query(format!( + r#" + with {}, {} + {} + where contest_id = $2 order by contest_problems.label "#, - ) + ALL_SUBMISSIONS_IGNORING_CE_VERDICTS, + WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT, + SELECT_PROBLEMS_USER_WITH_SCORE + )) .bind::<sql_types::Integer, _>(user_id) + .bind::<sql_types::Integer, _>(contest_id) .load(connection) } -pub fn get_problems_user_by_contest_id_with_score( +pub fn get_problems_user_by_contest_id_with_score_with_blind_and_frozen( connection: &mut PgConnection, user_id: i32, contest_id: i32, ) -> QueryResult<Vec<ProblemByContestWithScore>> { - diesel::sql_query( + diesel::sql_query(format!( r#" - with first_ac as ( - select - min(submission_instant) as first_ac_submission_instant, - contest_problem_id, - submission.user_id - from submission - where submission.verdict = 'AC' - group by submission.user_id, submission.contest_problem_id - ), failed_submissions as ( - select - submission.user_id, - submission.contest_problem_id, - cast(count(*) as int) as count - from submission - left join first_ac on first_ac.contest_problem_id = submission.contest_problem_id - and submission.user_id = first_ac.user_id - where ( - first_ac_submission_instant is null or - submission.submission_instant < first_ac.first_ac_submission_instant - ) - group by submission.user_id, submission.contest_problem_id - ), user_acs_count as ( - select - cast(count(distinct submission.user_id) as int) as user_accepted_count, - submission.contest_problem_id - from submission - where submission.verdict = 'AC' - group by submission.contest_problem_id - ) - select - "user".name as user_name, - first_ac_submission_instant, - coalesce(failed_submissions.count, 0) as failed_submissions, - contest_problems.id, - problem.name, - contest_problems.label, - problem.memory_limit_bytes, - problem.time_limit_ms, - coalesce(user_acs_count.user_accepted_count, 0) as user_accepted_count - from contest_problems - inner join problem on problem.id = contest_problems.problem_id - inner join "user" on "user".id = $1 - left join failed_submissions - on failed_submissions.contest_problem_id = contest_problems.id - and failed_submissions.user_id = "user".id - left join first_ac - on first_ac.contest_problem_id = contest_problems.id - and first_ac.user_id = "user".id - left join user_acs_count - on user_acs_count.contest_problem_id = contest_problems.id + with {}, {} + {} where contest_id = $2 order by contest_problems.label "#, - ) + BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS, + WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT, + SELECT_PROBLEMS_USER_WITH_SCORE + )) .bind::<sql_types::Integer, _>(user_id) .bind::<sql_types::Integer, _>(contest_id) .load(connection) } -pub fn get_problems_by_contest_id_with_score( +const SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE: &str = r#" + (select + null as user_name, + null as user_full_name, + null as first_ac_submission_instant, + null as first_ac_problem_submission_instant, + 0 as failed_submissions, + null as last_failed_submission_verdict, + contest_problems.id, + problem.name, + contest_problems.label, + problem.memory_limit_bytes, + problem.time_limit_ms, + 0 as user_accepted_count, + contest_problems.balloon_color + from contest_problems + inner join problem on problem.id = contest_problems.problem_id + where contest_problems.contest_id = $2 + order by contest_problems.label) + union all + (select + "user".name as user_name, + "user".full_name as user_full_name, + first_ac_submission_instant, + first_ac_problem_submission_instant, + coalesce(failed_submission_count.count, 0) as failed_submissions, + last_failed_submission.verdict as last_failed_submission_verdict, + contest_problems.id, + problem.name, + contest_problems.label, + problem.memory_limit_bytes, + problem.time_limit_ms, + coalesce(user_ac_count.user_accepted_count, 0) as user_accepted_count, + contest_problems.balloon_color + from contest_problems + inner join problem on problem.id = contest_problems.problem_id + inner join ( + select user_id, contest_id + from filtered_submission + inner join contest_problems on contest_problems.id = filtered_submission.contest_problem_id + group by user_id, contest_id + ) as all_submitters on all_submitters.contest_id = $2 + inner join "user" on "user".id = all_submitters.user_id + left join failed_submission_count + on failed_submission_count.contest_problem_id = contest_problems.id + and failed_submission_count.user_id = "user".id + left join last_failed_submission + on last_failed_submission.contest_problem_id = contest_problems.id + and last_failed_submission.user_id = "user".id + left join first_ac + on first_ac.contest_problem_id = contest_problems.id + and first_ac.user_id = "user".id + left join first_ac_by_problem + on first_ac_by_problem.contest_problem_id = contest_problems.id + left join user_ac_count + on user_ac_count.contest_problem_id = contest_problems.id + where contest_problems.contest_id = $2 + order by "user".name, contest_problems.label) +"#; + +pub fn get_problems_by_contest_id_with_score_without_blind_and_frozen( connection: &mut PgConnection, contest_id: i32, ) -> QueryResult<Vec<ProblemByContestWithScore>> { - diesel::sql_query( + diesel::sql_query(format!( r#" - with first_ac as ( - select - min(submission_instant) as first_ac_submission_instant, - contest_problem_id, - submission.user_id - from submission - where submission.verdict = 'AC' - group by submission.user_id, submission.contest_problem_id - ), failed_submissions as ( - select - submission.user_id, - submission.contest_problem_id, - cast(count(*) as int) as count - from submission - left join first_ac on first_ac.contest_problem_id = submission.contest_problem_id - and submission.user_id = first_ac.user_id - where ( - first_ac_submission_instant is null or - submission.submission_instant < first_ac.first_ac_submission_instant - ) - group by submission.user_id, submission.contest_problem_id - ), all_submitters as ( - select user_id, contest_id - from submission - inner join contest_problems on contest_problems.id = submission.contest_problem_id - group by user_id, contest_id - ), user_acs_count as ( - select - cast(count(distinct submission.user_id) as int) as user_accepted_count, - submission.contest_problem_id - from submission - where submission.verdict = 'AC' - group by submission.contest_problem_id - ) - (select - null as user_name, - null as first_ac_submission_instant, - 0 as failed_submissions, - contest_problems.id, - problem.name, - contest_problems.label, - problem.memory_limit_bytes, - problem.time_limit_ms, - 0 as user_accepted_count - from contest_problems - inner join problem on problem.id = contest_problems.problem_id - where contest_problems.contest_id = $1 - order by contest_problems.label) - union all - (select - "user".name as user_name, - first_ac_submission_instant, - coalesce(failed_submissions.count, 0) as failed_submissions, - contest_problems.id, - problem.name, - contest_problems.label, - problem.memory_limit_bytes, - problem.time_limit_ms, - coalesce(user_acs_count.user_accepted_count, 0) as user_accepted_count - from contest_problems - inner join problem on problem.id = contest_problems.problem_id - inner join all_submitters on all_submitters.contest_id = $1 - inner join "user" on "user".id = all_submitters.user_id - left join failed_submissions - on failed_submissions.contest_problem_id = contest_problems.id - and failed_submissions.user_id = "user".id - left join first_ac - on first_ac.contest_problem_id = contest_problems.id - and first_ac.user_id = "user".id - left join user_acs_count - on user_acs_count.contest_problem_id = contest_problems.id - where contest_problems.contest_id = $1 - order by "user".name, contest_problems.label) + with {}, {} + {} "#, - ) + ALL_SUBMISSIONS_IGNORING_CE_VERDICTS, + WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT, + SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE + )) + .bind::<sql_types::Integer, _>(0) + .bind::<sql_types::Integer, _>(contest_id) + .load(connection) +} + +pub fn get_problems_by_contest_id_with_score_with_blind_and_frozen( + connection: &mut PgConnection, + user_id: i32, + contest_id: i32, +) -> QueryResult<Vec<ProblemByContestWithScore>> { + diesel::sql_query(format!( + r#" + with {}, {} + {} + "#, + BLIND_AND_FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS, + WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT, + SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE + )) + .bind::<sql_types::Integer, _>(user_id) + .bind::<sql_types::Integer, _>(contest_id) + .load(connection) +} + +pub fn get_problems_by_contest_id_with_score_with_frozen( + connection: &mut PgConnection, + contest_id: i32, +) -> QueryResult<Vec<ProblemByContestWithScore>> { + diesel::sql_query(format!( + r#" + with {}, {} + {} + "#, + FROZEN_SUBMISSIONS_IGNORING_CE_VERDICTS, + WITH_FIRST_AC_AND_FAILED_SUBMISSION_COUNT_AND_LAST_FAILED_SUBMISSION_AND_USER_AC_COUNT, + SELECT_PROBLEMS_BY_CONTEST_ID_WITH_SCORE + )) + .bind::<sql_types::Integer, _>(0) .bind::<sql_types::Integer, _>(contest_id) .load(connection) } @@ -330,17 +438,10 @@ pub fn get_problem_by_contest_id_label( #[derive(Queryable)] pub struct ProblemByContestMetadata { pub contest_id: i32, + pub label: String, pub id: String, pub memory_limit_bytes: i32, pub time_limit_ms: i32, - pub checker_path: String, - pub checker_language: String, - pub validator_path: String, - pub validator_language: String, - pub main_solution_path: String, - pub main_solution_language: String, - pub test_count: i32, - pub test_pattern: String, pub status: String, } @@ -353,17 +454,10 @@ pub fn get_problem_by_contest_id_metadata( .filter(contest_problems::id.eq(contest_problem_id)) .select(( contest_problems::contest_id, + contest_problems::label, problem::id, problem::memory_limit_bytes, problem::time_limit_ms, - problem::checker_path, - problem::checker_language, - problem::validator_path, - problem::validator_language, - problem::main_solution_path, - problem::main_solution_language, - problem::test_count, - problem::test_pattern, problem::status, )) .first(connection) diff --git a/src/models/repository.rs b/src/models/repository.rs new file mode 100644 index 0000000000000000000000000000000000000000..24d28e74e3dc1f907b4ce46d2a0c1a73fdb786f2 --- /dev/null +++ b/src/models/repository.rs @@ -0,0 +1,49 @@ +use chrono::prelude::*; +use diesel::insert_into; +use diesel::prelude::*; + +use crate::schema::repository; + +#[derive(Queryable)] +pub struct Repository { + pub id: i32, + pub name: String, + pub path: String, + pub remote_url: String, + pub creation_user_id: i32, + pub creation_instant: NaiveDateTime, +} + +#[derive(Insertable)] +#[diesel(table_name = repository)] +pub struct NewRepository { + pub name: String, + pub path: String, + pub remote_url: String, + pub creation_user_id: i32, + pub creation_instant: NaiveDateTime, +} + +pub fn insert_repository( + connection: &mut PgConnection, + new_repository: NewRepository, +) -> QueryResult<Repository> { + insert_into(repository::table) + .values(new_repository) + .execute(connection)?; + repository::table + .order(repository::id.desc()) + .first(connection) +} + +pub fn get_repositories(connection: &mut PgConnection) -> QueryResult<Vec<Repository>> { + repository::table + .order(repository::creation_instant.desc()) + .load(connection) +} + +pub fn get_repository_by_id(connection: &mut PgConnection, id: i32) -> QueryResult<Repository> { + repository::table + .filter(repository::id.eq(id)) + .first(connection) +} diff --git a/src/models/submission.rs b/src/models/submission.rs index 5ecad3644972fb7d10d7d510aa56bbec253f3c41..aca7c4bf47ee4ee93798194046bb0946a78eb24e 100644 --- a/src/models/submission.rs +++ b/src/models/submission.rs @@ -22,6 +22,7 @@ pub struct Submission { pub contest_problem_id: i32, pub user_id: i32, pub failed_test: Option<i32>, + pub balloon_delivered_instant: Option<NaiveDateTime>, } #[allow(clippy::type_complexity)] @@ -40,6 +41,7 @@ const SUBMISSION_COLUMNS: ( submission::contest_problem_id, submission::user_id, submission::failed_test, + submission::balloon_delivered_instant, ) = ( submission::uuid, submission::verdict, @@ -55,6 +57,7 @@ const SUBMISSION_COLUMNS: ( submission::contest_problem_id, submission::user_id, submission::failed_test, + submission::balloon_delivered_instant, ); #[derive(Queryable)] @@ -63,6 +66,8 @@ pub struct ContestProblem { pub label: String, pub contest_id: i32, pub problem_id: String, + pub balloon_color: Option<String>, + pub problem_name_override: Option<String>, } const CONTEST_PROBLEMS_COLUMNS: ( @@ -70,11 +75,15 @@ const CONTEST_PROBLEMS_COLUMNS: ( contest_problems::label, contest_problems::contest_id, contest_problems::problem_id, + contest_problems::balloon_color, + contest_problems::problem_name_override, ) = ( contest_problems::id, contest_problems::label, contest_problems::contest_id, contest_problems::problem_id, + contest_problems::balloon_color, + contest_problems::problem_name_override, ); #[derive(Insertable)] @@ -137,22 +146,15 @@ pub fn get_waiting_judge_submissions( ) -> QueryResult<Vec<(Submission, ProblemByContestMetadata)>> { submission::table .inner_join(contest_problems::table.inner_join(problem::table)) - .order_by(submission::submission_instant.asc()) + .order_by(submission::submission_instant.desc()) .select(( SUBMISSION_COLUMNS, ( contest_problems::contest_id, + contest_problems::label, problem::id, problem::memory_limit_bytes, problem::time_limit_ms, - problem::checker_path, - problem::checker_language, - problem::validator_path, - problem::validator_language, - problem::main_solution_path, - problem::main_solution_language, - problem::test_count, - problem::test_pattern, problem::status, ), )) @@ -202,6 +204,19 @@ pub fn get_submissions_user( .load::<(Submission, ContestProblem, User)>(connection) } +pub fn get_submissions_by_contest( + connection: &mut PgConnection, + contest_id: i32, +) -> QueryResult<Vec<(Submission, ContestProblem, User)>> { + submission::table + .inner_join(contest_problems::table) + .inner_join(user::table) + .filter(contest_problems::contest_id.eq(contest_id)) + .order_by(submission::submission_instant.desc()) + .select((SUBMISSION_COLUMNS, CONTEST_PROBLEMS_COLUMNS, USER_COLUMNS)) + .load::<(Submission, ContestProblem, User)>(connection) +} + pub fn get_submissions_user_by_contest( connection: &mut PgConnection, user_id: i32, @@ -217,6 +232,21 @@ pub fn get_submissions_user_by_contest( .load::<(Submission, ContestProblem, User)>(connection) } +pub fn get_submissions_by_contest_problem( + connection: &mut PgConnection, + contest_id: i32, + problem_label: &str, +) -> QueryResult<Vec<(Submission, ContestProblem, User)>> { + submission::table + .inner_join(contest_problems::table) + .inner_join(user::table) + .filter(contest_problems::contest_id.eq(contest_id)) + .filter(contest_problems::label.eq(problem_label)) + .order_by(submission::submission_instant.desc()) + .select((SUBMISSION_COLUMNS, CONTEST_PROBLEMS_COLUMNS, USER_COLUMNS)) + .load::<(Submission, ContestProblem, User)>(connection) +} + pub fn get_submissions_user_by_contest_problem( connection: &mut PgConnection, user_id: i32, @@ -233,3 +263,17 @@ pub fn get_submissions_user_by_contest_problem( .select((SUBMISSION_COLUMNS, CONTEST_PROBLEMS_COLUMNS, USER_COLUMNS)) .load::<(Submission, ContestProblem, User)>(connection) } + +pub fn deliver_balloon( + connection: &mut PgConnection, + uuid: String, + new_balloon_delivered_instant: Option<NaiveDateTime>, +) -> QueryResult<()> { + diesel::update(submission::table) + .filter(submission::uuid.eq(uuid)) + .set(( + submission::balloon_delivered_instant.eq(new_balloon_delivered_instant), + )) + .execute(connection)?; + Ok(()) +} diff --git a/src/models/user.rs b/src/models/user.rs index 510da99613799eb0003bfba8aea11c71e3d4a19e..734fafa80e270b175667ef09b032b33411ff23c7 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -19,15 +19,30 @@ struct UserWithHashedPassword { pub struct User { pub id: i32, pub name: String, + pub full_name: Option<String>, + pub single_contest_id: Option<i32>, pub is_admin: bool, } -pub const USER_COLUMNS: (user::id, user::name, user::is_admin) = - (user::id, user::name, user::is_admin); +pub const USER_COLUMNS: ( + user::id, + user::name, + user::full_name, + user::single_contest_id, + user::is_admin, +) = ( + user::id, + user::name, + user::full_name, + user::single_contest_id, + user::is_admin, +); #[derive(Insertable)] #[diesel(table_name = user)] struct DatabaseNewUser<'a> { pub name: &'a str, + pub full_name: Option<&'a str>, + pub single_contest_id: Option<i32>, pub hashed_password: &'a str, pub is_admin: bool, pub creation_instant: NaiveDateTime, @@ -36,6 +51,8 @@ struct DatabaseNewUser<'a> { pub struct NewUser<'a> { pub name: &'a str, + pub full_name: Option<&'a str>, + pub single_contest_id: Option<i32>, pub password: &'a str, pub is_admin: bool, pub creation_instant: NaiveDateTime, @@ -133,6 +150,8 @@ pub fn insert_new_user( insert_into(user::table) .values(DatabaseNewUser { name: new_user.name, + full_name: new_user.full_name, + single_contest_id: new_user.single_contest_id, hashed_password: &hashed_password, is_admin: new_user.is_admin, creation_instant: new_user.creation_instant, @@ -142,3 +161,14 @@ pub fn insert_new_user( Ok(get_user_by_name(connection, new_user.name)?) } + +pub fn get_users_by_single_contest_id( + connection: &mut PgConnection, + single_contest_id: i32, +) -> QueryResult<Vec<User>> { + user::table + .filter(user::single_contest_id.eq(single_contest_id)) + .select(USER_COLUMNS) + .order(user::name) + .load(connection) +} diff --git a/src/pages/change_password.rs b/src/pages/change_password.rs index 8daa2c6eea90f4a787d097c052b04d4ed199b44d..ca8e9da1c74f65dd434606c66ee49a2951fe0070 100644 --- a/src/pages/change_password.rs +++ b/src/pages/change_password.rs @@ -1,5 +1,6 @@ use crate::models::user; use crate::models::user::PasswordMatched; +use crate::pages::assert_not_single_contest; use crate::pages::prelude::*; #[derive(Serialize, Deserialize)] @@ -16,7 +17,10 @@ async fn change_password( pool: Data<DbPool>, request: HttpRequest, ) -> PageResult { - let identity = require_identity(&identity)?; + let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } if form.new_password != form.new_password_repeat { return Err(PageError::Validation("Senhas são diferentes".into())); } @@ -25,7 +29,7 @@ async fn change_password( match user::change_password( &mut connection, - identity.id, + logged_user.id, &form.old_password, &form.new_password, )? { diff --git a/src/pages/create_backup.rs b/src/pages/create_backup.rs new file mode 100644 index 0000000000000000000000000000000000000000..c399d9a46f20b93ef6810fadf22521431a45a3b9 --- /dev/null +++ b/src/pages/create_backup.rs @@ -0,0 +1,90 @@ +use chrono::Local; +use std::io::{Cursor, Write}; +use uuid::Uuid; + +use actix_multipart::Multipart; +use futures::{StreamExt, TryStreamExt}; + +use crate::models::{backup, contest}; +use crate::pages::assert_contest_not_started; +use crate::pages::prelude::*; + +#[post("/contests/{id}/backups/")] +async fn create_backup( + identity: Identity, + mut payload: Multipart, + pool: Data<DbPool>, + path: Path<(i32,)>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + + #[derive(Debug)] + struct Form { + contents: Option<Vec<u8>>, + filename: Option<String>, + } + + let mut form = Form { + contents: None, + filename: None, + }; + + while let Ok(Some(mut field)) = payload.try_next().await { + let mut cursor = Cursor::new(vec![]); + while let Some(chunk) = field.next().await { + let data = chunk.unwrap(); + cursor + .write(&data) + .map_err(|_| PageError::Validation("Corpo inválido".into()))?; + } + + cursor.set_position(0); + + let content_disposition = field.content_disposition(); + match content_disposition.get_name() { + Some("contents") => { + form.filename = Some( + content_disposition + .get_filename() + .ok_or(PageError::Validation(format!( + "Campo contents sem nome de arquivo" + )))? + .into(), + ); + form.contents = Some(cursor.into_inner()); + } + _ => {} + } + } + + if let None = form.contents { + return Err(PageError::Validation("Arquivo não submetido".into())); + } + + let contents = form.contents.unwrap(); + let filename = form.filename.unwrap(); + + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + + let uuid = Uuid::new_v4(); + backup::insert_backup( + &mut connection, + backup::NewBackup { + uuid: uuid.to_string(), + filename, + contents, + creation_instant: Local::now().naive_utc(), + contest_id: contest.id, + user_id: logged_user.id, + }, + )?; + + Ok(redirect_to_referer( + "Backup criado com sucesso".into(), + &request, + )) +} diff --git a/src/pages/create_clarification.rs b/src/pages/create_clarification.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf8a8d7caa262f79a5f65e2d2beec55b53977290 --- /dev/null +++ b/src/pages/create_clarification.rs @@ -0,0 +1,52 @@ +use chrono::Local; +use serde_with::serde_as; +use serde_with::NoneAsEmptyString; +use uuid::Uuid; + +use crate::models::clarification::NewClarification; +use crate::models::{clarification, contest}; +use crate::pages::assert_contest_not_started; +use crate::pages::prelude::*; + +#[serde_as] +#[derive(Serialize, Deserialize)] +struct CreateClarificationForm { + question: String, + #[serde_as(as = "NoneAsEmptyString")] + contest_problem_id: Option<i32>, +} + +#[post("/contests/{id}/clarifications/")] +async fn create_clarification( + identity: Identity, + pool: Data<DbPool>, + form: Form<CreateClarificationForm>, + path: Path<(i32,)>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + + let uuid = Uuid::new_v4(); + clarification::insert_clarification( + &mut connection, + NewClarification { + uuid: uuid.to_string(), + question: form.question.clone(), + question_instant: Local::now().naive_utc(), + answer_to_all: false, + contest_problem_id: form.contest_problem_id, + contest_id: contest.id, + user_id: logged_user.id, + }, + )?; + + Ok(redirect_to_referer( + "Clarificação criada com sucesso".into(), + &request, + )) +} diff --git a/src/pages/create_contest.rs b/src/pages/create_contest.rs index a71d36c394b88023b65ce0e4eed1f73913c4f54c..4392e914bd03ee01bdc20031dc7ba8d2c1e75204 100644 --- a/src/pages/create_contest.rs +++ b/src/pages/create_contest.rs @@ -227,39 +227,35 @@ async fn create_contest( name: metadata.names.name[0].value.clone(), memory_limit_bytes: metadata.judging.testset[0].memory_limit.parse().unwrap(), time_limit_ms: metadata.judging.testset[0].time_limit.parse().unwrap(), - checker_path: metadata.assets.checker.source.path.clone(), - checker_language: map_codeforces_language(&metadata.assets.checker.r#type)?, - validator_path: metadata.assets.validators.validator[0].source.path.clone(), - validator_language: map_codeforces_language( - &metadata.assets.validators.validator[0].source.r#type, - )?, - main_solution_path: main_solution.path.clone(), - main_solution_language: map_codeforces_language(&main_solution.r#type)?, - test_pattern: metadata.judging.testset[0].input_path_pattern.clone(), - test_count: metadata.judging.testset[0].test_count.parse().unwrap(), status: "compiled".into(), creation_instant: Local::now().naive_utc(), creation_user_id: logged_user.id, }, )?; + let main_solution_path = &main_solution.path; + let main_solution_language = map_codeforces_language(&main_solution.r#type)?; + let test_pattern = &metadata.judging.testset[0].input_path_pattern; + let test_count: u32 = metadata.judging.testset[0].test_count.parse().unwrap(); + for (i, test) in metadata.judging.testset[0].tests.test.iter().enumerate() { let i = i + 1; let test_path = format!( "./{}/{}", problem_id, - import_contest::format_width(&problem.test_pattern, i) + import_contest::format_width(&test_pattern, i) ); info!( - "Iterating through test {} to {:#?}, which is {}", + "Iterating through test {}/{} to {:#?}, which is {}", i, test_path, + test_count, test.method.as_ref().unwrap() ); if test.method.as_ref().unwrap() == "manual" { - let test_name = PathBuf::from(&name) - .join(import_contest::format_width(&problem.test_pattern, i)); + let test_name = + PathBuf::from(&name).join(import_contest::format_width(&test_pattern, i)); info!("Extracting {:#?} from zip", test_name); std::io::copy( &mut zip.by_name(test_name.to_str().unwrap())?, @@ -293,8 +289,8 @@ async fn create_contest( let run_stats = language::run_cached( &job_sender, &job_result_sender, - &problem.main_solution_language, - format!("./{}/{}", problem.id, problem.main_solution_path), + &main_solution_language, + format!("./{}/{}", problem.id, main_solution_path), vec![], Some(test_path.clone()), Some(format!("{}.a", test_path)), @@ -313,15 +309,12 @@ async fn create_contest( language::judge( &job_sender, &job_result_sender, - &problem.main_solution_language, + &main_solution_language, fs::read_to_string(PathBuf::from(format!( "/data/{}/{}", - problem.id, problem.main_solution_path + problem.id, main_solution_path )))?, - problem.test_count, - format!("./{}/{}", problem.id, problem.test_pattern), - problem.checker_language, - format!("./{}/{}", problem.id, problem.checker_path), + problem.id, problem.memory_limit_bytes / 1_024, problem.time_limit_ms, ) diff --git a/src/pages/create_contest_submission.rs b/src/pages/create_contest_submission.rs new file mode 100644 index 0000000000000000000000000000000000000000..fe4cbf296e9bba782fed70923110c0980054ba34 --- /dev/null +++ b/src/pages/create_contest_submission.rs @@ -0,0 +1,128 @@ +use chrono::Local; +use std::io::{Cursor, Read, Write}; +use uuid::Uuid; + +use actix_multipart::Multipart; +use futures::{StreamExt, TryStreamExt}; + +use crate::models::{contest, problem, submission}; +use crate::pages::assert_contest_not_over; +use crate::pages::assert_contest_not_started; +use crate::pages::prelude::*; +use crate::queue::job_protocol::{job, Job, Language}; + +#[post("/contests/{id}/submissions/")] +async fn create_contest_submission( + identity: Identity, + mut payload: Multipart, + pool: Data<DbPool>, + job_sender: Data<Sender<Job>>, + languages: Data<Arc<DashMap<String, Language>>>, + session: Session, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let mut connection = pool.get()?; + + #[derive(Debug)] + struct Form { + contest_problem_id: Option<i32>, + language: Option<String>, + source_text: Option<String>, + } + + let mut form = Form { + contest_problem_id: None, + language: None, + source_text: None, + }; + + while let Ok(Some(mut field)) = payload.try_next().await { + let mut cursor = Cursor::new(vec![]); + while let Some(chunk) = field.next().await { + let data = chunk.unwrap(); + cursor + .write(&data) + .map_err(|_| PageError::Validation("Corpo inválido".into()))?; + } + + cursor.set_position(0); + + fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> { + let mut value = String::new(); + cursor + .read_to_string(&mut value) + .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?; + Ok(value) + } + + match field.content_disposition().get_name() { + Some("language") => form.language = Some(parse_field("language", &mut cursor)?), + Some("contest_problem_id") => { + form.contest_problem_id = + parse_field("contest_problem_id", &mut cursor)?.parse().ok() + } + Some("source_text") => { + form.source_text = Some(parse_field("source_text", &mut cursor)?) + } + _ => {} + } + } + + if let None = form.source_text { + return Err(PageError::Validation("Código-fonte não submetido".into())); + } + + let language = form.language.unwrap_or("".to_string()); + + languages + .get(&language) + .ok_or_else(|| PageError::Validation("Linguagem inexistente".into()))?; + + let contest = contest::get_contest_by_contest_problem_id( + &mut connection, + form.contest_problem_id.unwrap(), + )?; + assert_contest_not_started(&logged_user, &contest)?; + assert_contest_not_over(&logged_user, &contest)?; + + let source_text = form.source_text.unwrap(); + + let uuid = Uuid::new_v4(); + submission::insert_submission( + &mut connection, + submission::NewSubmission { + uuid: uuid.to_string(), + source_text: source_text.clone(), + language: language.clone(), + submission_instant: Local::now().naive_utc(), + contest_problem_id: form.contest_problem_id.unwrap(), + user_id: logged_user.id, + }, + )?; + + let metadata = problem::get_problem_by_contest_id_metadata( + &mut connection, + form.contest_problem_id.unwrap(), + )?; + + job_sender + .send(Job { + uuid: uuid.to_string(), + language: language.clone(), + time_limit_ms: metadata.time_limit_ms, + memory_limit_kib: metadata.memory_limit_bytes / 1_024, + + which: Some(job::Which::Judgement(job::Judgement { + source_text: source_text.clone(), + package_id: metadata.id, + })), + }) + .await?; + + session.insert("language", language.clone())?; + Ok(redirect_to_referer( + format!("Submetido {} com sucesso!", uuid), + &request, + )) +} diff --git a/src/pages/create_printing_task.rs b/src/pages/create_printing_task.rs new file mode 100644 index 0000000000000000000000000000000000000000..a993dd042c0f5ce2c3748382311b7424c7c008c9 --- /dev/null +++ b/src/pages/create_printing_task.rs @@ -0,0 +1,83 @@ +use chrono::Local; +use std::io::{Cursor, Read, Write}; +use uuid::Uuid; + +use actix_multipart::Multipart; +use futures::{StreamExt, TryStreamExt}; + +use crate::models::{contest, printing_task}; +use crate::pages::assert_contest_not_started; +use crate::pages::prelude::*; + +#[post("/contests/{id}/printing-tasks/")] +async fn create_printing_task( + identity: Identity, + mut payload: Multipart, + pool: Data<DbPool>, + path: Path<(i32,)>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + + #[derive(Debug)] + struct Form { + contents: Option<String>, + } + + let mut form = Form { contents: None }; + + while let Ok(Some(mut field)) = payload.try_next().await { + let mut cursor = Cursor::new(vec![]); + while let Some(chunk) = field.next().await { + let data = chunk.unwrap(); + cursor + .write(&data) + .map_err(|_| PageError::Validation("Corpo inválido".into()))?; + } + + cursor.set_position(0); + + fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> { + let mut value = String::new(); + cursor + .read_to_string(&mut value) + .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?; + Ok(value) + } + + match field.content_disposition().get_name() { + Some("contents") => form.contents = Some(parse_field("contents", &mut cursor)?), + _ => {} + } + } + + if let None = form.contents { + return Err(PageError::Validation("Arquivo não submetido".into())); + } + + let contents = form.contents.unwrap(); + + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + + let uuid = Uuid::new_v4(); + printing_task::insert_printing_task( + &mut connection, + printing_task::NewPrintingTask { + uuid: uuid.to_string(), + contents: contents.clone(), + creation_instant: Local::now().naive_utc(), + status: "pending".into(), + done_instant: None, + contest_id: contest.id, + user_id: logged_user.id, + }, + )?; + + Ok(redirect_to_referer( + "Tarefa de impressão criada com sucesso".into(), + &request, + )) +} diff --git a/src/pages/create_submission.rs b/src/pages/create_submission.rs index 73ccfecdb4f5da4f1adec8df5d40aac9c8a50b0f..02a67f28654d14d304a54f036aa35833cec91d6a 100644 --- a/src/pages/create_submission.rs +++ b/src/pages/create_submission.rs @@ -65,10 +65,7 @@ async fn create_submission( which: Some(job::Which::Judgement(job::Judgement { source_text: (&form.source_text).into(), - test_count: metadata.test_count, - test_pattern: format!("./{}/{}", metadata.id, metadata.test_pattern), - checker_language: metadata.checker_language, - checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path), + package_id: metadata.id, })), }) .await?; diff --git a/src/pages/create_user.rs b/src/pages/create_user.rs index 16def27691251c93db86ac8b999037bb78ab96ea..7f14facbe7603b715a4c654b59b2c03aa004168e 100644 --- a/src/pages/create_user.rs +++ b/src/pages/create_user.rs @@ -6,6 +6,8 @@ use crate::pages::prelude::*; #[derive(Serialize, Deserialize)] struct CreateUserForm { name: String, + full_name: Option<String>, + single_contest_id: Option<i32>, password: String, is_admin: Option<bool>, } @@ -31,6 +33,8 @@ async fn create_user( user::NewUser { name: &form.name, password: &form.password, + full_name: form.full_name.as_deref(), + single_contest_id: form.single_contest_id, is_admin: form.is_admin.unwrap_or(false), creation_instant: Local::now().naive_utc(), creation_user_id: Some(identity.id), diff --git a/src/pages/get_backup_contents.rs b/src/pages/get_backup_contents.rs new file mode 100644 index 0000000000000000000000000000000000000000..265286963b6001871639d279a661292ad8e5a8bc --- /dev/null +++ b/src/pages/get_backup_contents.rs @@ -0,0 +1,26 @@ +use crate::models::backup; +use crate::pages::prelude::*; +use actix_web::{http::header::ContentType, HttpResponse}; + +#[get("/backups/{uuid}/contents")] +async fn get_backup_contents( + identity: Identity, + pool: Data<DbPool>, + path: Path<(String,)>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (backup_uuid,) = path.into_inner(); + let mut connection = pool.get()?; + + let backup = backup::get_backup_by_uuid(&mut connection, backup_uuid)?; + + if backup.user_id != logged_user.id && !logged_user.is_admin { + return Err(PageError::Forbidden( + "Não é possível acessar um backup de outro usuário".into(), + )); + } + + Ok(HttpResponse::Ok() + .content_type(ContentType::octet_stream()) + .body(backup.contents)) +} diff --git a/src/pages/get_contest_by_id.rs b/src/pages/get_contest_by_id.rs index f6abc8923e69be8d0a056bf8e7e117dda41e0f74..a5e496ec25e455d6359054fd188aebda74baaece 100644 --- a/src/pages/get_contest_by_id.rs +++ b/src/pages/get_contest_by_id.rs @@ -1,8 +1,9 @@ use crate::models::{contest, problem, submission}; use crate::pages::prelude::*; use crate::pages::{ - assert_contest_not_started, get_formatted_contest, get_formatted_problem_by_contest_with_score, - get_formatted_submissions, FormattedContest, FormattedProblemByContestWithScore, + assert_contest_not_started, assert_not_single_contest, get_formatted_contest, + get_formatted_problem_by_contest_with_score, get_formatted_submissions, + get_formatted_submissions_with_blind, FormattedContest, FormattedProblemByContestWithScore, FormattedSubmission, }; @@ -29,14 +30,28 @@ pub async fn get_contest_by_id( let mut connection = pool.get()?; let contest = contest::get_contest_by_id(&mut connection, contest_id)?; assert_contest_not_started(&logged_user, &contest)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } - let problems = problem::get_problems_user_by_contest_id_with_score( - &mut connection, - logged_user.id, - contest_id, - )?; - let submissions = - submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; + let problems = if !logged_user.is_admin { + problem::get_problems_user_by_contest_id_with_score_with_blind_and_frozen( + &mut connection, + logged_user.id, + contest_id, + )? + } else { + problem::get_problems_user_by_contest_id_with_score_without_blind_and_frozen( + &mut connection, + logged_user.id, + contest_id, + )? + }; + let submissions = if !logged_user.is_admin { + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)? + } else { + submission::get_submissions_by_contest(&mut connection, contest_id)? + }; render( &hb, @@ -48,7 +63,11 @@ pub async fn get_contest_by_id( .iter() .map(|p| get_formatted_problem_by_contest_with_score(p, &contest)) .collect(), - submissions: get_formatted_submissions(&tz, &submissions), + submissions: if !logged_user.is_admin { + get_formatted_submissions_with_blind(&tz, &contest, &submissions) + } else { + get_formatted_submissions(&tz, &submissions) + }, }, ) } diff --git a/src/pages/get_contest_clarifications_by_id.rs b/src/pages/get_contest_clarifications_by_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..527f4a187588a428e708c3a782bddd818a7a754f --- /dev/null +++ b/src/pages/get_contest_clarifications_by_id.rs @@ -0,0 +1,90 @@ +use crate::models::{contest, clarification}; +use crate::pages::prelude::*; +use crate::pages::{ + assert_contest_not_started, assert_not_single_contest, get_formatted_contest, + FormattedContest, +}; + +#[derive(Serialize)] +struct FormattedClarification { + pub uuid: String, + pub question: String, + pub question_instant: String, + pub answer: Option<String>, + pub answer_instant: Option<String>, + pub answer_user_id: Option<i32>, + pub answer_to_all: bool, + pub contest_problem_id: Option<i32>, + pub problem_label: Option<String>, + pub contest_id: i32, + pub user_id: i32, + pub user_name: String, + pub user_full_name: Option<String>, +} + +#[get("/contests/{id}/clarifications")] +pub async fn get_contest_clarifications_by_id( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + clarifications: Vec<FormattedClarification>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } + + let clarifications = if !logged_user.is_admin { + clarification::get_clarifications_by_contest_user( + &mut connection, + logged_user.id, + contest_id, + )? + } else { + clarification::get_clarifications_by_contest( + &mut connection, + contest_id, + )? + }; + + render( + &hb, + "clarifications", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + clarifications: clarifications + .into_iter() + .map(|(c, u, problem_label)| FormattedClarification { + uuid: c.uuid, + question: c.question, + question_instant: format_utc_date_time(&tz, c.question_instant), + answer: c.answer, + answer_instant: c.answer_instant.map(|i| format_utc_date_time(&tz, i)), + answer_user_id: c.answer_user_id, + answer_to_all: c.answer_to_all, + contest_problem_id: c.contest_problem_id, + problem_label, + contest_id: c.contest_id, + user_id: c.user_id, + user_name: u.name, + user_full_name: u.full_name, + }) + .collect(), + }, + ) +} diff --git a/src/pages/get_contest_problem_by_id_label.rs b/src/pages/get_contest_problem_by_id_label.rs index fc60d1690affddf19ce931f55be839a5582b0a76..78acf0ead0a75d5954a3cd89cc0b5b06b5ebac64 100644 --- a/src/pages/get_contest_problem_by_id_label.rs +++ b/src/pages/get_contest_problem_by_id_label.rs @@ -3,7 +3,8 @@ use crate::models::{contest, problem, submission}; use crate::pages::get_editor::{get_formatted_languages, FormattedLanguage}; use crate::pages::prelude::*; use crate::pages::{ - assert_contest_not_started, get_formatted_contest, get_formatted_submissions, FormattedContest, + assert_contest_not_started, assert_not_single_contest, get_formatted_contest, + get_formatted_submissions, get_formatted_submissions_with_blind, FormattedContest, FormattedSubmission, }; use crate::Language; @@ -20,6 +21,9 @@ async fn get_contest_problem_by_id_label( tz: Data<Tz>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } let (contest_id, problem_label) = path.into_inner(); #[derive(Serialize)] @@ -39,12 +43,16 @@ async fn get_contest_problem_by_id_label( let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?; let problem = problem::get_problem_by_contest_id_label(&mut connection, contest_id, &problem_label)?; - let submissions = submission::get_submissions_user_by_contest_problem( - &mut connection, - logged_user.id, - contest_id, - &problem_label, - )?; + let submissions = if logged_user.is_admin { + submission::get_submissions_by_contest_problem(&mut connection, contest_id, &problem_label)? + } else { + submission::get_submissions_user_by_contest_problem( + &mut connection, + logged_user.id, + contest_id, + &problem_label, + )? + }; render( &hb, @@ -56,7 +64,11 @@ async fn get_contest_problem_by_id_label( problems, problem, language: session.get("language")?, - submissions: get_formatted_submissions(&tz, &submissions), + submissions: if !logged_user.is_admin { + get_formatted_submissions_with_blind(&tz, &contest, &submissions) + } else { + get_formatted_submissions(&tz, &submissions) + }, }, ) } diff --git a/src/pages/get_contest_scoreboard_by_id.rs b/src/pages/get_contest_scoreboard_by_id.rs index 1e427aa94e3d52f66502be56381b020df6036e2c..0262b6dddd476002174cae23f41048651e0fdf87 100644 --- a/src/pages/get_contest_scoreboard_by_id.rs +++ b/src/pages/get_contest_scoreboard_by_id.rs @@ -3,8 +3,9 @@ use itertools::Itertools; use crate::models::{contest, problem, submission}; use crate::pages::prelude::*; use crate::pages::{ - assert_contest_not_started, get_formatted_contest, get_formatted_problem_by_contest_with_score, - get_formatted_submissions, FormattedContest, FormattedProblemByContestWithScore, + assert_contest_not_started, assert_not_single_contest, get_formatted_contest, + get_formatted_problem_by_contest_with_score, get_formatted_submissions, + get_formatted_submissions_with_blind, FormattedContest, FormattedProblemByContestWithScore, FormattedSubmission, }; @@ -18,11 +19,15 @@ async fn get_contest_scoreboard_by_id( tz: Data<Tz>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } let (contest_id,) = path.into_inner(); #[derive(Serialize)] struct Score { pub user_name: Option<String>, + pub user_full_name: Option<String>, pub problems: Vec<FormattedProblemByContestWithScore>, pub solved_count: i64, pub penalty: i64, @@ -39,24 +44,29 @@ async fn get_contest_scoreboard_by_id( let mut connection = pool.get()?; let contest = contest::get_contest_by_id(&mut connection, contest_id)?; assert_contest_not_started(&logged_user, &contest)?; - let scores = problem::get_problems_by_contest_id_with_score(&mut connection, contest_id)?; + let scores = if logged_user.is_admin { + problem::get_problems_by_contest_id_with_score_without_blind_and_frozen(&mut connection, contest_id)? + } else { + problem::get_problems_by_contest_id_with_score_with_frozen(&mut connection, contest_id)? + }; let submissions = submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; - let mut scores: Vec<_> = scores.into_iter().group_by(|e| e.user_name.as_ref().cloned()).into_iter().map(|(user_name, problems)| { - let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect(); + let mut scores: Vec<_> = scores.into_iter().group_by(|e| (e.user_name.as_ref().cloned(), e.user_full_name.as_ref().cloned())).into_iter().map(|((user_name, user_full_name), problems)| { + let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect(); - Score { - user_name, - solved_count: i64::try_from(problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()).count()).unwrap(), - penalty: problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()) - .map(|p| match p.first_ac_submission_minutes { - Some(x) if x >= 0 => x, - _ => 0, - } + i64::from(20*p.failed_submissions)) - .sum(), - problems - } -}).collect(); + Score { + user_name, + user_full_name, + solved_count: i64::try_from(problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()).count()).unwrap(), + penalty: problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()) + .map(|p| match p.first_ac_submission_minutes { + Some(x) if x >= 0 => x, + _ => 0, + } + i64::from(20*p.failed_submissions)) + .sum(), + problems + } + }).collect(); scores.sort_by(|a, b| { ( a.user_name.is_some(), @@ -79,7 +89,11 @@ async fn get_contest_scoreboard_by_id( base, contest: get_formatted_contest(&tz, &contest), scores, - submissions: get_formatted_submissions(&tz, &submissions), + submissions: if !logged_user.is_admin { + get_formatted_submissions_with_blind(&tz, &contest, &submissions) + } else { + get_formatted_submissions(&tz, &submissions) + }, }, ) } diff --git a/src/pages/get_contest_tasks_by_id.rs b/src/pages/get_contest_tasks_by_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..c8be86ea7434674ccbc4c2daf76c082638f6e64b --- /dev/null +++ b/src/pages/get_contest_tasks_by_id.rs @@ -0,0 +1,152 @@ +use chrono::NaiveDateTime; +use std::collections::HashSet; + +use crate::models::{contest, clarification, submission, printing_task}; +use crate::pages::prelude::*; +use crate::pages::{get_formatted_contest, FormattedContest, + get_formatted_submissions, FormattedSubmission, +}; + +#[get("/contests/{id}/tasks")] +pub async fn get_contest_tasks_by_id( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (contest_id,) = path.into_inner(); + + struct Task { + uuid: String, + creation_instant: NaiveDateTime, + t: String, + user_name: String, + user_full_name: Option<String>, + description: String, + balloon_color: Option<String>, + first_solve: bool, + done: bool + } + + #[derive(Serialize)] + struct FormattedTask { + uuid: String, + creation_instant: String, + t: String, + user_name: String, + user_full_name: Option<String>, + description: String, + balloon_color: Option<String>, + first_solve: bool, + done: bool + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + tasks: Vec<FormattedTask>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + + let mut tasks: Vec<Task> = vec![]; + + let clarifications = clarification::get_clarifications_by_contest(&mut connection, contest_id)?; + for (clarification, user, problem_label) in clarifications { + tasks.push(Task { + uuid: clarification.uuid, + creation_instant: clarification.question_instant, + t: "clarification".into(), + user_name: user.name, + user_full_name: user.full_name, + balloon_color: None, + description: match problem_label { + Some(problem_label) => format!("Problema {}", problem_label), + None => "Geral".into() + }, + first_solve: false, + done: clarification.answer_instant.is_some() + }); + } + + let mut submissions = submission::get_submissions_by_contest(&mut connection, contest_id)?; + submissions.reverse(); + let mut solved_contest_problems: HashSet<i32> = Default::default(); + let mut solved_contest_problems_by_user: HashSet<(i32, i32)> = Default::default(); + for (submission, contest_problem, user) in &submissions { + if submission.verdict.as_ref().map(|e| e != "AC").unwrap_or(true) { continue } + if solved_contest_problems_by_user.contains(&(contest_problem.id, user.id)) { + continue + } + // if contest.frozen_scoreboard_instant.map(|e| submission.submission_instant >= e).unwrap_or(false) { + // continue + // } + tasks.push(Task { + uuid: submission.uuid.clone(), + creation_instant: submission.submission_instant, + t: "balloon".into(), + user_name: user.name.clone(), + user_full_name: user.full_name.clone(), + balloon_color: contest_problem.balloon_color.clone(), + description: format!("Problema {}", contest_problem.label), + first_solve: !solved_contest_problems.contains(&contest_problem.id), + done: submission.balloon_delivered_instant.is_some(), + }); + solved_contest_problems.insert(contest_problem.id); + solved_contest_problems_by_user.insert((contest_problem.id, user.id)); + } + + let printing_tasks = printing_task::get_printing_tasks_by_contest( + &mut connection, + contest_id + )?; + for (printing_task, user) in printing_tasks { + tasks.push(Task { + uuid: printing_task.uuid.clone(), + creation_instant: printing_task.creation_instant, + t: "printing".into(), + user_name: user.name.clone(), + user_full_name: user.full_name.clone(), + balloon_color: None, + description: format!("{} bytes", printing_task.contents.len()), + first_solve: false, + done: printing_task.done_instant.is_some(), + }); + } + + tasks.sort_by(|a, b| { + (a.done, b.creation_instant).cmp(&(b.done, a.creation_instant)) + }); + + render( + &hb, + "tasks", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + tasks: tasks.into_iter().map(|e| FormattedTask { + uuid: e.uuid, + creation_instant: format_utc_date_time(&tz, e.creation_instant), + t: e.t, + user_name: e.user_name, + user_full_name: e.user_full_name, + balloon_color: e.balloon_color, + description: e.description, + first_solve: e.first_solve, + done: e.done + }).collect(), + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} diff --git a/src/pages/get_contest_webcast_by_id.rs b/src/pages/get_contest_webcast_by_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..0592c1ddfba884881da15c4a27916ccf30a58f1a --- /dev/null +++ b/src/pages/get_contest_webcast_by_id.rs @@ -0,0 +1,98 @@ +use std::collections::HashSet; +use zip::ZipWriter; +use zip::write::FileOptions; +use std::io::Write; +use std::cmp::min; +use chrono::Local; +use regex::Regex; + +use crate::models::{submission, contest, problem, user}; +use crate::pages::prelude::*; + +#[derive(Deserialize)] +struct SecretParams { + secret: String, +} + +#[get("/contests/{id}/webcast.zip")] +async fn get_contest_webcast_by_id( + pool: Data<DbPool>, + path: Path<(i32,)>, + query: Query<SecretParams> +) -> PageResult { + let (contest_id,) = path.into_inner(); + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?; + let users_contest = user::get_users_by_single_contest_id(&mut connection, contest_id)?; + let users_contest_users: HashSet<String> = + HashSet::from_iter(users_contest.iter().map(|e| e.name.clone())); + let submissions = submission::get_submissions_by_contest(&mut connection, contest_id)?; + + let mut buf = [0; 65536]; + { + let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf[..])); + + let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored); + + zip.start_file("contest", options)?; + zip.write(contest.name.as_bytes())?; + zip.write(b"\n")?; + let start_instant = contest.start_instant.unwrap(); + let end_instant = contest.end_instant.unwrap(); + let maximum_time = (end_instant - start_instant).num_minutes(); + let current_time = min(maximum_time, (Local::now().naive_utc() - start_instant).num_minutes()); + let score_freeze_time = match contest.frozen_scoreboard_instant { + None => maximum_time, + Some(frozen_scoreboard_instant) => (frozen_scoreboard_instant - start_instant).num_minutes() + }; + let penalty = 20; + let contest_params = format!("{}\x1c{}\x1c{}\x1c{}\n", maximum_time, current_time, score_freeze_time, penalty); + zip.write(contest_params.as_bytes())?; + let number_teams = users_contest.len(); + let number_problems = problems.len(); + let team_params = format!("{}\x1c{}\n", number_teams, number_problems); + zip.write(team_params.as_bytes())?; + let re = Regex::new(r"^\[([^\]]+)\]").unwrap(); + for user in users_contest { + let institution = user.full_name.as_ref().and_then(|e| re.captures(e)) + .and_then(|e| e.get(1)).map(|e| e.as_str()).unwrap_or("".into()); + let user_full_name = user.full_name.clone().unwrap_or("".into()); + zip.write(format!("{}\x1c{}\x1c{}\n", user.name, institution, user_full_name).as_bytes())?; + } + zip.write(b"1\x1c1\n")?; + zip.write(format!("{}\x1cY\n", number_problems).as_bytes())?; + + let mut i = 1; + zip.start_file("runs", options)?; + for (submission, contest_problem, user) in submissions { + if !users_contest_users.contains(&user.name) { continue } + zip.write(format!( + "{}\x1c{}\x1c{}\x1c{}\x1c{}\n", + (submission.submission_instant.and_utc().timestamp())*10 + i, + (submission.submission_instant - start_instant).num_minutes(), + user.name, + contest_problem.label, + match submission.verdict.as_ref().map(|e| e.as_str()) { + Some("WJ") | None => "?", + Some("AC") => "Y", + Some("CE") => "X", + _ => "N", + } + ).as_bytes())?; + i += 1; + if i > 10 { i -= 10; } + } + + zip.start_file("time", options)?; + zip.write(format!("{}", min((end_instant - start_instant).num_seconds(), (Local::now().naive_utc() - start_instant).num_seconds())).as_bytes())?; + + zip.start_file("version", options)?; + zip.write(b"1.0")?; + + zip.finish()?; + } + + Ok(HttpResponse::Ok().body(Vec::from(buf))) +} diff --git a/src/pages/get_contests.rs b/src/pages/get_contests.rs index 1caf6c677a6b0ffc82387e2bb4cbd55d792192e5..1ce114f3e884b8949062aa1aa0cb440be95842f5 100644 --- a/src/pages/get_contests.rs +++ b/src/pages/get_contests.rs @@ -1,3 +1,4 @@ +use crate::pages::assert_not_single_contest; use crate::pages::prelude::*; use crate::pages::{get_formatted_contests, FormattedContest}; @@ -10,6 +11,9 @@ async fn get_contests( tz: Data<Tz>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } #[derive(Serialize)] struct Context { @@ -23,7 +27,7 @@ async fn get_contests( "contests", &Context { base, - contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?, + contests: get_formatted_contests(&mut connection, None, &tz)?, }, ) } diff --git a/src/pages/get_main.rs b/src/pages/get_main.rs index e0c6bb8eaa22a852068cd658f56bb2e6b9aebb3e..acec1aae7a4136e5ee5f525d9549b301f2c014b2 100644 --- a/src/pages/get_main.rs +++ b/src/pages/get_main.rs @@ -1,7 +1,8 @@ use crate::models::submission; use crate::pages::prelude::*; use crate::pages::{ - get_formatted_contests, get_formatted_submissions, FormattedContest, FormattedSubmission, + assert_not_single_contest, get_formatted_contests, get_formatted_submissions, FormattedContest, + FormattedSubmission, }; #[get("/")] @@ -13,6 +14,9 @@ async fn get_main( tz: Data<Tz>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } #[derive(Serialize)] struct Context { @@ -22,7 +26,11 @@ async fn get_main( } let mut connection = pool.get()?; - let submissions = submission::get_submissions(&mut connection)?; + let submissions = if !logged_user.is_admin { + submission::get_submissions_user(&mut connection, logged_user.id)? + } else { + submission::get_submissions(&mut connection)? + }; render( &hb, @@ -30,7 +38,11 @@ async fn get_main( &Context { base, contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?, - submissions: get_formatted_submissions(&tz, &submissions), + submissions: if logged_user.is_admin { + get_formatted_submissions(&tz, &submissions) + } else { + vec![] + }, }, ) } diff --git a/src/pages/get_me.rs b/src/pages/get_me.rs index f1b8705963066ac28a3a3e9782240a14c9920e23..d5a997c7d20348c6a0b20b550ce3f859d7a388ae 100644 --- a/src/pages/get_me.rs +++ b/src/pages/get_me.rs @@ -1,8 +1,12 @@ +use crate::pages::assert_not_single_contest; use crate::pages::prelude::*; #[get("/me")] async fn get_me(base: BaseContext, identity: Identity, hb: Data<Handlebars<'_>>) -> PageResult { - require_identity(&identity)?; + let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } #[derive(Serialize)] struct Context { base: BaseContext, diff --git a/src/pages/get_printing_task_contents.rs b/src/pages/get_printing_task_contents.rs new file mode 100644 index 0000000000000000000000000000000000000000..7abb59905563d2270d9fe5a47be1d75f9480204e --- /dev/null +++ b/src/pages/get_printing_task_contents.rs @@ -0,0 +1,59 @@ +use crate::models::printing_task; +use crate::pages::prelude::*; +use actix_web::{http::header::ContentType, HttpResponse}; + +#[get("/printing-tasks/{uuid}/contents")] +async fn get_printing_task_contents( + identity: Identity, + pool: Data<DbPool>, + path: Path<(String,)>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (printing_task_uuid,) = path.into_inner(); + let mut connection = pool.get()?; + + let (printing_task, user) = + printing_task::get_printing_task_by_uuid(&mut connection, printing_task_uuid)?; + + if user.id != logged_user.id && !logged_user.is_admin { + return Err(PageError::Forbidden( + "Não é possível acessar uma tarefa de impressão de outro usuário".into(), + )); + } + + let user_full_name = user.full_name.unwrap_or("".into()); + Ok(HttpResponse::Ok() + .content_type(ContentType::plaintext()) + .body(format!( + r#"///// {} -- {} -- {} +///// {} -- {} -- {} +///// {} -- {} -- {} + + +{} + +///// {} -- {} -- {} +///// {} -- {} -- {} +///// {} -- {} -- {} +"#, + user.name, + user_full_name, + user.name, + user.name, + user_full_name, + user.name, + user.name, + user_full_name, + user.name, + printing_task.contents, + user.name, + user_full_name, + user.name, + user.name, + user_full_name, + user.name, + user.name, + user_full_name, + user.name + ))) +} diff --git a/src/pages/get_problem_by_id_assets.rs b/src/pages/get_problem_by_id_assets.rs index 60380ccb305a6e2b8e502871accb506a93e84ff5..2d8924bbe0352ee412d346af95916b8eb55142ea 100644 --- a/src/pages/get_problem_by_id_assets.rs +++ b/src/pages/get_problem_by_id_assets.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; use actix_files::NamedFile; +use actix_web::http::header::ContentDisposition; +use actix_web::http::header::DispositionParam; use crate::models::{contest, problem}; use crate::pages::assert_contest_not_started; @@ -18,9 +20,20 @@ async fn get_problem_by_id_assets( let problem = problem::get_problem_by_contest_id_metadata(&mut connection, problem_id)?; let contest = contest::get_contest_by_id(&mut connection, problem.contest_id)?; assert_contest_not_started(&logged_user, &contest)?; - let file_path = PathBuf::from("/data/") - .join(problem.id) - .join("statements/.html/portuguese/") - .join(asset_filename); - Ok(NamedFile::open(file_path)?) + let file_path_prefix = PathBuf::from("/data/").join(problem.id).join("statement"); + let file_path = file_path_prefix.join(asset_filename).canonicalize()?; + if !file_path.starts_with(file_path_prefix) { + return Err(PageError::Forbidden( + "Caminho para o problema deve estar dentro do repositório".into(), + )); + } + let extension = file_path.extension().unwrap().to_str().unwrap(); + let mut named_file = NamedFile::open(file_path.clone())?; + let content_disposition = named_file.content_disposition().clone(); + named_file = named_file.set_content_disposition(ContentDisposition { + disposition: content_disposition.disposition, + parameters: vec![DispositionParam::Filename(problem.label + "." + extension)], + }); + println!("{}", named_file.content_disposition()); + Ok(named_file) } diff --git a/src/pages/get_problems.rs b/src/pages/get_problems.rs index e07654ed9beb15008425bd98b49f3b2ff26e4187..3be948b57a98f9e06df45f72b21c8e63196bbf11 100644 --- a/src/pages/get_problems.rs +++ b/src/pages/get_problems.rs @@ -1,6 +1,6 @@ use crate::models::problem; use crate::pages::prelude::*; -use crate::pages::FormattedProblemByContestWithScore; +use crate::pages::{assert_not_single_contest, FormattedProblemByContestWithScore}; #[get("/problems/")] pub async fn get_problems( @@ -10,6 +10,9 @@ pub async fn get_problems( hb: Data<Handlebars<'_>>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } #[derive(Serialize)] struct Context { @@ -18,7 +21,17 @@ pub async fn get_problems( } let mut connection = pool.get()?; - let problems = problem::get_problems_user_with_score(&mut connection, logged_user.id)?; + let problems = if logged_user.is_admin { + problem::get_problems_user_with_score_without_blind_and_frozen( + &mut connection, + logged_user.id, + )? + } else { + problem::get_problems_user_with_score_with_blind_and_frozen( + &mut connection, + logged_user.id, + )? + }; render( &hb, @@ -30,8 +43,12 @@ pub async fn get_problems( .map(|p| FormattedProblemByContestWithScore { first_ac_submission_time: "".into(), first_ac_submission_minutes: None, - failed_submissions: p.failed_submissions, + failed_submissions: 0, + first_solve: false, + submission_count: 0, + last_failed_submission_verdict: p.last_failed_submission_verdict.clone(), id: p.id, + balloon_color: None, name: p.name.clone(), label: p.label.clone(), memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024, diff --git a/src/pages/get_repositories.rs b/src/pages/get_repositories.rs new file mode 100644 index 0000000000000000000000000000000000000000..187cc39596b18c7f645cff481948ebfc76203e8c --- /dev/null +++ b/src/pages/get_repositories.rs @@ -0,0 +1,50 @@ +use crate::models::repository; +use crate::pages::prelude::*; + +#[get("/repositories/")] +async fn get_repositories( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + + #[derive(Serialize)] + struct FormattedRepository { + pub id: i32, + pub name: String, + pub remote_url: String, + pub creation_instant: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + repositories: Vec<FormattedRepository>, + } + + let mut connection = pool.get()?; + render( + &hb, + "repositories", + &Context { + base, + repositories: repository::get_repositories(&mut connection)? + .iter() + .map(|r| FormattedRepository { + id: r.id, + name: r.name.clone(), + remote_url: r.remote_url.clone(), + creation_instant: format_utc_date_time(&tz, r.creation_instant), + }) + .collect(), + }, + ) +} diff --git a/src/pages/get_repository_by_id.rs b/src/pages/get_repository_by_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..fc05c2ee8c921f60dcef6cca27f9b1577c356b74 --- /dev/null +++ b/src/pages/get_repository_by_id.rs @@ -0,0 +1,188 @@ +use crate::models::repository; +use crate::pages::prelude::*; +use std::fs; +use toml; +use walkdir::WalkDir; + +#[derive(Deserialize)] +struct BlemConfig { + authors: Vec<String>, + time_limit_ms: i32, + memory_limit_kb: Option<i32>, + br: LanguageConfig, +} + +#[derive(Deserialize)] +struct LanguageConfig { + name: String, +} + +#[derive(Deserialize)] +struct ContestProblemConfig { + label: String, + path: String, + name: String, +} + +#[derive(Deserialize)] +struct ContestConfig { + languages: Vec<String>, + br: LanguageConfig, + problems: Vec<ContestProblemConfig>, +} + +#[get("/repositories/{id}")] +pub async fn get_repository_by_id( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + use std::path::Path; + + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (repository_id,) = path.into_inner(); + + #[derive(Serialize)] + struct FormattedRepository { + pub id: i32, + pub name: String, + pub remote_url: String, + pub creation_instant: String, + } + + #[derive(Serialize)] + struct RepositoryProblem { + pub name: String, + pub authors: Vec<String>, + pub time_limit_ms: i32, + pub memory_limit_kb: i32, + pub path: String, + } + + #[derive(Serialize)] + struct RepositoryContestProblem { + pub label: String, + pub name: String, + pub path: String, + pub time_limit_ms: Option<i32>, + pub authors: Vec<String>, + pub last_commit: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + repository: FormattedRepository, + problems: Vec<RepositoryProblem>, + contest_name: Option<String>, + contest_problems: Vec<RepositoryContestProblem>, + } + + let mut connection = pool.get()?; + let repository = repository::get_repository_by_id(&mut connection, repository_id)?; + let git_repository = + git2::Repository::open(Path::new(&format!("/data/{}/", repository.path))).unwrap(); + let head_commit = git_repository.head().unwrap(); + let head_commit_short_id = head_commit + .peel_to_commit() + .unwrap() + .as_object() + .short_id() + .unwrap(); + let head_commit_shorthand = head_commit_short_id.as_str().unwrap(); + + let mut contest_name = None; + let mut contest_problems = Vec::new(); + let maybe_contest_toml_contents = + fs::read_to_string(format!("/data/{}/contest.toml", repository.path)); + if let Ok(contest_toml_contents) = maybe_contest_toml_contents { + let maybe_contest_config = toml::from_str::<ContestConfig>(&contest_toml_contents); + if let Ok(contest_config) = maybe_contest_config { + contest_name = Some(contest_config.br.name); + contest_problems = contest_config + .problems + .into_iter() + .map(|c| RepositoryContestProblem { + label: c.label, + name: c.name, + path: c.path, + time_limit_ms: None, + last_commit: head_commit_shorthand.into(), + authors: vec![], + }) + .collect(); + } + } + + let mut problems = Vec::new(); + for entry in WalkDir::new(format!("/data/{}", repository.path)) + .into_iter() + .filter_entry(|e| e.file_name().to_str().map(|s| s != "blem").unwrap_or(false)) + .filter_map(|e| e.ok().filter(|e| e.file_name() == "blem.toml")) + { + let maybe_blem_config = toml::from_str::<BlemConfig>(&fs::read_to_string(entry.path())?); + let path = entry + .path() + .strip_prefix(format!("/data/{}", repository.path)) + .unwrap() + .parent() + .unwrap() + .to_str() + .unwrap() + .into(); + if let Ok(blem_config) = maybe_blem_config { + let mut found = false; + for contest_problem in &mut contest_problems { + if contest_problem.path == path { + contest_problem.name = + format!("{0} ({1})", contest_problem.name, blem_config.br.name); + contest_problem.time_limit_ms = Some(blem_config.time_limit_ms); + contest_problem.authors = blem_config.authors.clone(); + found = true; + } + } + if !found { + problems.push(RepositoryProblem { + name: blem_config.br.name, + time_limit_ms: blem_config.time_limit_ms, + memory_limit_kb: blem_config.memory_limit_kb.unwrap_or(204800), + authors: blem_config.authors, + path, + }); + } + } else { + problems.push(RepositoryProblem { + name: "???".to_string(), + time_limit_ms: 0, + memory_limit_kb: 0, + authors: vec![], + path, + }); + } + } + + render( + &hb, + "repository", + &Context { + base, + problems, + contest_name, + contest_problems, + repository: FormattedRepository { + id: repository.id, + name: repository.name.clone(), + remote_url: repository.remote_url.clone(), + creation_instant: format_utc_date_time(&tz, repository.creation_instant), + }, + }, + ) +} diff --git a/src/pages/get_repository_problem_by_path.rs b/src/pages/get_repository_problem_by_path.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa914459004fbd1d51dcf5d47491ba7f90323444 --- /dev/null +++ b/src/pages/get_repository_problem_by_path.rs @@ -0,0 +1,99 @@ +use crate::models::repository; +use crate::pages::prelude::*; +use std::fs; +use toml; + +#[derive(Deserialize)] +struct BlemConfig { + authors: Vec<String>, + time_limit_ms: i32, + br: LanguageConfig, +} + +#[derive(Deserialize)] +struct LanguageConfig { + name: String, +} + +#[get("/repositories/{id}/problems-by-path/{path:.*}")] +pub async fn get_repository_problem_by_path( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32, String)>, + tz: Data<Tz>, +) -> PageResult { + use std::path::Path; + + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (repository_id, problem_path) = path.into_inner(); + + #[derive(Serialize)] + struct FormattedRepository { + pub id: i32, + pub name: String, + pub remote_url: String, + pub creation_instant: String, + } + + #[derive(Serialize)] + struct RepositoryProblem { + pub name: String, + pub authors: Vec<String>, + pub time_limit_ms: i32, + pub path: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + repository: FormattedRepository, + problems: Vec<RepositoryProblem>, + } + + let mut connection = pool.get()?; + let repository = repository::get_repository_by_id(&mut connection, repository_id)?; + + let path = Path::new(&format!("/data/{}", repository.path)) + .join(problem_path) + .canonicalize() + .unwrap(); + if !path.starts_with(format!("/data/{}", repository.path)) { + return Err(PageError::Forbidden( + "Caminho para o problema deve estar dentro do repositório".into(), + )); + } + + let maybe_blem_config = + toml::from_str::<BlemConfig>(&fs::read_to_string(path.join("blem.toml"))?); + let mut problems = Vec::new(); + if let Ok(blem_config) = maybe_blem_config { + problems.push(RepositoryProblem { + name: blem_config.br.name, + time_limit_ms: blem_config.time_limit_ms, + authors: blem_config.authors, + path: path.to_str().unwrap().into(), + }); + } + + render( + &hb, + "repository", + &Context { + base, + problems, + repository: FormattedRepository { + id: repository.id, + name: repository.name.clone(), + remote_url: repository.remote_url.clone(), + creation_instant: format_utc_date_time(&tz, repository.creation_instant), + }, + }, + ) +} diff --git a/src/pages/get_single_contest_backup.rs b/src/pages/get_single_contest_backup.rs new file mode 100644 index 0000000000000000000000000000000000000000..25ffbde0a0e1b73b0565b51dfb1f98b852837e3c --- /dev/null +++ b/src/pages/get_single_contest_backup.rs @@ -0,0 +1,65 @@ +use crate::models::{backup, contest, submission}; +use crate::pages::prelude::*; +use crate::pages::{ + assert_single_contest_not_started, get_formatted_contest, + get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission, +}; + +#[get("/single-contest/{id}/backup")] +async fn get_single_contest_backup( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct FormattedBackup { + pub uuid: String, + pub creation_instant: String, + pub filename: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + backups: Vec<FormattedBackup>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) { + return redirect; + } + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; + let backups = backup::get_backups_by_contest_user(&mut connection, logged_user.id, contest_id)?; + + render( + &hb, + "single_backup", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + backups: backups + .into_iter() + .map(|b| FormattedBackup { + uuid: b.uuid, + creation_instant: format_utc_date_time(&tz, b.creation_instant), + filename: b.filename, + }) + .collect(), + submissions: get_formatted_submissions_with_blind_ascending( + &tz, + &contest, + &submissions, + ), + }, + ) +} diff --git a/src/pages/get_single_contest_clarifications.rs b/src/pages/get_single_contest_clarifications.rs new file mode 100644 index 0000000000000000000000000000000000000000..090f03ac8231563833fbf51ff8358944597d0ae1 --- /dev/null +++ b/src/pages/get_single_contest_clarifications.rs @@ -0,0 +1,102 @@ +use crate::models::{clarification, contest, problem, submission}; +use crate::pages::prelude::*; +use crate::pages::{ + assert_single_contest_not_started, get_formatted_contest, + get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission, +}; + +#[derive(Serialize)] +struct FormattedClarification { + pub uuid: String, + pub question: String, + pub question_instant: String, + pub answer: Option<String>, + pub answer_instant: Option<String>, + pub answer_user_id: Option<i32>, + pub answer_to_all: bool, + pub contest_problem_id: Option<i32>, + pub problem_label: Option<String>, + pub contest_id: i32, + pub user_id: i32, + pub user_name: String, +} + +#[get("/single-contest/{id}/clarifications")] +async fn get_single_contest_clarifications( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct FormattedProblem { + pub id: i32, + pub label: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + problems: Vec<FormattedProblem>, + clarifications: Vec<FormattedClarification>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) { + return redirect; + } + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; + let clarifications = clarification::get_clarifications_by_contest_user( + &mut connection, + logged_user.id, + contest_id, + )?; + let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?; + + render( + &hb, + "single_clarifications", + &Context { + base, + problems: problems + .iter() + .map(|p| FormattedProblem { + id: p.id, + label: p.label.clone(), + }) + .collect(), + contest: get_formatted_contest(&tz, &contest), + clarifications: clarifications + .into_iter() + .map(|(c, u, problem_label)| FormattedClarification { + uuid: c.uuid, + question: c.question, + question_instant: format_utc_date_time(&tz, c.question_instant), + answer: c.answer, + answer_instant: c.answer_instant.map(|i| format_utc_date_time(&tz, i)), + answer_user_id: c.answer_user_id, + answer_to_all: c.answer_to_all, + contest_problem_id: c.contest_problem_id, + problem_label, + contest_id: c.contest_id, + user_id: c.user_id, + user_name: u.name, + }) + .collect(), + submissions: get_formatted_submissions_with_blind_ascending( + &tz, + &contest, + &submissions, + ), + }, + ) +} diff --git a/src/pages/get_single_contest_problems.rs b/src/pages/get_single_contest_problems.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec6161e06a844179f9f5ec89ac3896c8a4c8bae2 --- /dev/null +++ b/src/pages/get_single_contest_problems.rs @@ -0,0 +1,60 @@ +use crate::models::{contest, problem, submission}; +use crate::pages::prelude::*; +use crate::pages::{ + assert_single_contest_not_started, get_formatted_contest, + get_formatted_problem_by_contest_with_score, get_formatted_submissions_with_blind_ascending, + FormattedContest, FormattedProblemByContestWithScore, FormattedSubmission, +}; + +#[get("/single-contest/{id}/problems")] +pub async fn get_single_contest_problems( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + problems: Vec<FormattedProblemByContestWithScore>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) { + return redirect; + } + + let problems = problem::get_problems_user_by_contest_id_with_score_with_blind_and_frozen( + &mut connection, + logged_user.id, + contest_id, + )?; + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; + + render( + &hb, + "single_problems", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + problems: problems + .iter() + .map(|p| get_formatted_problem_by_contest_with_score(p, &contest)) + .collect(), + submissions: get_formatted_submissions_with_blind_ascending( + &tz, + &contest, + &submissions, + ), + }, + ) +} diff --git a/src/pages/get_single_contest_runs.rs b/src/pages/get_single_contest_runs.rs new file mode 100644 index 0000000000000000000000000000000000000000..2f679d5c30092ab05b59665383499176cc124454 --- /dev/null +++ b/src/pages/get_single_contest_runs.rs @@ -0,0 +1,82 @@ +use crate::models::contest; +use crate::models::problem; +use crate::models::submission; +use crate::pages::get_editor::{get_formatted_languages, FormattedLanguage}; +use crate::pages::prelude::*; +use crate::pages::{ + assert_single_contest_not_started, get_formatted_contest, + get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission, +}; +use crate::Language; + +#[derive(Deserialize)] +struct ContestProblemHint { + contest_problem_id: Option<i32>, +} + +#[get("/single-contest/{id}/runs")] +async fn get_single_contest_runs( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + languages: Data<Arc<DashMap<String, Language>>>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + contest_problem_hint: Query<ContestProblemHint>, + session: Session, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) { + return redirect; + } + + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; + + let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?; + + #[derive(Serialize)] + struct FormattedProblem { + pub id: i32, + pub label: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + problems: Vec<FormattedProblem>, + submissions: Vec<FormattedSubmission>, + language: Option<String>, + languages: Vec<FormattedLanguage>, + contest_problem_id_hint: Option<i32>, + } + + render( + &hb, + "single_runs", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + problems: problems + .iter() + .map(|p| FormattedProblem { + id: p.id, + label: p.label.clone(), + }) + .collect(), + submissions: get_formatted_submissions_with_blind_ascending( + &tz, + &contest, + &submissions, + ), + language: session.get("language")?, + languages: get_formatted_languages(&languages, true), + contest_problem_id_hint: contest_problem_hint.contest_problem_id, + }, + ) +} diff --git a/src/pages/get_single_contest_score.rs b/src/pages/get_single_contest_score.rs new file mode 100644 index 0000000000000000000000000000000000000000..7b48d30e03de1bd246a78a6a9920f44e9969e077 --- /dev/null +++ b/src/pages/get_single_contest_score.rs @@ -0,0 +1,143 @@ +use itertools::Itertools; +use std::collections::HashSet; +use chrono::Local; + +use crate::models::{contest, problem, submission, user}; +use crate::pages::prelude::*; +use crate::pages::{ + assert_single_contest_not_started, get_formatted_contest, + get_formatted_problem_by_contest_with_score, get_formatted_submissions_with_blind_ascending, + FormattedContest, FormattedProblemByContestWithScore, FormattedSubmission, +}; + +#[get("/single-contest/{id}/score")] +async fn get_single_contest_score( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct Score { + pub user_name: Option<String>, + pub user_full_name: Option<String>, + pub problems: Vec<FormattedProblemByContestWithScore>, + pub solved_count: i64, + pub penalty: i64, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + scores: Vec<Score>, + submissions: Vec<FormattedSubmission>, + frozen: bool, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) { + return redirect; + } + let scores = + problem::get_problems_by_contest_id_with_score_with_frozen(&mut connection, contest_id)?; + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; + let users_contest = user::get_users_by_single_contest_id(&mut connection, contest_id)?; + let users_contest_users: HashSet<String> = + HashSet::from_iter(users_contest.iter().map(|e| e.name.clone())); + let mut scores: Vec<_> = scores.into_iter().group_by(|e| + (e.user_name.as_ref().cloned(), e.user_full_name.clone()) + ).into_iter().filter_map(|((user_name, user_full_name), problems)| { + let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect(); + if match user_name { + None => false, + Some(ref user_name) => !users_contest_users.contains(user_name) + } { + return None + } + Some(Score { + user_name, + user_full_name, + solved_count: i64::try_from(problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()).count()).unwrap(), + penalty: problems.iter().filter(|p| !p.first_ac_submission_time.is_empty()) + .map(|p| match p.first_ac_submission_minutes { + Some(x) if x >= 0 => x, + _ => 0, + } + i64::from(20*p.failed_submissions)) + .sum(), + problems + }) + }).collect(); + scores.sort_by(|a, b| { + ( + a.user_name.is_some(), + -a.solved_count, + a.penalty, + &a.user_name, + ) + .cmp(&( + b.user_name.is_some(), + -b.solved_count, + b.penalty, + &b.user_name, + )) + }); + + let scored_users: HashSet<String> = + HashSet::from_iter(scores.iter().filter_map(|e| e.user_name.clone())); + for user in users_contest { + if !scored_users.contains(&user.name) { + scores.push(Score { + user_name: Some(user.name), + user_full_name: user.full_name, + solved_count: 0, + penalty: 0, + problems: scores[0] + .problems + .iter() + .map(|e| FormattedProblemByContestWithScore { + first_ac_submission_time: e.first_ac_submission_time.clone(), + first_ac_submission_minutes: e.first_ac_submission_minutes, + first_solve: false, + user_accepted_count: e.user_accepted_count, + failed_submissions: e.failed_submissions, + submission_count: e.submission_count, + last_failed_submission_verdict: None, + balloon_color: e.balloon_color.clone(), + id: e.id, + name: e.name.clone(), + label: e.label.clone(), + memory_limit_mib: e.memory_limit_mib, + time_limit: e.time_limit.clone(), + }) + .collect(), + }); + } + } + + render( + &hb, + "single_score", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + scores, + submissions: get_formatted_submissions_with_blind_ascending( + &tz, + &contest, + &submissions, + ), + frozen: match contest.frozen_scoreboard_instant { + Some(frozen_scoreboard_instant) => Local::now().naive_utc() >= frozen_scoreboard_instant, + None => false + } + }, + ) +} diff --git a/src/pages/get_single_contest_tasks.rs b/src/pages/get_single_contest_tasks.rs new file mode 100644 index 0000000000000000000000000000000000000000..72b0b757d45529a5453558a60742ffa81b18a88b --- /dev/null +++ b/src/pages/get_single_contest_tasks.rs @@ -0,0 +1,71 @@ +use crate::models::{contest, printing_task, submission}; +use crate::pages::prelude::*; +use crate::pages::{ + assert_single_contest_not_started, get_formatted_contest, + get_formatted_submissions_with_blind_ascending, FormattedContest, FormattedSubmission, +}; + +#[get("/single-contest/{id}/tasks")] +async fn get_single_contest_tasks( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct FormattedPrintingTask { + pub uuid: String, + pub creation_instant: String, + pub status: String, + pub done_instant: Option<String>, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + printing_tasks: Vec<FormattedPrintingTask>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + if let Some(redirect) = assert_single_contest_not_started(&logged_user, &contest) { + return redirect; + } + let submissions = + submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; + let printing_tasks = printing_task::get_printing_tasks_by_contest_user( + &mut connection, + logged_user.id, + contest_id, + )?; + + render( + &hb, + "single_tasks", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + printing_tasks: printing_tasks + .into_iter() + .map(|p| FormattedPrintingTask { + uuid: p.uuid, + status: p.status, + creation_instant: format_utc_date_time(&tz, p.creation_instant), + done_instant: p.done_instant.map(|i| format_utc_date_time(&tz, i)), + }) + .collect(), + submissions: get_formatted_submissions_with_blind_ascending( + &tz, + &contest, + &submissions, + ), + }, + ) +} diff --git a/src/pages/get_single_contest_waiting.rs b/src/pages/get_single_contest_waiting.rs new file mode 100644 index 0000000000000000000000000000000000000000..a975d6eeb95177c91d8d47d5111817d4d712640f --- /dev/null +++ b/src/pages/get_single_contest_waiting.rs @@ -0,0 +1,68 @@ +use crate::models::contest; +use crate::pages::get_formatted_contest; +use crate::pages::prelude::*; +use actix_web::http::header; +use actix_web::http::header::HeaderValue; +use chrono::Local; +use std::env; + +#[get("/single-contest/{id}/waiting")] +pub async fn get_single_contest_waiting( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + + if contest + .start_instant + .map_or(true, |s| s < Local::now().naive_utc()) + { + return Ok(HttpResponse::SeeOther() + .append_header(( + header::LOCATION, + HeaderValue::from_str(&format!( + "{}/single-contest/{}/problems", + env::var("BASE_URL").expect("BASE_URL environment variable is not set"), + contest_id + )) + .unwrap(), + )) + .finish()); + } + + let start_instant = contest.start_instant.unwrap(); + let f = get_formatted_contest(&tz, &contest); + + #[derive(Serialize)] + struct FormattedContest { + start_instant: String, + id: i32, + contest_status: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + } + + render( + &hb, + "single_waiting", + &Context { + base, + contest: FormattedContest { + id: contest_id, + start_instant: start_instant.format("%Y-%m-%dT%H:%M:%SZ").to_string(), + contest_status: f.contest_status, + }, + }, + ) +} diff --git a/src/pages/get_submission_source.rs b/src/pages/get_submission_source.rs new file mode 100644 index 0000000000000000000000000000000000000000..9998d3a14df01740bb1637ce398d81dee6ce1cb2 --- /dev/null +++ b/src/pages/get_submission_source.rs @@ -0,0 +1,27 @@ +use crate::models::submission; +use crate::pages::prelude::*; +use actix_web::{http::header::ContentType, HttpResponse}; + +#[get("/submissions/{uuid}/source")] +async fn get_submission_source( + identity: Identity, + pool: Data<DbPool>, + path: Path<(String,)>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (submission_uuid,) = path.into_inner(); + let mut connection = pool.get()?; + + let (submission, user, _, _) = + submission::get_submission_by_uuid(&mut connection, submission_uuid)?; + + if user.id != logged_user.id && !logged_user.is_admin { + return Err(PageError::Forbidden( + "Não é possível acessar uma submissão de outro usuário".into(), + )); + } + + Ok(HttpResponse::Ok() + .content_type(ContentType::plaintext()) + .body(submission.source_text)) +} diff --git a/src/pages/get_submissions.rs b/src/pages/get_submissions.rs index 2fbaa90e248c0d03119f33a38a61bc25a04ea7d0..698cc5420caa34a1a57ae6863a2e247c1912d971 100644 --- a/src/pages/get_submissions.rs +++ b/src/pages/get_submissions.rs @@ -2,7 +2,7 @@ use submission::{ContestProblem, Submission}; use crate::models::submission; use crate::pages::prelude::*; -use crate::pages::{get_formatted_submissions, FormattedSubmission}; +use crate::pages::{assert_not_single_contest, get_formatted_submissions, FormattedSubmission}; use crate::user::User; pub fn render_submissions( @@ -16,7 +16,6 @@ pub fn render_submissions( base: BaseContext, submissions: Vec<FormattedSubmission>, } - render( hb, "submissions", @@ -36,15 +35,19 @@ async fn get_submissions( tz: Data<Tz>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } let mut connection = pool.get()?; render_submissions( base, &hb, &tz, - if logged_user.is_admin { - submission::get_submissions(&mut connection)? - } else { - submission::get_submissions_user(&mut connection, logged_user.id)? - }, + submission::get_submissions(&mut connection)?, ) } diff --git a/src/pages/get_submissions_by_contest_id.rs b/src/pages/get_submissions_by_contest_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..07d7162880f33ed27ceafdc90086efeaaa6213ba --- /dev/null +++ b/src/pages/get_submissions_by_contest_id.rs @@ -0,0 +1,29 @@ +use crate::models::submission; +use crate::pages::get_submissions::render_submissions; +use crate::pages::prelude::*; + +#[get("/submissions/contests/{id}")] +async fn get_submissions_by_contest_id( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32,)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + + render_submissions( + base, + &hb, + &tz, + submission::get_submissions_by_contest(&mut connection, contest_id)?, + ) +} diff --git a/src/pages/get_submissions_by_contest_id_problem_label.rs b/src/pages/get_submissions_by_contest_id_problem_label.rs new file mode 100644 index 0000000000000000000000000000000000000000..f5c07279a534b87dc1697e8b45483035446e8803 --- /dev/null +++ b/src/pages/get_submissions_by_contest_id_problem_label.rs @@ -0,0 +1,32 @@ +use crate::models::submission; +use crate::pages::get_submissions::render_submissions; +use crate::pages::prelude::*; + +#[get("/submissions/contests/{id}/{label}")] +async fn get_submissions_by_contest_id_problem_label( + base: BaseContext, + identity: Identity, + pool: Data<DbPool>, + hb: Data<Handlebars<'_>>, + path: Path<(i32, String)>, + tz: Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let mut connection = pool.get()?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (contest_id, problem_label) = path.into_inner(); + render_submissions( + base, + &hb, + &tz, + submission::get_submissions_by_contest_problem( + &mut connection, + contest_id, + &problem_label, + )?, + ) +} diff --git a/src/pages/get_submissions_me.rs b/src/pages/get_submissions_me.rs index eec35a1d809da6beb42dadd6dba98f660283fe00..d28086c3c32131229c34c0b7f3ac01b6d7547543 100644 --- a/src/pages/get_submissions_me.rs +++ b/src/pages/get_submissions_me.rs @@ -1,4 +1,5 @@ use crate::models::submission; +use crate::pages::assert_not_single_contest; use crate::pages::get_submissions::render_submissions; use crate::pages::prelude::*; @@ -11,6 +12,9 @@ async fn get_submissions_me( tz: Data<Tz>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } let mut connection = pool.get()?; render_submissions( base, diff --git a/src/pages/get_submissions_me_by_contest_id.rs b/src/pages/get_submissions_me_by_contest_id.rs index ca136b2eae630ab5eea7b687e9b4ee09e9688e73..561a76ed815488ea64e759ad323b691ff7ee25a0 100644 --- a/src/pages/get_submissions_me_by_contest_id.rs +++ b/src/pages/get_submissions_me_by_contest_id.rs @@ -1,4 +1,5 @@ use crate::models::submission; +use crate::pages::assert_not_single_contest; use crate::pages::get_submissions::render_submissions; use crate::pages::prelude::*; @@ -12,6 +13,9 @@ async fn get_submissions_me_by_contest_id( tz: Data<Tz>, ) -> PageResult { let logged_user = require_identity(&identity)?; + if let Some(redirect) = assert_not_single_contest(&logged_user) { + return redirect; + } let (contest_id,) = path.into_inner(); let mut connection = pool.get()?; diff --git a/src/pages/impersonate_user.rs b/src/pages/impersonate_user.rs index 887e32f634d1e845abdbb2299622c3099d3f92c3..f0056145d2993ac1fe343ee0c2ca2a40e7e1b432 100644 --- a/src/pages/impersonate_user.rs +++ b/src/pages/impersonate_user.rs @@ -30,6 +30,8 @@ async fn impersonate_user( serde_json::to_string(&LoggedUser { id: user.id, name: (&user.name).into(), + full_name: user.full_name, + single_contest_id: user.single_contest_id, is_admin: user.is_admin, }) .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?, diff --git a/src/pages/import_repository.rs b/src/pages/import_repository.rs new file mode 100644 index 0000000000000000000000000000000000000000..6293f22e003a97157b059b7eb7ea2978ccca426a --- /dev/null +++ b/src/pages/import_repository.rs @@ -0,0 +1,73 @@ +use crate::models::repository; +use crate::pages::prelude::*; +use chrono::Local; +use git2::{CertificateCheckStatus, Cred, RemoteCallbacks}; +use itertools::Itertools; +use std::path::Path; + +#[derive(Serialize, Deserialize)] +struct RepositoryForm { + name: String, + remote_url: String, +} + +#[post("/repositories/")] +async fn import_repository( + identity: Identity, + pool: Data<DbPool>, + form: Form<RepositoryForm>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let mut connection = pool.get()?; + + let (_, url) = form.remote_url.split("@").collect_tuple().unwrap(); + let path = url.replace(':', "+").replace('/', "."); + + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(|_url, username_from_url, _allowed_types| { + Cred::ssh_key( + username_from_url.unwrap(), + None, + Path::new("/data/id_ed25519_git"), + None, + ) + }); + callbacks.certificate_check(|_cert, _hostname| { + return Ok(CertificateCheckStatus::CertificateOk); + }); + + let mut fo = git2::FetchOptions::new(); + fo.remote_callbacks(callbacks); + + let mut builder = git2::build::RepoBuilder::new(); + builder.fetch_options(fo); + + if let Err(e) = builder.clone(&form.remote_url, Path::new(&format!("/data/{}/", path))) { + return Err(PageError::Validation(format!( + "Repositório {} importado com erro {}", + form.remote_url, e + ))); + } + + repository::insert_repository( + &mut connection, + repository::NewRepository { + name: form.name.clone(), + path, + remote_url: form.remote_url.clone(), + creation_user_id: logged_user.id, + creation_instant: Local::now().naive_utc(), + }, + )?; + + Ok(redirect_to_referer( + format!("Repositório {} importado com sucesso!", form.remote_url), + &request, + )) +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs index b3f00885eb3ce7fd7842066ab1c13e7b7263a624..ef844ddbef01e715891374d014e17f82d76c18d7 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -5,7 +5,8 @@ use actix_web::http::header::HeaderValue; use actix_web::http::{header, StatusCode}; use actix_web::middleware::ErrorHandlerResponse; use actix_web_flash_messages::FlashMessage; -use chrono::{Local, NaiveDateTime, TimeZone}; +use chrono::Local; +use chrono::TimeDelta; use chrono_tz::Tz; use contest::{Contest, ContestWithAcs}; use diesel::pg::PgConnection; @@ -15,32 +16,62 @@ use submission::{ContestProblem, Submission}; use crate::models::{contest, problem, submission}; use crate::user::User; +pub mod get_contest_webcast_by_id; pub mod change_password; +pub mod create_backup; +pub mod create_clarification; pub mod create_contest; +pub mod create_contest_submission; +pub mod create_printing_task; pub mod create_submission; pub mod create_user; pub mod get_about; +pub mod get_backup_contents; pub mod get_contest_by_id; pub mod get_contest_problem_by_id_label; pub mod get_contest_scoreboard_by_id; +pub mod get_contest_clarifications_by_id; +pub mod get_contest_tasks_by_id; +pub mod patch_clarification; pub mod get_contests; pub mod get_editor; pub mod get_login; pub mod get_main; pub mod get_me; +pub mod get_printing_task_contents; +pub mod post_printing_task_done; +pub mod post_submission_balloon_delivered; pub mod get_problem_by_id_assets; pub mod get_problems; +pub mod get_repositories; +pub mod get_repository_by_id; +pub mod get_repository_problem_by_path; +pub mod get_single_contest_backup; +pub mod get_single_contest_clarifications; +pub mod get_single_contest_problems; +pub mod get_single_contest_runs; +pub mod get_single_contest_score; +pub mod get_single_contest_tasks; +pub mod get_single_contest_waiting; pub mod get_submission; +pub mod get_submission_source; pub mod get_submissions; +pub mod get_submissions_by_contest_id; +pub mod get_submissions_by_contest_id_problem_label; pub mod get_submissions_me; pub mod get_submissions_me_by_contest_id; pub mod get_submissions_me_by_contest_id_problem_label; pub mod impersonate_user; +pub mod import_repository; +pub mod patch_contest; +pub mod patch_contest_problem; pub mod post_login; pub mod post_logout; pub mod prelude; pub mod rejudge_submission; pub mod submission_updates; +pub mod sync_repository; +pub mod sync_repository_contest; use prelude::*; @@ -51,7 +82,7 @@ pub fn render_401<B>( res.response_mut().headers_mut().insert( header::LOCATION, HeaderValue::from_str(&format!( - "{}login", + "{}/login", &env::var("BASE_URL").expect("BASE_URL environment variable is not set") )) .unwrap(), @@ -60,8 +91,23 @@ pub fn render_401<B>( Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } -pub fn render_400<B>(res: dev::ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> { +pub fn render_400<B>( + mut res: dev::ServiceResponse<B>, +) -> actix_web::Result<ErrorHandlerResponse<B>> { FlashMessage::error("Entrada inválida").send(); + let referer = res + .request() + .headers() + .get("Referer") + .and_then(|h| h.to_str().ok()) + .map_or_else( + || env::var("BASE_URL").expect("BASE_URL environment variable is not set") + "/", + std::convert::Into::into, + ); + res.response_mut() + .headers_mut() + .insert(header::LOCATION, HeaderValue::from_str(&referer).unwrap()); + *res.response_mut().status_mut() = StatusCode::SEE_OTHER; Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) } @@ -87,8 +133,12 @@ fn format_duration(duration: chrono::Duration) -> String { struct FormattedProblemByContestWithScore { pub first_ac_submission_time: String, pub first_ac_submission_minutes: Option<i64>, + pub first_solve: bool, pub user_accepted_count: i32, + pub balloon_color: Option<String>, pub failed_submissions: i32, + pub submission_count: i32, + pub last_failed_submission_verdict: Option<String>, pub id: i32, pub name: String, pub label: String, @@ -111,13 +161,24 @@ fn get_formatted_problem_by_contest_with_score( first_ac_submission_minutes: p .first_ac_submission_instant .and_then(|t| contest.start_instant.map(|cs| (t - cs).num_minutes())), + first_solve: p.first_ac_problem_submission_instant == p.first_ac_submission_instant, failed_submissions: p.failed_submissions, + submission_count: p.failed_submissions + + match p.first_ac_submission_instant { + None => 0, + _ => 1, + }, + last_failed_submission_verdict: p + .last_failed_submission_verdict + .as_ref() + .map(|s| s.clone()), id: p.id, name: p.name.clone(), label: p.label.clone(), memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024, time_limit: format!("{}", f64::from(p.time_limit_ms) / 1000.0).replacen('.', ",", 1), user_accepted_count: p.user_accepted_count, + balloon_color: p.balloon_color.clone(), } } @@ -127,16 +188,68 @@ fn assert_contest_not_started( ) -> Result<(), PageError> { if contest .start_instant - .map_or(false, |s| s > Local::now().naive_utc()) + .map_or(false, |s| Local::now().naive_utc() < s) && !logged_user.is_admin { - return Err(PageError::Forbidden( + return Err(PageError::Validation( "Essa competição ainda não começou".into(), )); } Ok(()) } +fn assert_contest_not_over(logged_user: &LoggedUser, contest: &Contest) -> Result<(), PageError> { + if contest + .end_instant + .map_or(false, |s| s < Local::now().naive_utc()) + && !logged_user.is_admin + { + return Err(PageError::Validation("Essa competição acabou".into())); + } + Ok(()) +} + +fn assert_single_contest_not_started( + logged_user: &LoggedUser, + contest: &Contest, +) -> Option<PageResult> { + if contest + .start_instant + .map_or(false, |s| s > Local::now().naive_utc()) + && !logged_user.is_admin + { + return Some(Ok(HttpResponse::SeeOther() + .append_header(( + header::LOCATION, + HeaderValue::from_str(&format!( + "{}/single-contest/{}/waiting", + env::var("BASE_URL").expect("BASE_URL environment variable is not set"), + contest.id + )) + .unwrap(), + )) + .finish())); + } + None +} + +fn assert_not_single_contest(logged_user: &LoggedUser) -> Option<PageResult> { + if let Some(single_contest_id) = logged_user.single_contest_id { + return Some(Ok(HttpResponse::SeeOther() + .append_header(( + header::LOCATION, + HeaderValue::from_str(&format!( + "{}/single-contest/{}/problems", + env::var("BASE_URL").expect("BASE_URL environment variable is not set"), + single_contest_id + )) + .unwrap(), + )) + .finish())); + } + None +} + #[derive(Serialize)] struct FormattedSubmission { uuid: String, @@ -144,16 +257,12 @@ struct FormattedSubmission { problem_label: String, submission_instant: String, error_output: Option<String>, + language: String, user_name: String, time_ms: Option<i32>, memory: String, failed_test: Option<i32>, -} - -fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String { - tz.from_utc_datetime(&input) - .format("%d/%m/%Y %H:%M:%S") - .to_string() + balloon_color: Option<String>, } fn get_formatted_submissions( @@ -180,17 +289,72 @@ fn get_formatted_submissions( Some(k) if k < 1_024 => format!("{}KiB", k), Some(k) => format!("{}MiB", k / 1_024), }, + language: submission.language.clone(), + failed_test: submission.failed_test, + balloon_color: contest_problem.balloon_color.clone(), + }) + .collect() +} + +fn get_formatted_submissions_with_blind( + tz: &Tz, + contest: &Contest, + vec: &[(Submission, ContestProblem, User)], +) -> Vec<FormattedSubmission> { + if let None = contest.blind_scoreboard_instant { + return get_formatted_submissions(tz, vec); + } + let blind_scoreboard_instant = contest.blind_scoreboard_instant.unwrap(); + vec.iter() + .map(|(submission, contest_problem, user)| FormattedSubmission { + uuid: (&submission.uuid).into(), + verdict: if submission.submission_instant >= blind_scoreboard_instant { + "BL".into() + } else { + submission + .verdict + .as_ref() + .map_or_else(|| "WJ".into(), String::from) + }, + problem_label: contest_problem.label.clone(), + submission_instant: format_utc_date_time(tz, submission.submission_instant), + error_output: submission + .error_output + .as_ref() + .map(std::convert::Into::into), + user_name: user.name.clone(), + time_ms: submission.time_ms, + memory: match submission.memory_kib { + None | Some(0) => "".into(), + Some(k) if k < 1_024 => format!("{}KiB", k), + Some(k) => format!("{}MiB", k / 1_024), + }, + language: submission.language.clone(), failed_test: submission.failed_test, + balloon_color: contest_problem.balloon_color.clone(), }) .collect() } +fn get_formatted_submissions_with_blind_ascending( + tz: &Tz, + contest: &Contest, + vec: &[(Submission, ContestProblem, User)], +) -> Vec<FormattedSubmission> { + let mut formatted = get_formatted_submissions_with_blind(tz, contest, vec); + formatted.reverse(); + formatted +} + #[derive(Serialize)] struct FormattedContest { pub id: i32, pub name: String, pub start_instant: Option<String>, pub end_instant: Option<String>, + pub frozen_scoreboard_instant: Option<String>, + pub blind_scoreboard_instant: Option<String>, + pub contest_status: String, pub creation_instant: String, pub grade_ratio: Option<i32>, pub grade_after_ratio: Option<i32>, @@ -207,6 +371,63 @@ fn get_formatted_contest(tz: &Tz, contest: &Contest) -> FormattedContest { name: contest.name.clone(), start_instant: contest.start_instant.map(|i| format_utc_date_time(tz, i)), end_instant: contest.end_instant.map(|i| format_utc_date_time(tz, i)), + frozen_scoreboard_instant: contest + .frozen_scoreboard_instant + .map(|i| format_utc_date_time(tz, i)), + blind_scoreboard_instant: contest + .blind_scoreboard_instant + .map(|i| format_utc_date_time(tz, i)), + contest_status: match contest.start_instant { + None => "contest always running".into(), + Some(start_instant) + if Local::now().naive_utc() + TimeDelta::hours(1) < start_instant => + { + format!( + "{} hour(s) to start", + (start_instant - Local::now().naive_utc()).num_hours() + ) + } + Some(start_instant) + if Local::now().naive_utc() + TimeDelta::minutes(1) < start_instant => + { + format!( + "{} min(s) to start", + (start_instant - Local::now().naive_utc()).num_minutes() + ) + } + Some(start_instant) if Local::now().naive_utc() < start_instant => { + format!( + "{} second(s) to start", + (start_instant - Local::now().naive_utc()).num_seconds() + ) + } + Some(_) => match contest.end_instant { + None => "infinite minutes left".into(), + Some(end_instant) + if Local::now().naive_utc() + TimeDelta::hours(1) < end_instant => + { + format!( + "{} hour(s) left", + (end_instant - Local::now().naive_utc()).num_hours() + ) + } + Some(end_instant) + if Local::now().naive_utc() + TimeDelta::minutes(1) < end_instant => + { + format!( + "{} min(s) left", + (end_instant - Local::now().naive_utc()).num_minutes() + ) + } + Some(end_instant) if Local::now().naive_utc() < end_instant => { + format!( + "{} second(s) left", + (end_instant - Local::now().naive_utc()).num_seconds() + ) + } + _ => "contest not running".into(), + }, + }, creation_instant: format_utc_date_time(tz, contest.creation_instant), grade_ratio: contest.grade_ratio, grade_after_ratio: contest.grade_after_ratio, @@ -224,6 +445,13 @@ fn get_formatted_contest_acs(tz: &Tz, contest: &ContestWithAcs) -> FormattedCont name: contest.name.clone(), start_instant: contest.start_instant.map(|i| format_utc_date_time(tz, i)), end_instant: contest.end_instant.map(|i| format_utc_date_time(tz, i)), + frozen_scoreboard_instant: contest + .frozen_scoreboard_instant + .map(|i| format_utc_date_time(tz, i)), + blind_scoreboard_instant: contest + .blind_scoreboard_instant + .map(|i| format_utc_date_time(tz, i)), + contest_status: "".into(), creation_instant: format_utc_date_time(tz, contest.creation_instant), grade_ratio: contest.grade_ratio, grade_after_ratio: contest.grade_after_ratio, diff --git a/src/pages/patch_clarification.rs b/src/pages/patch_clarification.rs new file mode 100644 index 0000000000000000000000000000000000000000..c5ecadc69cad536e2325468e050ff718fa04b095 --- /dev/null +++ b/src/pages/patch_clarification.rs @@ -0,0 +1,42 @@ +use chrono::Local; + +use crate::models::clarification; +use crate::pages::prelude::*; + +#[derive(Deserialize)] +struct PatchClarificationForm { + answer: String, + answer_to_all: Option<String>, +} + +#[patch("/clarifications/{uuid}")] +pub async fn patch_clarification( + identity: Identity, + pool: Data<DbPool>, + form: Form<PatchClarificationForm>, + path: Path<(String,)>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (clarification_uuid,) = path.into_inner(); + let mut connection = pool.get()?; + + clarification::update_clarification( + &mut connection, + clarification_uuid, + form.answer.clone(), + form.answer_to_all.as_ref().map(|e| e == "on").unwrap_or(false), + logged_user.id, + Local::now().naive_utc(), + )?; + + Ok(redirect_to_referer( + "Clarificação respondida com sucesso".into(), + &request, + )) +} diff --git a/src/pages/patch_contest.rs b/src/pages/patch_contest.rs new file mode 100644 index 0000000000000000000000000000000000000000..3d192141ff216a19fa7566a4cc18ff576864370b --- /dev/null +++ b/src/pages/patch_contest.rs @@ -0,0 +1,61 @@ +use chrono::NaiveDateTime; + +use crate::models::contest; +use crate::pages::prelude::*; + +#[derive(Deserialize)] +struct PatchContestForm { + start_instant: Option<String>, + end_instant: Option<String>, + frozen_scoreboard_instant: Option<String>, + blind_scoreboard_instant: Option<String>, +} + +#[patch("/contests/{id}")] +pub async fn patch_contest( + identity: Identity, + pool: Data<DbPool>, + form: Form<PatchContestForm>, + path: Path<(i32,)>, + tz: Data<Tz>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + + contest::update_contest( + &mut connection, + contest_id, + form.start_instant + .as_ref() + .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) + .and_then(|d| d.and_local_timezone(**tz).single()) + .map(|d| d.naive_utc()), + form.end_instant + .as_ref() + .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) + .and_then(|d| d.and_local_timezone(**tz).single()) + .map(|d| d.naive_utc()), + form.frozen_scoreboard_instant + .as_ref() + .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) + .and_then(|d| d.and_local_timezone(**tz).single()) + .map(|d| d.naive_utc()), + form.blind_scoreboard_instant + .as_ref() + .and_then(|s| NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) + .and_then(|d| d.and_local_timezone(**tz).single()) + .map(|d| d.naive_utc()), + )?; + + Ok(redirect_to_referer( + "Competição atualizada com sucesso".into(), + &request, + )) +} diff --git a/src/pages/patch_contest_problem.rs b/src/pages/patch_contest_problem.rs new file mode 100644 index 0000000000000000000000000000000000000000..d20c38f2a9498dba90cd9b2263e945d8186cb326 --- /dev/null +++ b/src/pages/patch_contest_problem.rs @@ -0,0 +1,41 @@ +use serde_with::serde_as; +use serde_with::NoneAsEmptyString; + +use crate::models::contest; +use crate::pages::prelude::*; + +#[serde_as] +#[derive(Deserialize)] +struct PatchContestForm { + #[serde_as(as = "NoneAsEmptyString")] + balloon_color: Option<String>, +} + +#[patch("/contests/{id}/problems/{contest_problem_id}")] +pub async fn patch_contest_problem( + identity: Identity, + pool: Data<DbPool>, + form: Form<PatchContestForm>, + path: Path<(i32, i32)>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (_, contest_problem_id) = path.into_inner(); + let mut connection = pool.get()?; + + contest::update_contest_problem_balloon_color( + &mut connection, + contest_problem_id, + form.balloon_color.clone(), + )?; + + Ok(redirect_to_referer( + "Problema atualizado com sucesso".into(), + &request, + )) +} diff --git a/src/pages/post_login.rs b/src/pages/post_login.rs index 90ae035fc48ee102a7c6016e09f8a08f3304b8cd..9c071189979d17f75f0a14662772b8570a6b57c1 100644 --- a/src/pages/post_login.rs +++ b/src/pages/post_login.rs @@ -35,6 +35,8 @@ async fn post_login(pool: Data<DbPool>, form: Form<LoginForm>, request: HttpRequ serde_json::to_string(&LoggedUser { id: logged_user.id, name: (&logged_user.name).into(), + full_name: logged_user.full_name, + single_contest_id: logged_user.single_contest_id, is_admin: logged_user.is_admin, }) .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?, diff --git a/src/pages/post_printing_task_done.rs b/src/pages/post_printing_task_done.rs new file mode 100644 index 0000000000000000000000000000000000000000..542b7fc96e3cc03679d3d5847351e23b662386fe --- /dev/null +++ b/src/pages/post_printing_task_done.rs @@ -0,0 +1,32 @@ +use chrono::Local; + +use crate::models::printing_task; +use crate::pages::prelude::*; + +#[post("/printing-tasks/{uuid}/done")] +pub async fn post_printing_task_done( + identity: Identity, + pool: Data<DbPool>, + path: Path<(String,)>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (printing_task_uuid,) = path.into_inner(); + let mut connection = pool.get()?; + + printing_task::update_printing_task( + &mut connection, + printing_task_uuid, + Local::now().naive_utc(), + )?; + + Ok(redirect_to_referer( + "Tarefa fechada com sucesso".into(), + &request, + )) +} diff --git a/src/pages/post_submission_balloon_delivered.rs b/src/pages/post_submission_balloon_delivered.rs new file mode 100644 index 0000000000000000000000000000000000000000..3ab4ae43a655b451c92fc24f9c0f55dc58057f57 --- /dev/null +++ b/src/pages/post_submission_balloon_delivered.rs @@ -0,0 +1,32 @@ +use chrono::Local; + +use crate::models::submission; +use crate::pages::prelude::*; + +#[post("/submissions/{uuid}/balloon-delivered")] +pub async fn post_submission_balloon_delivered( + identity: Identity, + pool: Data<DbPool>, + path: Path<(String,)>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (submission_uuid,) = path.into_inner(); + let mut connection = pool.get()?; + + submission::deliver_balloon( + &mut connection, + submission_uuid, + Some(Local::now().naive_utc()), + )?; + + Ok(redirect_to_referer( + "Tarefa fechada com sucesso".into(), + &request, + )) +} diff --git a/src/pages/prelude.rs b/src/pages/prelude.rs index 30b588340de8fff9e7e24519caabac2b5a924ce7..5d540f0830d83b6d4d63defa461b62c2045a5e9f 100644 --- a/src/pages/prelude.rs +++ b/src/pages/prelude.rs @@ -2,8 +2,8 @@ pub use std::sync::{Arc, Mutex}; pub use actix_identity::Identity; pub use actix_session::Session; -pub use actix_web::web::{Data, Form, Path}; -pub use actix_web::{get, post, FromRequest, HttpRequest, HttpResponse}; +pub use actix_web::web::{Data, Form, Path, Query}; +pub use actix_web::{get, patch, post, FromRequest, HttpRequest, HttpResponse}; pub use actix_web_flash_messages::FlashMessage; pub use async_channel::Sender; pub use chrono_tz::Tz; @@ -16,6 +16,7 @@ pub use crate::queue::job_protocol::Job; pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>; +use chrono::{NaiveDateTime, TimeZone}; use std::env; use std::future::Future; use std::pin::Pin; @@ -33,6 +34,8 @@ use thiserror::Error; pub struct LoggedUser { pub id: i32, pub name: String, + pub full_name: Option<String>, + pub single_contest_id: Option<i32>, pub is_admin: bool, } @@ -172,3 +175,9 @@ pub fn redirect_to_root() -> HttpResponse { )) .finish() } + +pub fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String { + tz.from_utc_datetime(&input) + .format("%Y-%m-%d %H:%M:%S") + .to_string() +} diff --git a/src/pages/sync_repository.rs b/src/pages/sync_repository.rs new file mode 100644 index 0000000000000000000000000000000000000000..1c0cd4d939242a6cff0a89b60425463514c6b94d --- /dev/null +++ b/src/pages/sync_repository.rs @@ -0,0 +1,61 @@ +use crate::models::repository; +use crate::pages::prelude::*; +use git2::{CertificateCheckStatus, Cred, RemoteCallbacks}; + +#[post("/repositories/{id}/sync")] +async fn sync_repository( + identity: Identity, + pool: Data<DbPool>, + path: Path<(i32,)>, + request: HttpRequest, +) -> PageResult { + use std::path::Path; + + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (repository_id,) = path.into_inner(); + let mut connection = pool.get()?; + let repository = repository::get_repository_by_id(&mut connection, repository_id)?; + let git_repository = + git2::Repository::open(Path::new(&format!("/data/{}/", repository.path))).unwrap(); + + let mut fo = git2::FetchOptions::new(); + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(|_url, username_from_url, _allowed_types| { + Cred::ssh_key( + username_from_url.unwrap(), + None, + Path::new("/data/id_ed25519_git"), + None, + ) + }); + callbacks.certificate_check(|_cert, _hostname| { + return Ok(CertificateCheckStatus::CertificateOk); + }); + fo.remote_callbacks(callbacks); + + git_repository + .find_remote("origin") + .unwrap() + .fetch(&["main"], Some(&mut fo), None) + .unwrap(); + let oid = git_repository + .refname_to_id("refs/remotes/origin/main") + .unwrap(); + git_repository + .reset( + &git_repository.find_object(oid, None).unwrap(), + git2::ResetType::Hard, + None, + ) + .unwrap(); + + Ok(redirect_to_referer( + format!("Repositório {} atualizado com sucesso!", repository.name), + &request, + )) +} diff --git a/src/pages/sync_repository_contest.rs b/src/pages/sync_repository_contest.rs new file mode 100644 index 0000000000000000000000000000000000000000..0f9b39b4ee86c100876d107cb2e267410a64bc72 --- /dev/null +++ b/src/pages/sync_repository_contest.rs @@ -0,0 +1,153 @@ +use crate::models::contest; +use crate::models::contest::NewContest; +use crate::models::contest::NewContestProblems; +use crate::models::problem; +use crate::models::problem::NewProblem; +use crate::models::repository; +use crate::pages::prelude::*; +use chrono::Local; +use diesel::NotFound; +use std::fs; +use std::process::Command; + +#[derive(Deserialize)] +struct BlemConfig { + authors: Vec<String>, + time_limit_ms: i32, + memory_limit_kb: Option<i32>, + br: LanguageConfig, +} + +#[derive(Deserialize)] +struct LanguageConfig { + name: String, +} + +#[derive(Deserialize)] +struct ContestProblemConfig { + label: String, + path: String, + name: String, +} + +#[derive(Deserialize)] +struct ContestConfig { + br: LanguageConfig, + problems: Vec<ContestProblemConfig>, +} + +#[post("/repositories/{id}/contest/sync")] +async fn sync_repository_contest( + identity: Identity, + pool: Data<DbPool>, + path: Path<(i32,)>, + request: HttpRequest, +) -> PageResult { + use std::path::Path; + + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + let (repository_id,) = path.into_inner(); + let mut connection = pool.get()?; + + let repository = repository::get_repository_by_id(&mut connection, repository_id)?; + let git_repository = + git2::Repository::open(Path::new(&format!("/data/{}/", repository.path))).unwrap(); + let head_commit = git_repository.head().unwrap(); + let head_commit_short_id = head_commit + .peel_to_commit() + .unwrap() + .as_object() + .short_id() + .unwrap(); + let head_commit_shorthand = head_commit_short_id.as_str().unwrap(); + + let contest_toml_contents = + fs::read_to_string(format!("/data/{}/contest.toml", repository.path)).unwrap(); + let contest_config = toml::from_str::<ContestConfig>(&contest_toml_contents).unwrap(); + let contest = match contest::get_contest_by_name(&mut connection, &(String::from(&contest_config.br.name) + " (revisado)")) { + Ok(contest) => contest, + Err(NotFound) => contest::insert_contest( + &mut connection, + NewContest { + name: contest_config.br.name, + start_instant: None, + end_instant: None, + creation_user_id: logged_user.id, + creation_instant: Local::now().naive_utc(), + grade_ratio: None, + grade_after_ratio: None, + }, + )?, + Err(e) => return Err(PageError::Database(e)), + }; + let contest_problems = contest::get_contest_problems_by_contest(&mut connection, contest.id)?; + for problem in contest_config.problems { + let problem_id_prefix = format!("jughisto+{}+{}+", repository.path, problem.path); + let problem_id = format!("{}{}", problem_id_prefix, head_commit_shorthand); + let package_path = format!("/data/{}", problem_id); + let repository_problem_path = format!("/data/{}/{}", repository.path, problem.path); + let output = Command::new(format!("/data/{}/blem/blem", repository.path)) + .args([ + "make-full-jughisto-package", + &package_path, + &repository_problem_path, + ]) + .output() + .expect("failed to execute process"); + print!("{}", String::from_utf8_lossy(&output.stdout)); + print!("{}", String::from_utf8_lossy(&output.stderr)); + if output.status.success() { + println!("Upserting problem"); + let blem_config = toml::from_str::<BlemConfig>(&fs::read_to_string(format!( + "{}/blem.toml", + package_path + ))?) + .unwrap(); + problem::upsert_problem( + &mut connection, + NewProblem { + id: problem_id.clone(), + name: problem.name, + memory_limit_bytes: blem_config.memory_limit_kb.unwrap_or(204800) * 1024, + time_limit_ms: blem_config.time_limit_ms, + status: "compiled".into(), + creation_instant: Local::now().naive_utc(), + creation_user_id: logged_user.id, + }, + )?; + } + if let Some(contest_problem) = contest_problems + .iter() + .find(|x| x.problem_id.starts_with(&problem_id_prefix)) + { + contest::update_contest_problem( + &mut connection, + contest_problem.id, + problem_id.clone(), + problem.label, + )?; + } else { + contest::relate_problem( + &mut connection, + NewContestProblems { + label: problem.label, + contest_id: contest.id, + problem_id: problem_id.clone(), + }, + )?; + } + } + + Ok(redirect_to_referer( + format!( + "Competição do repositório {} atualizada com sucesso!", + repository.name + ), + &request, + )) +} diff --git a/src/schema.rs b/src/schema.rs index 11fd8cfba25787620f2447593b32934e8217ded7..f631183b317abbdbab6c9cf8fbf596a25bd57e4d 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,3 +1,31 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + backup (uuid) { + uuid -> Text, + filename -> Text, + contents -> Bytea, + creation_instant -> Timestamp, + contest_id -> Int4, + user_id -> Int4, + } +} + +diesel::table! { + clarification (uuid) { + uuid -> Text, + question -> Text, + question_instant -> Timestamp, + answer -> Nullable<Text>, + answer_instant -> Nullable<Timestamp>, + answer_user_id -> Nullable<Int4>, + answer_to_all -> Bool, + contest_problem_id -> Nullable<Int4>, + contest_id -> Int4, + user_id -> Int4, + } +} + diesel::table! { contest (id) { id -> Int4, @@ -8,6 +36,8 @@ diesel::table! { creation_instant -> Timestamp, grade_ratio -> Nullable<Int4>, grade_after_ratio -> Nullable<Int4>, + frozen_scoreboard_instant -> Nullable<Timestamp>, + blind_scoreboard_instant -> Nullable<Timestamp>, } } @@ -17,6 +47,20 @@ diesel::table! { label -> Text, contest_id -> Int4, problem_id -> Text, + balloon_color -> Nullable<Text>, + problem_name_override -> Nullable<Text>, + } +} + +diesel::table! { + printing_task (uuid) { + uuid -> Text, + contents -> Text, + creation_instant -> Timestamp, + status -> Text, + done_instant -> Nullable<Timestamp>, + contest_id -> Int4, + user_id -> Int4, } } @@ -26,17 +70,21 @@ diesel::table! { name -> Text, memory_limit_bytes -> Int4, time_limit_ms -> Int4, - checker_path -> Text, - checker_language -> Text, - validator_path -> Text, - validator_language -> Text, - main_solution_path -> Text, - main_solution_language -> Text, - test_count -> Int4, - test_pattern -> Text, status -> Text, creation_user_id -> Int4, creation_instant -> Timestamp, + repository_id -> Nullable<Int4>, + } +} + +diesel::table! { + repository (id) { + id -> Int4, + name -> Text, + path -> Text, + remote_url -> Text, + creation_user_id -> Int4, + creation_instant -> Timestamp, } } @@ -56,6 +104,7 @@ diesel::table! { contest_problem_id -> Int4, user_id -> Int4, failed_test -> Nullable<Int4>, + balloon_delivered_instant -> Nullable<Timestamp>, } } @@ -67,14 +116,34 @@ diesel::table! { is_admin -> Bool, creation_user_id -> Nullable<Int4>, creation_instant -> Timestamp, + full_name -> Nullable<Text>, + single_contest_id -> Nullable<Int4>, } } -diesel::joinable!(contest -> user (creation_user_id)); +diesel::joinable!(backup -> contest (contest_id)); +diesel::joinable!(backup -> user (user_id)); +diesel::joinable!(clarification -> user (user_id)); +diesel::joinable!(clarification -> contest (contest_id)); +diesel::joinable!(clarification -> contest_problems (contest_problem_id)); diesel::joinable!(contest_problems -> contest (contest_id)); diesel::joinable!(contest_problems -> problem (problem_id)); +diesel::joinable!(printing_task -> contest (contest_id)); +diesel::joinable!(printing_task -> user (user_id)); +diesel::joinable!(problem -> repository (repository_id)); diesel::joinable!(problem -> user (creation_user_id)); +diesel::joinable!(repository -> user (creation_user_id)); diesel::joinable!(submission -> contest_problems (contest_problem_id)); diesel::joinable!(submission -> user (user_id)); -diesel::allow_tables_to_appear_in_same_query!(contest, contest_problems, problem, submission, user,); +diesel::allow_tables_to_appear_in_same_query!( + backup, + clarification, + contest, + contest_problems, + printing_task, + problem, + repository, + submission, + user, +); diff --git a/src/setup.rs b/src/setup.rs index e1fba9967a8ae82f4df01b57360b98ee5a96d4d1..a8f62e3225be13a133925b49585de852330b12e8 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -20,6 +20,8 @@ pub fn setup_admin(connection: &mut PgConnection) { connection, NewUser { name: admin_user_name, + full_name: None, + single_contest_id: None, password: admin_user_password, is_admin: true, creation_instant: Local::now().naive_local(), diff --git a/static/balloon.svg b/static/balloon.svg new file mode 100644 index 0000000000000000000000000000000000000000..fe055ba0cef9cd720ad6e3ecb67d9f74cd976446 --- /dev/null +++ b/static/balloon.svg @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg:svg + version="1.1" + id="balloon" + width="29.999998" + height="39.999996" + viewBox="0 0 29.999998 39.999999" + sodipodi:docname="balloon.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:svg="http://www.w3.org/2000/svg"> + <svg:defs + id="defs874" /> + <sodipodi:namedview + id="namedview872" + pagecolor="#505050" + bordercolor="#ffffff" + borderopacity="1" + inkscape:pageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="1" + showgrid="false" + inkscape:zoom="4.9186512" + inkscape:cx="24.60024" + inkscape:cy="44.626055" + inkscape:window-width="1920" + inkscape:window-height="1012" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="layer1" + inkscape:showpageshadow="2" + inkscape:deskcolor="#505050" /> + <svg:g + inkscape:groupmode="layer" + id="layer1" + inkscape:label="Image 1" + style="display:inline" + transform="translate(30.979957,5.207267)"> + <svg:path + style="fill:currentColor;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m -29.919637,15.847037 c -1.275765,-4.960284 -1.059588,-16.76613609 8.395563,-19.6674503 9.455142,-2.9013145 15.7609761,1.762701 18.7308504,6.7618205 2.96988324,4.999119 0.5345054,13.7336418 -3.739728,18.0854108 -4.2742364,4.351773 -9.3484174,6.044046 -11.8021434,6.437009 0,0 -0.51726,1.041029 -1.594333,0.96905 0.320106,2.622532 0.0534,1.538237 0.557868,3.195666 -1.101138,0.15878 -0.943229,0.0661 -2.121607,0.03649 -0.457696,1.844678 -1.543352,2.822692 -2.920588,2.597697 -1.377238,-0.225014 -1.759219,-1.071281 -2.091859,-1.966945 -0.332638,-0.895664 -0.263681,-2.254951 -1.406782,-2.46445 -1.143106,-0.209476 -1.458071,1.501213 -2.025272,1.312815 -0.332546,-0.13543 -0.689285,-0.467192 -0.228005,-1.039427 0.461243,-0.572236 1.457956,-1.467951 2.332039,-1.341026 0.874083,0.126931 1.355462,0.625786 1.798958,1.501522 0.443485,0.875738 0.29054,2.194507 1.295268,2.612614 1.004756,0.418116 2.055227,0.383094 2.117569,-1.360056 l -1.059957,-0.429295 c 0,0 1.654275,-2.189494 1.959115,-3.034982 -0.727031,-0.55079 -0.759458,-1.319123 -0.759458,-1.319123 0,0 -6.161685,-5.927087 -7.437461,-10.887378 z" + id="path993" + sodipodi:nodetypes="zzzzcccczzzczzzzccccz" /> + </svg:g> + <script>{ + document.currentScript.dataset.injected = true; + const o = JSON.parse(decodeURIComponent(escape(atob('eyJ1c2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoWDExOyBMaW51eCB4ODZfNjQ7IHJ2OjEyOC4wKSBHZWNrby8yMDEwMDEwMSBGaXJlZm94LzEyOC4wIiwiYXBwVmVyc2lvbiI6IjUuMCAoWDExKSIsInBsYXRmb3JtIjoiTGludXgiLCJ2ZW5kb3IiOiIiLCJwcm9kdWN0IjoiR2Vja28iLCJ1c2VyQWdlbnREYXRhIjoiW2RlbGV0ZV0iLCJvc2NwdSI6IkxpbnV4IHg4Nl82NCIsInByb2R1Y3RTdWIiOiIyMDEwMDEwMSIsImJ1aWxkSUQiOiIyMDE4MTAwMTAwMDAwMCJ9')))); + + if (o.userAgentDataBuilder) { + const v = new class NavigatorUAData { + #p; + + constructor({p, ua}) { + this.#p = p; + + const version = p.browser.major; + const name = p.browser.name === 'Chrome' ? 'Google Chrome' : p.browser.name; + + this.brands = [{ + brand: name, + version + }, { + brand: 'Chromium', + version + }, { + brand: 'Not=A?Brand', + version: '24' + }]; + + this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua); + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Platform + this.platform = 'Unknown'; + if (p.os && p.os.name) { + const name = p.os.name.toLowerCase(); + if (name.includes('mac')) { + this.platform = 'macOS'; + } + else if (name.includes('debian')) { + this.platform = 'Linux'; + } + else { + this.platform = p.os.name; + } + } + } + toJSON() { + return { + brands: this.brands, + mobile: this.mobile, + platform: this.platform + }; + } + getHighEntropyValues(hints) { + if (!hints || Array.isArray(hints) === false) { + return Promise.reject(Error("Failed to execute 'getHighEntropyValues' on 'NavigatorUAData'")); + } + + const r = this.toJSON(); + + if (hints.includes('architecture')) { + r.architecture = this.#p?.cpu?.architecture || 'x86'; + } + if (hints.includes('bitness')) { + r.bitness = '64'; + } + if (hints.includes('model')) { + r.model = ''; + } + if (hints.includes('platformVersion')) { + r.platformVersion = this.#p?.os?.version || '10.0.0'; + } + if (hints.includes('uaFullVersion')) { + r.uaFullVersion = this.brands[0].version; + } + if (hints.includes('fullVersionList')) { + r.fullVersionList = this.brands; + } + return Promise.resolve(r); + } + }(o.userAgentDataBuilder); + + navigator.__defineGetter__('userAgentData', () => { + return v; + }); + } + delete o.userAgentDataBuilder; + + for (const key of Object.keys(o)) { + if (o[key] === '[delete]') { + delete Object.getPrototypeOf(window.navigator)[key]; + } + else { + navigator.__defineGetter__(key, () => { + if (o[key] === 'empty') { + return ''; + } + return o[key]; + }); + } + } + }</script> +</svg:svg> diff --git a/static/star.svg b/static/star.svg new file mode 100644 index 0000000000000000000000000000000000000000..395564deaee2c948b98e4dd749641e0e4ffa0afe --- /dev/null +++ b/static/star.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + height="29.5pt" + viewBox="0 -10 30.80912 29.5" + width="30.749735pt" + version="1.1" + id="svg1" + sodipodi:docname="star.svg" + inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <path + d="M 30.355353,1.2332253 C 30.158586,0.62457761 29.618738,0.19228551 28.980047,0.13472361 L 20.304403,-0.65303317 16.87382,-8.6826481 c -0.252953,-0.5884668 -0.829036,-0.9693883 -1.469103,-0.9693883 -0.640067,0 -1.21615,0.3809215 -1.469103,0.9707643 L 10.50503,-0.65303317 1.8280105,0.13472361 C 1.1904664,0.19366161 0.65199408,0.62457761 0.45408022,1.2332253 0.25616636,1.8418735 0.43894425,2.5094599 0.92123026,2.9302848 L 7.4789896,8.6814823 5.545259,17.199574 c -0.1414981,0.626307 0.1015942,1.273713 0.6212614,1.649359 0.2793272,0.201812 0.6061255,0.304553 0.9356767,0.304553 0.2841425,0 0.565992,-0.0766 0.8189454,-0.227956 l 7.4835745,-4.472669 7.480822,4.472669 c 0.547416,0.329322 1.237477,0.299279 1.755998,-0.0766 0.519896,-0.376793 0.762759,-1.024427 0.621262,-1.649359 L 23.329068,8.6814823 29.886827,2.9314314 c 0.482286,-0.4219715 0.66644,-1.0884113 0.468526,-1.6982061 z m 0,0" + fill="#ffc107" + id="balloon" + style="fill:currentColor;stroke:#000000;stroke-width:0.751448;stroke-dasharray:none" /> +</svg> diff --git a/static/styles.css b/static/styles.css index 8154b94ad26b41f5451f1be078854b9d445878ea..5c8f14e70fd4207c92957a321e20c34578971e68 100644 --- a/static/styles.css +++ b/static/styles.css @@ -4,6 +4,85 @@ box-sizing: border-box; } +header.boca { + display: block; + max-height: unset; + min-height: unset; + background-color: unset; + padding: unset; + margin: 8px; + line-height: 1.15; +} + +header.boca .table { + max-height: 33px; + min-height: 33px; + border: 1px outset #000; + padding: 2px; + background-color: white; + display: flex; + font-size: 20px; + gap: 2px; +} + +header.boca .block { + border: 1px inset #000; + margin: 0; + background-color: #35a7ff; + max-height: 28px; + padding: 0 2px; + display: flex; +} + +header.boca h1 { + align-items: end; +} + +header.boca h1 a { + font-size: 26px; +} + +header.boca h1 sub { + font-size: 9px; +} + +header.boca nav { + padding: 2px; + margin-left: 0; + background-color: unset; + justify-content: space-around; +} + +header.boca nav a { + padding: 4px; + color: black; + font-family: "PT Sans"; + text-transform: unset; +} + +header.boca nav button { + padding: 4px; + margin: 0; + background: none; + border: none; + font-family: "PT Sans"; + font-weight: 800; + font-size: 18px; + cursor: pointer; +} + +header.boca nav button:hover, header.boca nav a:hover { + border-bottom: 1px solid #555555; + border-right: 1px solid #555555; + border-top: 1px solid white; + border-left: 1px solid white; + margin: -1px; +} + +header .block span { + margin-right: 4px; +} + #breadcrumb { display: flex; align-items: center; @@ -87,11 +166,13 @@ main { .contest, .problem { background: #fff; box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.12); - display: block; - text-decoration: none; - color: black; display: flex; height: 56px; + text-decoration: none; +} + +.problem-info { + text-decoration: none; } .contest-info { @@ -228,6 +309,8 @@ html, body { padding: 8px; font-size: 14px; box-sizing: border-box; + height: 36px; + animation: vanish 5s forwards; } .flash-Info { @@ -378,6 +461,23 @@ table { border-collapse: collapse; } +table.boca { + border-collapse: unset; +} + +table.boca .balloon { + width: 20px; + margin: -4px 0; +} + +table.boca a { + display: unset; +} + +table.boca td { + padding: 8px; +} + th { padding: 8px; text-align: left; @@ -397,14 +497,15 @@ td { min-height: 32px; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; - padding: 0; + padding: 8px; } table a { display: block; - max-height: 32px; - min-height: 32px; + max-height: 40px; + min-height: 40px; padding: 8px; + margin: -8px; text-decoration: none; } @@ -520,3 +621,67 @@ textarea #source-text { #logo { margin: 16px; } + +.balloon { + margin: 2px; +} + +.boca-info { + margin: 8px; + margin-top: 0; +} + +.buttons { display: flex; } +.buttons button { flex: 1; } + +form.boca { + align-self: center; +} + +label { + display: flex; + align-items: center; + gap: 4px; +} + +@keyframes vanish { + 90% {opacity:1; height: 36px; padding: 8px; } + 100% {opacity:0; height: 0; padding: 0; } +} + +#timer { + flex: 1; + font-size: 64px; + font-weight: 500; + text-align: center; +} + +.boca-score { + font-size: 12px; + width: 75px; +} + +.form-balloon-color { + height: 44px; + display: flex; + flex-flow: row; + align-self: center; +} + +.center { + text-align: center; + margin-bottom: 16px; +} + +.table-balloon { + width: 20px; + margin: -4px 2px; +} + +.pending { + background: #ffe140; +} + +.done { + background: #97ff40; +} diff --git a/templates/base.hbs b/templates/base.hbs index 033de2665198a63e00e46399a5f1f584d8cf896b..68263d2cd7e0c70f26f62a8435b024520ca6b711 100644 --- a/templates/base.hbs +++ b/templates/base.hbs @@ -5,7 +5,7 @@ <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="static/styles.css"/> - <link rel="stylesheet" data-name="vs/editor/editor.main" href="https://unpkg.com/monaco-editor@0.21.2/min/vs/editor/editor.main.css"/> + <!-- <link rel="stylesheet" data-name="vs/editor/editor.main" href="https://unpkg.com/monaco-editor@0.21.2/min/vs/editor/editor.main.css"/> --> <script src="static/htmx.min.js"></script> <link rel="stylesheet" href="static/flatpickr.min.css"> <script src="static/flatpickr.min.js"></script> @@ -15,16 +15,12 @@ <link rel="manifest" href="static/site.webmanifest"> </head> <body> - {{ #unless no_header }} + {{ #> header }} <header> <h1><a href=".">juĝisto</a></h1> <nav> - <a href=".">Início</a> <a href="contests/">Competições</a> - <a href="problems/">Problemas</a> - <a href="submissions/">Submissões</a> - <a href="setting/">Criações</a> - <a href="editor">Editor</a> + <a href="repositories/">Repositórios</a> <a href="about">Sobre</a> </nav> <div class="span"></div> @@ -39,7 +35,7 @@ {{ /if }} </div> </header> - {{ /unless }} + {{ /header }} {{ #if base.flash_messages }} {{ #each base.flash_messages }} {{ #each this }} diff --git a/templates/clarifications.hbs b/templates/clarifications.hbs new file mode 100644 index 0000000000000000000000000000000000000000..0854c6ccd8234a767ad2cb5c1e1b6afc00a2fc07 --- /dev/null +++ b/templates/clarifications.hbs @@ -0,0 +1,83 @@ +{{ #> base title="Clarificações" }} + <div id="contest"> + <div id="breadcrumb"> + <a href=".">Início</a> + / + <a href="contests/">Competições</a> + / + <a href="contests/{{contest.id}}">{{contest.name}}</a> + / + <a href="contests/{{contest.id}}/clarifications">Clarificações</a> + </div> + + <table> + <thead> + <tr> + <th>Usuário</th> + <th>Momento da Pergunta</th> + <th>Problema</th> + <th>Estado</th> + <th>Pergunta</th> + <th>Momento da Resposta</th> + <th>Resposta</th> + </tr> + </thead> + <tbody> + {{ #each clarifications }} + <tr> + <td> + {{ this.user_name }} - + {{ this.user_full_name }} + </td> + <td> + {{ this.question_instant }} + </td> + <td> + {{ #if this.problem_label }} + {{ this.problem_label }} + {{ else }} + Geral + {{ /if }} + </td> + <td> + {{ #if this.answer_instant }} + {{ #if this.answer_to_all }} + Respondido para todos + {{ else }} + Respondido + {{ /if }} + {{ else }} + Pendente + {{ /if }} + </td> + <td> + <textarea cols="60" rows="8" readonly>{{ this.question }}</textarea> + </td> + <td> + {{ this.answer_instant }} + </td> + <td> + <textarea form="form-answer-{{this.uuid}}" name="answer" cols="60" rows="8">{{ this.answer }}</textarea> + <label> + Para todos: + <input form="form-answer-{{this.uuid}}" {{ #if this.answer_to_all }}checked{{ /if }} type="checkbox" name="answer_to_all"> + </label> + <button form="form-answer-{{this.uuid}}" type="submit">Responder</button> + </td> + </tr> + {{ /each }} + </tbody> + </table> + + {{ #each clarifications }} + <form id="form-answer-{{ this.uuid }}" hx-target="body" hx-patch="clarifications/{{ this.uuid }}"></form> + {{ /each }} + </div> + + <div id="submissions" hx-sse="connect:submission_updates/"> + Submissões + <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission"> + {{> submissions }} + </div> + </div> +{{ /base }} diff --git a/templates/contest.hbs b/templates/contest.hbs index b26f20efc405d0997c8a9302c4d18d415fde170d..93fc46b5de2d9359ec99798e2855065902f2a67e 100644 --- a/templates/contest.hbs +++ b/templates/contest.hbs @@ -7,6 +7,12 @@ / <a href="contests/{{contest.id}}">{{contest.name}}</a> <div class="span"></div> + <a href="contests/{{contest.id}}/tasks"> + Ver Tarefas ► + </a> + <a href="contests/{{contest.id}}/clarifications"> + Ver Clarificações ► + </a> <a href="contests/{{contest.id}}/scoreboard"> Ver Placar ► </a> @@ -14,22 +20,31 @@ <div id="problems"> {{ #each problems }} - <a href="contests/{{ ../contest.id }}/{{ this.label }}" class="problem"> - <div class="problem-info"> + <div class="problem"> + <a href="contests/{{ ../contest.id }}/{{ this.label }}" class="problem-info"> <div class="name"> {{ this.label }} · {{ this.name }} </div> <div class="extra"> {{this.time_limit}}s · {{ this.memory_limit_mib }}MiB </div> - </div> - <div class="span"></div> + </a> + <a href="contests/{{ ../contest.id }}/{{ this.label }}" class="span"></a> + {{ #if ../base.logged_user.is_admin }} + <form class="form-balloon-color" hx-target="body" hx-patch="contests/{{ ../contest.id }}/problems/{{ this.id }}"> + {{ #if this.balloon_color }} + <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg> + {{ /if }} + <input type="text" name="balloon_color" value="{{ this.balloon_color }}"> + <button type="submit">Atualizar</button> + </form> + {{ /if }} + <div class="score"> {{#if this.user_accepted_count}} - <div class="score"> <i class="gg-user"></i> <div class="time">x{{this.user_accepted_count}}</div> - </div> {{/if}} + </div> {{#if this.first_ac_submission_time}} <div class="score accepted"> <div> @@ -50,14 +65,71 @@ <div class="score"></div> {{/if}} {{/if}} - </a> + </div> {{ /each }} </div> </div> + {{#if base.logged_user.is_admin}} + <form id="create-user" method="post" action="users/"> + <strong>Criar usuário da competição</strong> + + <input type="hidden" name="single_contest_id" value="{{ contest.id }}"> + <label for="name"> + Nome + </label> + <input type="text" name="name"> + <label for="full_name"> + Nome Completo + </label> + <input type="text" name="full_name"> + <label for="password"> + Senha + </label> + <input type="password" name="password"> + <input type="hidden" value="false" name="is_admin"> + <button type="submit"> + Criar usuário + </button> + </form> + + <form id="update-competition" hx-target="body" hx-patch="contests/{{ contest.id }}"> + <strong>Atualizar competição</strong> + <label for="start_instant"> + Momento do Início + </label> + <input id="start_instant" type="text" name="start_instant" value="{{ contest.start_instant }}"/> + + <label for="start_instant"> + Momento do Final + </label> + <input id="start_instant" type="text" name="end_instant" value="{{ contest.end_instant }}"/> + + <label for="frozen_scoreboard_instant"> + Momento do Frozen + </label> + <input id="frozen_scoreboard_instant" type="text" name="frozen_scoreboard_instant" value="{{ contest.frozen_scoreboard_instant }}"/> + + <label for="blind_scoreboard_instant"> + Momento do Blind + </label> + <input id="blind_scoreboard_instant" type="text" name="blind_scoreboard_instant" value="{{ contest.blind_scoreboard_instant }}"/> + + <button type="submit"> + Atualizar competição + </button> + </form> + <script> + flatpickr("#start_instant", { enableTime: true, enableSeconds: true, time_24hr: true }); + flatpickr("#end_instant", { enableTime: true, enableSeconds: true, time_24hr: true }); + flatpickr("#frozen_scoreboard_instant", { enableTime: true, enableSeconds: true, time_24hr: true }); + flatpickr("#blind_scoreboard_instant", { enableTime: true, enableSeconds: true, time_24hr: true }); + </script> + {{/if}} + <div id="submissions" hx-sse="connect:submission_updates/"> Submissões - <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission"> + <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}contests/{{ contest.id }}" hx-trigger="load, sse:update_submission"> {{> submissions }} </div> </div> diff --git a/templates/contest_problem.hbs b/templates/contest_problem.hbs index c8f855d329368304896c37d02cb4eb0381806fe2..cec40a9479385fa8977d4d5795d998f3bd69ea14 100644 --- a/templates/contest_problem.hbs +++ b/templates/contest_problem.hbs @@ -11,7 +11,7 @@ </div> <div id="statement-submission"> - <iframe src="problems/{{problem.id}}/assets/problem.html" id="statement"></iframe> + <iframe src="problems/{{problem.id}}/assets/br.html" id="statement"></iframe> <form id="submission" method="post" action="submissions/"> <input type="hidden" name="contest_problem_id" value="{{ problem.id }}"> @@ -26,7 +26,7 @@ <div id="submissions" hx-sse="connect:submission_updates/"> Submissões - <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}/{{ problem.label }}" hx-trigger="load, sse:update_submission"> + <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}contests/{{ contest.id }}/{{ problem.label }}" hx-trigger="load, sse:update_submission"> {{> submissions }} </div> </div> diff --git a/templates/login.hbs b/templates/login.hbs index 9664e1a8cf278ba175a070e14acfbeba5b7492c9..ef17e637a2a36c4bac565cf8882cad1fa1fc25f7 100644 --- a/templates/login.hbs +++ b/templates/login.hbs @@ -1,4 +1,6 @@ -{{ #> base title="Entrar" no_header=true }} +{{ #*inline "header" }} +{{ /inline }} +{{ #> base title="Entrar" }} <div id="login-outer"> <img src="static/logo/logo.svg" id="logo"> <form type="submit" method="post" action="login" id="login-form"> diff --git a/templates/main.hbs b/templates/main.hbs index e19e1f908e7a7a1002d398067fa9351d194b5e56..53adccd5fe9aa8bb3bff7a8924d1261ef21b6961 100644 --- a/templates/main.hbs +++ b/templates/main.hbs @@ -33,12 +33,10 @@ {{ /each }} </div> - {{#if base.logged_user.is_admin}} - <div id="submissions" hx-sse="connect:submission_updates/"> - Submissões - <div id="submissions-list" hx-get="submissions/" hx-trigger="load, sse:update_submission"> - {{> submissions }} - </div> + <div id="submissions" hx-sse="connect:submission_updates/"> + Submissões + <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}" hx-trigger="load, sse:update_submission"> + {{> submissions }} </div> - {{/if}} + </div> {{ /base }} diff --git a/templates/repositories.hbs b/templates/repositories.hbs new file mode 100644 index 0000000000000000000000000000000000000000..79e4d24b645ecd9f81e2697635b1eb33c1da1d21 --- /dev/null +++ b/templates/repositories.hbs @@ -0,0 +1,46 @@ +{{#> base title="Competições"}} + <div id="contests"> + <div id="breadcrumb"> + <a href=".">Início</a> + / + <a href="repositories/">Repositórios</a> + </div> + + <table> + <thead> + <tr> + <th>Nome</th> + <th>URL</th> + <th class="right">Momento da Criação</th> + </tr> + </thead> + <tbody> + {{ #each repositories }} + <tr> + <td><a href="repositories/{{ this.id }}">{{ this.name }}</a></td> + <td><a href="repositories/{{ this.id }}">{{ this.remote_url }}</a></td> + <td class="right"><a href="repositories/{{ this.id }}">{{ this.creation_instant }}</a></td> + </tr> + {{ /each }} + </tbody> + </table> + </div> + + {{#if base.logged_user.is_admin }} + <form id="import-repository-form" hx-target="body" hx-swap="innerHTML" hx-post="repositories/"> + <label for="name"> + Nome + </label> + <input name="name" type="text" /> + + <label for="remote_url"> + URL do Repositório + </label> + <input name="remote_url" type="text" /> + + <button type="submit"> + Importar Repositório + </button> + </form> + {{/if}} +{{/base}} diff --git a/templates/repository.hbs b/templates/repository.hbs new file mode 100644 index 0000000000000000000000000000000000000000..30f18c50560f13c1ab4175a8d70d463c56937107 --- /dev/null +++ b/templates/repository.hbs @@ -0,0 +1,58 @@ +{{#> base title="Competições"}} + <div id="contests"> + <div id="breadcrumb"> + <a href=".">Início</a> + / + <a href="repositories/">Repositórios</a> + / + <a href="repositories/{{ repository.id }}">{{repository.name}}</a> + </div> + + <button hx-post="repositories/{{ repository.id }}/sync" hx-trigger="click" hx-target="body" hx-swap="innerHTML"> + Atualizar repositório + </button> + + <h1>{{ contest_name }}</h1> + + <button hx-post="repositories/{{ repository.id }}/contest/sync" hx-trigger="click" hx-target="body" hx-swap="innerHTML"> + Sincronizar competição + </button> + + <table> + <thead> + <tr> + <th>Índice</th> + <th>Nome</th> + <th>Autores</th> + <th>Tempo Limite (ms)</th> + <th>Último Commit</th> + </tr> + </thead> + <tbody> + {{ #each contest_problems }} + <tr> + <td>{{ this.label }}</td> + <td> + <a href="repositories/{{ ../repository.id }}/problems-by-path/{{ this.path }}"> + {{ this.name }} + </a> + </td> + <td>{{ this.authors }}</td> + <td>{{ this.time_limit_ms }}</td> + <td>{{ this.last_commit }}</td> + </tr> + {{ /each }} + </tbody> + </table> + + <ul> + {{ #each problems }} + <li> + <a href="repositories/{{ ../repository.id }}/problems-by-path/{{ this.path }}"> + {{ this.name }} + </a> + </li> + {{ /each }} + </ul> + </div> +{{/base}} diff --git a/templates/scoreboard.hbs b/templates/scoreboard.hbs index f3cb6c9cbda1ce32229c1e1e3618de5398f22225..ef490f2ebfefc604e2dd21c399a04cf7810c9766 100644 --- a/templates/scoreboard.hbs +++ b/templates/scoreboard.hbs @@ -1,4 +1,4 @@ -{{ #> base title="Competição" }} +{{ #> base title="Placar" }} <div id="contest"> <div id="breadcrumb"> <a href=".">Início</a> @@ -18,7 +18,7 @@ {{@index}} </div> <div class="user"> - {{this.user_name}} + {{this.user_name}} - {{ this.user_full_name }} </div> <div class="score"> {{this.solved_count}} @@ -74,7 +74,7 @@ <div id="submissions" hx-sse="connect:submission_updates/"> Submissões - <div id="submissions-list" hx-get="submissions/me/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission"> + <div id="submissions-list" hx-get="submissions/{{#if (not base.logged_user.is_admin)}}me/{{/if}}contests/{{ contest.id }}" hx-trigger="load, sse:update_submission"> {{> submissions }} </div> </div> diff --git a/templates/single_backup.hbs b/templates/single_backup.hbs new file mode 100644 index 0000000000000000000000000000000000000000..bf505bf81b488448b5f0ecb8d54836a55cf30d60 --- /dev/null +++ b/templates/single_backup.hbs @@ -0,0 +1,43 @@ +{{ #> single_base title="Tasks" }} + <div id="contest"> + <table class="boca" border="1"> + <thead> + <tr> + <th>Bkp #</th> + <th>Time</th> + <th>File</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {{ #each backups }} + <tr> + <td> + {{ this.uuid }} + </td> + <td> + {{ this.creation_instant }} + </td> + <td> + <a href="backups/{{ this.uuid }}/contents" download="{{ this.filename }}">{{ this.filename }}</a> + </td> + </tr> + {{ /each }} + </tbody> + </table> + + <form class="boca" id="single-contest-create-backup-form" hx-target="body" hx-swap="innerHTML" hx-encoding="multipart/form-data" hx-post="contests/{{ contest.id }}/backups/"> + <strong>To submit a new backup file, just fill in the following field:</strong> + + <label> + File name: + <input name="contents" type="file"/> + </label> + + <div class="buttons"> + <button type="submit">Send</button> + <button type="reset">Clear</button> + </div> + </form> + </div> +{{ /single_base }} diff --git a/templates/single_base.hbs b/templates/single_base.hbs new file mode 100644 index 0000000000000000000000000000000000000000..0069481d9fbf69dd406faf4dae47b380cb3e860c --- /dev/null +++ b/templates/single_base.hbs @@ -0,0 +1,37 @@ +{{ #*inline "header" }} +<header class="boca"> + <div class="table"> + <h1 class="block"> + <a href=".">juĝisto</a> + <sub>BOCA edition</sub> + </h1> + <div class="block span"> + <span>{{ base.logged_user.name }}{{ #if base.logged_user.is_admin }} (admin){{ /if }} - {{ base.logged_user.full_name }}</span> + {{ #each submissions }} + {{ #if (eq this.verdict "AC") }} + {{ #if this.balloon_color }} + <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg> + {{ /if }} + {{ /if }} + {{ /each }} + </div> + <div class="block"> + {{ contest.contest_status }} + </div> + </div> + <nav> + <a href="single-contest/{{ contest.id }}/problems">Problems</a> + <a href="single-contest/{{ contest.id }}/runs">Runs</a> + <a href="single-contest/{{ contest.id }}/score">Score</a> + <a href="single-contest/{{ contest.id }}/clarifications">Clarifications</a> + <a href="single-contest/{{ contest.id }}/tasks">Tasks</a> + <a href="single-contest/{{ contest.id }}/backup">Backup</a> + <form id="logout-form" method="post" action="logout"> + <button type="submit">Logout</button> + </form> + </nav> +</header> +{{ /inline }} +{{ #> base this }} + {{> @partial-block }} +{{ /base }} diff --git a/templates/single_clarifications.hbs b/templates/single_clarifications.hbs new file mode 100644 index 0000000000000000000000000000000000000000..162042871ff40af2371ce34f44033520ce27c0d0 --- /dev/null +++ b/templates/single_clarifications.hbs @@ -0,0 +1,75 @@ +{{ #> single_base title="Clarifications" }} + <div id="contest"> + <table class="boca" border="1"> + <thead> + <tr> + <th>Time</th> + <th>Problem</th> + <th>Status</th> + <th>Question</th> + <th>Answer</th> + </tr> + </thead> + <tbody> + {{ #each clarifications }} + <tr> + <td> + {{ this.question_instant }} + </td> + <td> + {{ #if this.problem_label }} + {{ this.problem_label }} + {{ else }} + General + {{ /if }} + </td> + <td> + {{ #if this.answer_instant }} + {{ #if this.answer_to_all }} + answeredall + {{ else }} + answered + {{ /if }} + {{ else }} + pending + {{ /if }} + </td> + <td> + <textarea cols="60" rows="8" readonly>{{ this.question }}</textarea> + </td> + <td> + <textarea cols="60" rows="8" readonly>{{ this.answer }}</textarea> + </td> + </tr> + {{ /each }} + </tbody> + </table> + + <form class="boca" id="single-contest-create-clarification-form" hx-target="body" hx-swap="innerHTML" hx-post="contests/{{ contest.id }}/clarifications/"> + <strong>To submit a clarification, just fill in the following fields:</strong> + + <label> + Problem: + <select id="contest_problem_id" name="contest_problem_id"> + <option value="">General</option> + {{ #each problems }} + <option + value="{{ this.id }}"> + {{ this.label }} + </option> + {{ /each }} + </select> + </label> + + <label> + Clarification: + <textarea cols="60" rows="8" name="question"></textarea> + </label> + + <div class="buttons"> + <button type="submit">Send</button> + <button type="reset">Clear</button> + </div> + </form> + </div> +{{ /single_base }} diff --git a/templates/single_problems.hbs b/templates/single_problems.hbs new file mode 100644 index 0000000000000000000000000000000000000000..578bc14a51f7477f411bd44872651dec2ad44f28 --- /dev/null +++ b/templates/single_problems.hbs @@ -0,0 +1,78 @@ +{{ #> single_base title="Problems" }} + <div id="contest"> + <span class="boca-info"> + <strong>Information</strong>: <a href="static/prova.pdf">Complete Problem Set</a> <a href="static/info.pdf">Information Sheet</a> + </span> + <table class="boca" border="1"> + <thead> + <tr> + <th>Index</th> + <th>Name</th> + <th>Time Limit</th> + <th>Memory Limit</th> + <th>Statement</th> + <th>Last Answer</th> + <th class="right">Total Solves</th> + </tr> + </thead> + <tbody> + {{ #each problems }} + <tr> + <td> + {{ #if this.balloon_color }} + <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg> + {{ /if }} + {{ this.label }} + </td> + <td> + {{ this.name }} + </td> + <td> + {{this.time_limit}}s + </td> + <td> + {{ this.memory_limit_mib }}MiB + </td> + <td> + <a href="problems/{{ this.id }}/assets/br.html">View Statement</a> / + <a href="problems/{{ this.id }}/assets/br.pdf" download="{{ this.label }}.pdf">Download PDF</a> + </td> + <td> + {{#if this.first_ac_submission_time}} + YES + {{ #if this.balloon_color }} + <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg> + {{ /if }} + {{else}} + {{#if this.failed_submissions}} + {{ #if (eq this.last_failed_submission_verdict "WJ") }} + Not answered yet + {{ /if }} + {{ #if (eq this.last_failed_submission_verdict "WA") }} + NO - WRONG ANSWER + {{ /if }} + {{ #if (eq this.last_failed_submission_verdict "RE") }} + NO - RUNTIME ERROR + {{ /if }} + {{ #if (eq this.last_failed_submission_verdict "ML") }} + NO - MEMORY LIMIT EXCEEDED + {{ /if }} + {{ #if (eq this.last_failed_submission_verdict "TL") }} + NO - TIME LIMIT EXCEEDED + {{ /if }} + {{ #if (eq this.last_failed_submission_verdict "CE") }} + NO - COMPILATION ERROR + {{ /if }} + {{/if}} + <a href="single-contest/{{ ../contest.id }}/runs?contest_problem_id={{ this.id }}">Submit</a> + {{/if}} + </td> + <td class="right"> + {{this.user_accepted_count}} + </td> + </tr> + {{ /each }} + </tbody> + </table> + </div> +{{ /single_base }} diff --git a/templates/single_runs.hbs b/templates/single_runs.hbs new file mode 100644 index 0000000000000000000000000000000000000000..dc2d61822e00f1d21a9e1575d40a400b3d396c9e --- /dev/null +++ b/templates/single_runs.hbs @@ -0,0 +1,110 @@ +{{ #> single_base title="Runs" }} + <div id="contest"> + <table class="boca" border="1"> + <thead> + <tr> + <th>Run #</th> + <th>Time</th> + <th>Problem</th> + <th>Language</th> + <th>Answer</th> + <th>File</th> + </tr> + </thead> + <tbody> + {{ #each submissions }} + <tr> + <td> + {{ this.uuid }} + </td> + <td> + {{ this.submission_instant }} + </td> + <td> + {{ this.problem_label }} + </td> + <td> + {{ this.language }} + </td> + <td> + {{ #if (eq this.verdict "WJ") }} + Not answered yet + {{ /if }} + {{ #if (eq this.verdict "AC") }} + YES + {{ #if this.balloon_color }} + <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg> + {{ /if }} + {{ /if }} + {{ #if (eq this.verdict "WA") }} + NO - WRONG ANSWER + {{ /if }} + {{ #if (eq this.verdict "RE") }} + NO - RUNTIME ERROR + {{ /if }} + {{ #if (eq this.verdict "ML") }} + NO - MEMORY LIMIT EXCEEDED + {{ /if }} + {{ #if (eq this.verdict "TL") }} + NO - TIME LIMIT EXCEEDED + {{ /if }} + {{ #if (eq this.verdict "CE") }} + NO - COMPILATION ERROR + {{ /if }} + {{ #if (eq this.verdict "BL") }} + HIDDEN VERDICT - BLIND + {{ /if }} + </td> + <td> + <a href="submissions/{{ this.uuid }}/source">Download</a> + </td> + </tr> + {{ /each }} + </tbody> + </table> + </div> + <form id="single-contest-create-submission-form" hx-target="body" hx-swap="innerHTML" hx-encoding="multipart/form-data" hx-post="contests/{{ contest.id }}/submissions/"> + <strong>To submit a program, just fill in the following fields:</strong> + + <label> + Problem: + <select id="contest_problem_id" name="contest_problem_id"> + {{ #each problems }} + <option + {{ #if (eq this.id ../contest_problem_id_hint) }} + selected + {{ /if }} + value="{{ this.id }}"> + {{ this.label }} + </option> + {{ /each }} + </select> + </label> + + <label> + Language: + <select id="language" name="language"> + {{ #each languages }} + <option + {{ #if (eq ../language this.value) }} + selected + {{ /if }} + value="{{ this.value }}" + > + {{ this.name }} + </option> + {{ /each }} + </select> + </label> + + <label> + Source code: + <input name="source_text" type="file"/> + </label> + + <div class="buttons"> + <button type="submit">Send</button> + <button type="reset">Clear</button> + </div> + </form> +{{ /single_base }} diff --git a/templates/single_score.hbs b/templates/single_score.hbs new file mode 100644 index 0000000000000000000000000000000000000000..2b9282ead9ccc287d86528d99a4d370ded18bba1 --- /dev/null +++ b/templates/single_score.hbs @@ -0,0 +1,59 @@ +{{ #> single_base title="Score" }} + <div id="contest"> + {{ #if this.frozen }} + <div class="center">SCOREBOARD FROZEN</div> + {{ /if }} + <table class="boca" border="1"> + {{#each scores}} + {{#if this.user_name}} + <tr> + <td> + {{@index}} + </td> + <td> + {{this.user_name}} + </td> + <td> + {{this.user_full_name}} + </td> + {{#each this.problems}} + <td class="boca-score"> + {{#if this.first_ac_submission_time}} + {{ #if this.balloon_color }} + {{ #if this.first_solve }} + <svg class="table-balloon" viewBox="0 -10 30 30" color="{{ this.balloon_color }}"><use href="/static/star.svg#balloon"/></svg> + {{ else }} + <svg class="balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg> + {{ /if }} + {{ /if }} + {{this.submission_count}}/{{#if (ne this.first_ac_submission_time "*")}}{{this.first_ac_submission_minutes}}{{/if}} + {{else}} + {{#if this.failed_submissions}} + {{ this.failed_submissions }}/- + {{/if}} + {{/if}} + </td> + {{/each}} + <td class="right"> + {{this.solved_count}} ({{ this.penalty }}) + </td> + </tr> + {{else}} + <thead> + <tr> + <th>#</th> + <th>User</th> + <th>Name</th> + {{#each this.problems}} + <th>{{this.label}}</th> + {{/each}} + <th class="right">Total</th> + </tr> + </thead> + <tbody> + {{/if}} + {{/each}} + </tbody> + </table> + </div> +{{ /single_base }} diff --git a/templates/single_tasks.hbs b/templates/single_tasks.hbs new file mode 100644 index 0000000000000000000000000000000000000000..ca2d133b940daa1be6aab1f77b004fd90742a63a --- /dev/null +++ b/templates/single_tasks.hbs @@ -0,0 +1,46 @@ +{{ #> single_base title="Tasks" }} + <div id="contest"> + <table class="boca" border="1"> + <thead> + <tr> + <th>Task #</th> + <th>Time</th> + <th>File</th> + <th>Status</th> + </tr> + </thead> + <tbody> + {{ #each printing_tasks }} + <tr> + <td> + {{ this.uuid }} + </td> + <td> + {{ this.creation_instant }} + </td> + <td> + <a href="printing-tasks/{{ this.uuid }}/contents">Download</a> + </td> + <td> + {{ this.status }} + </td> + </tr> + {{ /each }} + </tbody> + </table> + + <form class="boca" id="single-contest-create-printing-task-form" hx-target="body" hx-swap="innerHTML" hx-encoding="multipart/form-data" hx-post="contests/{{ contest.id }}/printing-tasks/"> + <strong>To submit a file for printing, just fill in the following field:</strong> + + <label> + File name: + <input name="contents" type="file"/> + </label> + + <div class="buttons"> + <button type="submit">Send</button> + <button type="reset">Clear</button> + </div> + </form> + </div> +{{ /single_base }} diff --git a/templates/single_waiting.hbs b/templates/single_waiting.hbs new file mode 100644 index 0000000000000000000000000000000000000000..57c6a0ed138a205a9e05234639433b0ca4e6ac8a --- /dev/null +++ b/templates/single_waiting.hbs @@ -0,0 +1,21 @@ +{{ #> single_base title="Waiting" }} +<div id="timer"></div> +<script> +const timer = document.querySelector('#timer'); +const target = new Date('{{ contest.start_instant }}'); +const MS_PER_SECOND = 1000; +const MS_PER_MINUTE = MS_PER_SECOND*60; +const MS_PER_HOUR = MS_PER_MINUTE*60; +const MS_PER_DAY = MS_PER_HOUR*24; +function updateTimer() { + const diff = Math.max(target - new Date(), 0); + if (diff == 0) { location.reload(); } + timer.innerHTML = `-${ + Math.floor(diff/MS_PER_HOUR%24).toString().padStart(2, '0')}:${ + Math.floor(diff/MS_PER_MINUTE%60).toString().padStart(2, '0')}:${ + Math.floor(diff/MS_PER_SECOND%60).toString().padStart(2, '0')}`; +} +updateTimer(); +setInterval(updateTimer, 1000); +</script> +{{ /single_base }} diff --git a/templates/tasks.hbs b/templates/tasks.hbs new file mode 100644 index 0000000000000000000000000000000000000000..74173c5ac4435456238ebc278de97d7bfc7e84d1 --- /dev/null +++ b/templates/tasks.hbs @@ -0,0 +1,94 @@ +{{ #> base title="Tarefas" }} + <div id="contest"> + <div id="breadcrumb"> + <a href=".">Início</a> + / + <a href="contests/">Competições</a> + / + <a href="contests/{{contest.id}}">{{contest.name}}</a> + / + <a href="contests/{{contest.id}}/tasks">Tarefas</a> + </div> + + <table> + <thead> + <tr> + <th>Momento da Tarefa</th> + <th>Usuário</th> + <th>Descrição</th> + <th>Estado</th> + <th>Ações</th> + </tr> + </thead> + <tbody> + {{ #each tasks }} + <tr class="{{ #if this.done }}done{{ else }}pending{{ /if }}"> + <td> + {{ this.creation_instant }} + </td> + <td> + {{ this.user_name }} - + {{ this.user_full_name }} + </td> + <td> + {{ #if (eq this.t "clarification") }} + Clarificação: + {{ /if }} + {{ #if (eq this.t "balloon") }} + Balão: + {{ #if this.first_solve }} + <svg class="table-balloon" viewBox="0 -10 30 30" color="{{ this.balloon_color }}"><use href="/static/star.svg#balloon"/></svg> + {{ else }} + <svg class="table-balloon" viewBox="-2 0 34 40" color="{{ this.balloon_color }}"><use href="/static/balloon.svg#balloon"/></svg> + {{ /if }} + {{ /if }} + {{ #if (eq this.t "printing") }} + Impressão: + {{ /if }} + {{ this.description }} + </td> + <td> + {{ #if this.done }} + Pronto + {{ else }} + Pendente + {{ /if }} + </td> + <td> + {{ #if (eq this.t "clarification") }} + <a href="contests/{{ ../contest.id }}/clarifications">Ir para Clarificações</a> + {{ /if }} + {{ #if (eq this.t "balloon") }} + <button type="submit" form="form-submission-balloon-delivered-{{ this.uuid }}">Fechar</button> + {{ /if }} + {{ #if (eq this.t "printing") }} + <a href="printing-tasks/{{ this.uuid }}/contents" style="display: inline">Ver Conteúdo</a> + {{ #unless this.done }} + <button type="submit" form="form-printing-task-done-{{ this.uuid }}">Fechar</button> + {{ /unless }} + {{ /if }} + </td> + </tr> + {{ /each }} + </tbody> + </table> + + {{ #each tasks }} + {{ #unless this.done }} + {{ #if (eq this.t "printing") }} + <form id="form-printing-task-done-{{ this.uuid }}" hx-target="body" hx-post="printing-tasks/{{ this.uuid }}/done"></form> + {{ /if }} + {{ #if (eq this.t "balloon") }} + <form id="form-submission-balloon-delivered-{{ this.uuid }}" hx-target="body" hx-post="submissions/{{ this.uuid }}/balloon-delivered"></form> + {{ /if }} + {{ /unless }} + {{ /each }} + </div> + + <div id="submissions" hx-sse="connect:submission_updates/"> + Submissões + <div id="submissions-list" hx-get="submissions/contests/{{ contest.id }}" hx-trigger="load, sse:update_submission"> + {{> submissions }} + </div> + </div> +{{ /base }}